Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1ef138b
ci: add k8s stack PR preview via labeled deploy + Cloudflare Tunnel
aktech Aug 3, 2026
72dab46
ci: temporarily trigger smoketest on this PR for iteration
aktech Aug 3, 2026
52d9f86
fix: move cloudflared --no-autoupdate flag before the run subcommand
aktech Aug 3, 2026
2818f48
ci: use single-level github.fyi hostnames for the preview tunnel
aktech Aug 3, 2026
fedb68f
ci: drop temporary pull_request trigger from smoketest workflow
aktech Aug 3, 2026
b22f710
fix: fetch chart dependencies before helm install in preview workflow
aktech Aug 3, 2026
1c7618a
ci: post smoketest URL as a PR comment via optional pr_number input
aktech Aug 3, 2026
0a75683
fix: register the jupyterhub chart repo before dependency build
aktech Aug 3, 2026
1eec127
fix: opt preview namespace into nic-operator management, add failure …
aktech Aug 3, 2026
17f723b
fix: disable nebariapp for preview deploy, hub crash-loops otherwise
aktech Aug 3, 2026
a43dfb7
fix: set jupyterhub.custom.external-url to the tunnel hostname
aktech Aug 3, 2026
57f1b00
Merge branch 'main' into add-k8s-preview-workflow
aktech Aug 5, 2026
a146616
ci: show deploy/expiry timestamps in preview PR comments
aktech Aug 5, 2026
c0ca336
Merge branch 'add-k8s-preview-workflow' of github.com:nebari-dev/neba…
aktech Aug 5, 2026
65651b4
fix: make tunnel creation idempotent, retries reuse the run_id
aktech Aug 5, 2026
774c0d6
Merge branch 'main' into add-k8s-preview-workflow
aktech Aug 31, 2026
74d3d49
ci: source preview domain from a repo Variable instead of hardcoding it
aktech Aug 31, 2026
71817f5
ci: switch preview domain to openteams.app, flatten per-PR hostname
aktech Aug 31, 2026
4919f73
ci: migrate k8s-preview to action-nebari-sandbox v3 (kind, not k3d)
aktech Aug 31, 2026
c849c85
ci: bump kind CLI to v0.33.0 in k8s-preview for containerd config v4
aktech Aug 31, 2026
cec54be
ci: always dump hub logs in k8s-preview to diagnose jhub-apps startup
aktech Aug 31, 2026
f577f6a
ci: smoke-test jhub-apps directly in k8s-preview before opening the t…
aktech Aug 31, 2026
10fa371
ci: capture hub logs after the tunnel closes, not just at startup
aktech Aug 31, 2026
26f7b65
ci: cache docker build, enable Keycloak auth, always start tmate debug
aktech Aug 31, 2026
d15efbc
ci: wait for the operator's Keycloak secret instead of blocking helm …
aktech Aug 31, 2026
2b6f1a9
ci: cover the keycloak namespace in the deploy-failure debug dump
aktech Aug 31, 2026
494eee4
ci: label the preview namespace so nic-operator actually reconciles it
aktech Aug 31, 2026
be49af8
docs: update k8s-preview comments to match the current auth setup
aktech Aug 31, 2026
daba573
ci: diagnose why the operator's issuer-url secret key comes back empty
aktech Aug 31, 2026
47e6606
ci: wait for issuer-url specifically, not just the secret's existence
aktech Aug 31, 2026
612f07d
ci: retry the hub restart until it actually picks up the operator secret
aktech Aug 31, 2026
b0c12f0
ci: fix the actual bug -- hub was mounting the wrong secret name
aktech Aug 31, 2026
0d041ce
ci: use --set-json for extraVolumes, not indexed --set
aktech Aug 31, 2026
0b2e76e
ci: expose Keycloak's own login page through a second tunnel route
aktech Aug 31, 2026
f925800
ci: point Keycloak's own hostname at the public route instead of rewr…
aktech Aug 31, 2026
c85f9b4
ci: fix Keycloak hostname via GitOps instead of a direct kubectl patch
aktech Aug 31, 2026
78b0086
ci: create a simple reviewer/admin test user in Keycloak
aktech Aug 31, 2026
6c96912
ci: route hub's Keycloak backchannel calls in-cluster, not through Ac…
aktech Aug 31, 2026
d5c27c7
fix: point jhub-apps' hub-API client at localhost, not the hub Service
aktech Aug 31, 2026
fdec180
fix: the env-dict override for JUPYTERHUB_API_URL doesn't survive spawn
aktech Aug 31, 2026
3ca96ef
ci: pre-fill email/name on the reviewer test user
aktech Aug 31, 2026
928adec
fix: the JUPYTERHUB_API_URL rewrite for jhub-apps never actually ran
aktech Aug 31, 2026
ba3fd45
ci: bound the tunnel step to 20min instead of the job timeout
aktech Sep 1, 2026
d284543
ci: add a GitHub Deployment for the preview, not just a comment
aktech Sep 1, 2026
044a28e
ci: fix the tunnel step's exit-124 handling under bash -e
aktech Sep 1, 2026
7371cc1
ci: replace the preview comment's prose with a Vercel-style table
aktech Sep 1, 2026
f2896bc
ci: fix the deployment table's Project column
aktech Sep 1, 2026
7b1df6a
ci: show a live-updating expiry time instead of a static UTC string
aktech Sep 1, 2026
26bbfe1
ci: update the preview comment again when it expires
aktech Sep 1, 2026
f6c9f4a
ci: extend the preview's lifetime in place via an extend-preview label
aktech Sep 1, 2026
6bdfafb
Merge branch 'main' into add-k8s-preview-workflow
aktech Sep 1, 2026
3693d08
ci: don't let extend-preview cancel the run it's meant to extend
aktech Sep 1, 2026
9ab529d
refactor: add a testable Python foundation for k8s-preview.yaml's CI …
aktech Sep 2, 2026
748e6d5
refactor: move Cloudflare Tunnel/DNS logic into scripts/preview/cloud…
aktech Sep 2, 2026
6844087
refactor: move Keycloak + k8s readiness logic into scripts/preview/
aktech Sep 2, 2026
037c993
refactor: move remaining GitHub API/comment logic into scripts/preview/
aktech Sep 2, 2026
9305abe
refactor: move the extend-preview tunnel loop into scripts/preview/tu…
aktech Sep 2, 2026
df6d007
ci: dump ArgoCD Application status on deploy failure
aktech Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions .github/workflows/k8s-preview-tunnel-smoketest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
name: K8s Preview Tunnel Smoketest
# Manual, on-demand check of the Cloudflare Tunnel + DNS + Access plumbing
# that k8s-preview.yaml depends on — without paying for a full k3d +
# platform-stack + helm deploy on every iteration. Serves a static
# index.html instead of JupyterHub; same tunnel/DNS create+configure+
# delete API calls as the real workflow, same secrets. Run this first
# when validating the Cloudflare-side setup (token scopes, Access
# application, GitHub identity provider); once a visit to the printed
# URL round-trips through GitHub SSO successfully, k8s-preview.yaml's
# tunnel plumbing is known-good and any remaining issue is in the
# k3d/helm/chart side, not Cloudflare.

on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to post the smoketest URL to (optional; skips the comment if blank)'
required: false

env:
# Same repo Variable k8s-preview.yaml uses (Settings -> Actions ->
# Variables -> PREVIEW_DOMAIN) so the two workflows can't drift apart.
PREVIEW_DOMAIN: ${{ vars.PREVIEW_DOMAIN }}
CLOUDFLARED_VERSION: "2026.7.3"
CLOUDFLARED_SHA256: "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17"

jobs:
smoketest:
name: Tunnel smoketest
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
steps:
- name: Serve a trivial static page
run: |
mkdir -p /tmp/preview-test
cat > /tmp/preview-test/index.html <<'EOF'
<!doctype html>
<html><body><h1>Tunnel smoketest OK</h1></body></html>
EOF
python3 -m http.server 8000 --directory /tmp/preview-test \
> /tmp/http-server.log 2>&1 &

- name: Install cloudflared
run: |
curl -fsSL -o /tmp/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
echo "${CLOUDFLARED_SHA256} /tmp/cloudflared" | sha256sum -c -
chmod +x /tmp/cloudflared

- name: Create Cloudflare Tunnel for this run
id: cf_tunnel
env:
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_TUNNEL_ACCOUNT_ID }}
PREVIEW_HOSTNAME: smoketest-${{ github.run_id }}.${{ env.PREVIEW_DOMAIN }}
run: |
tunnel_secret=$(openssl rand -base64 32)
echo "::add-mask::${tunnel_secret}"

tunnel_name="smoketest-${{ github.run_id }}"
create_resp=$(curl -sS -X POST \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg name "$tunnel_name" --arg secret "$tunnel_secret" \
'{name: $name, config_src: "cloudflare", tunnel_secret: $secret}')")
tunnel_id=$(jq -r '.result.id // empty' <<< "$create_resp")

# Same retry-safety as the real workflow: a retry reuses run_id,
# so reuse the existing tunnel by name on a 409 name conflict.
if [ -z "$tunnel_id" ]; then
echo "::warning::Tunnel create failed (likely a name conflict from a retry), looking up existing tunnel named ${tunnel_name}: $create_resp"
tunnel_id=$(curl -fsS "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel?name=${tunnel_name}&is_deleted=false" \
-H "Authorization: Bearer ${CF_API_TOKEN}" | jq -r '.result[0].id // empty')
fi
if [ -z "$tunnel_id" ]; then
echo "::error::Tunnel creation failed and no existing tunnel named ${tunnel_name} found: $create_resp"
exit 1
fi
echo "TUNNEL_ID=${tunnel_id}" >> "$GITHUB_ENV"

token_resp=$(curl -fsS \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${tunnel_id}/token" \
-H "Authorization: Bearer ${CF_API_TOKEN}")
tunnel_token=$(jq -r '.result' <<< "$token_resp")
echo "::add-mask::${tunnel_token}"
echo "TUNNEL_TOKEN=${tunnel_token}" >> "$GITHUB_ENV"

curl -fsS -X PUT \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${tunnel_id}/configurations" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg host "$PREVIEW_HOSTNAME" \
'{config: {ingress: [{hostname: $host, service: "http://localhost:8000"}, {service: "http_status:404"}]}}')" \
> /dev/null

- name: Point DNS at the tunnel
id: cf_dns
env:
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_API_TOKEN }}
PREVIEW_HOSTNAME: smoketest-${{ github.run_id }}.${{ env.PREVIEW_DOMAIN }}
run: |
zone_id=$(curl -fsS "https://api.cloudflare.com/client/v4/zones?name=${PREVIEW_DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" | jq -r '.result[0].id')
if [ -z "$zone_id" ] || [ "$zone_id" = "null" ]; then
echo "::error::Could not resolve zone id for ${PREVIEW_DOMAIN}"
exit 1
fi
echo "ZONE_ID=${zone_id}" >> "$GITHUB_ENV"

record_resp=$(curl -fsS -X POST "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg host "$PREVIEW_HOSTNAME" --arg target "${TUNNEL_ID}.cfargotunnel.com" \
'{type: "CNAME", name: $host, content: $target, proxied: true}')")
record_id=$(jq -r '.result.id' <<< "$record_resp")
if [ -z "$record_id" ] || [ "$record_id" = "null" ]; then
echo "::error::DNS record creation failed: $record_resp"
exit 1
fi
echo "DNS_RECORD_ID=${record_id}" >> "$GITHUB_ENV"

echo "## Smoketest URL" >> "$GITHUB_STEP_SUMMARY"
echo "https://${PREVIEW_HOSTNAME}" >> "$GITHUB_STEP_SUMMARY"
echo "Visiting it should challenge you with Cloudflare Access GitHub SSO," >> "$GITHUB_STEP_SUMMARY"
echo "then show 'Tunnel smoketest OK' once you're through." >> "$GITHUB_STEP_SUMMARY"
echo "Live for up to 15 minutes (this job's timeout)." >> "$GITHUB_STEP_SUMMARY"
echo "url=https://${PREVIEW_HOSTNAME}" >> "$GITHUB_OUTPUT"

- name: Compute deployment timestamps
id: timestamps
run: |
echo "deployed_at=$(date -u +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"
echo "expires_at=$(date -u -d '+15 minutes' +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"

- name: Comment smoketest link on PR
if: github.event.inputs.pr_number != ''
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: k8s-preview-smoketest
number_force: ${{ github.event.inputs.pr_number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
message: |
**Tunnel smoketest** (static page, not the real stack):
${{ steps.cf_dns.outputs.url }}

Deployed: ${{ steps.timestamps.outputs.deployed_at }} · Expires: ${{ steps.timestamps.outputs.expires_at }}

Sign-in via Cloudflare Access (GitHub SSO), then should show "Tunnel smoketest OK".

- name: Run tunnel until the job times out
run: /tmp/cloudflared tunnel --no-autoupdate run --token "${TUNNEL_TOKEN}"

- name: Delete DNS record
if: always()
env:
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_API_TOKEN }}
run: |
[ -n "${ZONE_ID:-}" ] && [ -n "${DNS_RECORD_ID:-}" ] || exit 0
curl -fsS -X DELETE \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${DNS_RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" || true

- name: Delete Cloudflare Tunnel
if: always()
env:
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_TUNNEL_ACCOUNT_ID }}
run: |
[ -n "${TUNNEL_ID:-}" ] || exit 0
curl -fsS -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" || true
Loading
Loading