Skip to content

build: bump jupyterlab-launchpad to 1.1.1 in jupyterlab image #620

build: bump jupyterlab-launchpad to 1.1.1 in jupyterlab image

build: bump jupyterlab-launchpad to 1.1.1 in jupyterlab image #620

Workflow file for this run

name: Test Deployment
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit:
name: unit
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Set up isolated Python env
run: |
uv venv .venv-unit --python 3.10
# Pinned versions the hub image runs against — tests exercise
# the real oauthenticator + jupyterhub contracts, not a stub.
uv pip install --python .venv-unit/bin/python \
jupyterhub==5.5.0 \
oauthenticator==17.4.0 \
jupyterhub-kubespawner==7.0.0 \
pytest
- name: pytest tests/unit
run: .venv-unit/bin/python -m pytest tests/unit -v --color=yes
# Two roles for prep:
# 1. Cache the multi-GB singleuser image tar so every matrix leg
# restores it from cache instead of paying the docker-pull cost.
# Key by the chart-rendered image ref so values.yaml bumps
# invalidate automatically.
# 2. Collect the list of pytest node IDs from tests/e2e/ and emit
# it as a JSON array the matrix consumes. Adding/removing tests
# flows through the matrix automatically with no workflow edits.
prep:
name: prep (image + test matrix)
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
ref: ${{ steps.img.outputs.ref }}
tar: ${{ steps.img.outputs.tar }}
tests: ${{ steps.collect.outputs.tests }}
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Resolve singleuser image ref
id: img
run: |
REF=$(yq -r '.jupyterhub.singleuser.image | .name + ":" + .tag' values.yaml)
echo "ref=$REF" >> "$GITHUB_OUTPUT"
echo "tar=/tmp/singleuser.tar" >> "$GITHUB_OUTPUT"
- name: Cache singleuser image
id: img-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.img.outputs.tar }}
key: singleuser-${{ steps.img.outputs.ref }}
- name: Pull + save singleuser image (cache miss only)
if: steps.img-cache.outputs.cache-hit != 'true'
run: |
docker pull "${{ steps.img.outputs.ref }}"
docker save "${{ steps.img.outputs.ref }}" -o "${{ steps.img.outputs.tar }}"
- name: Collect e2e test node IDs into a JSON array
id: collect
run: |
# Emit `[{"id":"<short>","path":"<full pytest node id>"}, …]`.
# `id` becomes the matrix label so the GitHub UI groups the
# legs as `e2e / <short>` instead of the full file path
# noise. `path` is what pytest needs to run the test.
# tests/e2e/pytest.ini sets rootdir to tests/e2e, so the
# collected IDs come without the prefix; add it back.
uvx pytest tests/e2e --collect-only -q 2>/dev/null \
| grep -E '^test_.*::' \
| while IFS= read -r node; do
# node = test_FILE.py::test_FN[param]
# short = FN[param] (or FN if not parametrized)
short=$(echo "$node" | sed -E 's|^test_[^.]+\.py::test_||')
jq -n --arg id "$short" --arg path "tests/e2e/$node" \
'{id: $id, path: $path}'
done \
| jq -s -c . > /tmp/tests.json
echo "tests=$(cat /tmp/tests.json)" >> "$GITHUB_OUTPUT"
echo "Collected:"
cat /tmp/tests.json | jq -r '.[] | "\(.id) \t\(.path)"'
# Matrix-per-test e2e: each leg gets its own kind cluster, helm
# install, and runs ONE pytest node id. Wall time = max(leg) instead
# of sum(tests). Adding a test is free — prep job's collection adds
# a matrix leg automatically.
e2e:
name: e2e / ${{ matrix.test.id }}
runs-on: ubuntu-latest
needs: prep
timeout-minutes: 10
strategy:
fail-fast: false
max-parallel: 10
matrix:
test: ${{ fromJSON(needs.prep.outputs.tests) }}
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install kind
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with:
version: v0.27.0
install_only: true
- name: Install Helm
run: curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Restore singleuser image from cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ needs.prep.outputs.tar }}
key: singleuser-${{ needs.prep.outputs.ref }}
fail-on-cache-miss: true
# Sanitise the pytest node id into a kind-cluster name: kind
# names are DNS-1123 (lowercase alnum + dashes) and max 38 chars.
# SHA1-truncate so duplicates can't collide.
- name: Derive cluster name
id: cluster
run: |
SAFE=$(echo '${{ matrix.test.path }}' | sha1sum | cut -c1-12)
echo "name=nbe2e-${SAFE}" >> "$GITHUB_OUTPUT"
- name: Create kind cluster
run: kind create cluster --name "${{ steps.cluster.outputs.name }}" --wait 60s
- name: Side-load singleuser image into kind node
run: kind load image-archive "${{ needs.prep.outputs.tar }}" --name "${{ steps.cluster.outputs.name }}"
- name: Run single test
env:
KIND_CLUSTER: ${{ steps.cluster.outputs.name }}
KIND_KEEP: "1"
PYTHONUNBUFFERED: "1"
run: uvx pytest "${{ matrix.test.path }}" -v --color=yes
- name: Hub logs on failure
if: failure()
run: kubectl logs -l component=hub --tail=200 || true
# Integration tests for files/keycloak_rbac_bootstrap.py against a
# real Keycloak. The bootstrap is the chart's only piece of Python
# that talks to Keycloak directly — KC version skew or payload
# changes would break it silently in prod. nebari-dev/action-nebari
# -sandbox spins up NIC's foundational stack (Keycloak + nebari realm
# included) in ~3 min on a vanilla runner; the tests then run against
# that live KC via a port-forward.
integration-rbac-bootstrap:
name: integration / rbac-bootstrap
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Provision Nebari sandbox (kind + NIC platform stack)
id: sandbox
uses: nebari-dev/action-nebari-sandbox@9ac369ebf87ac2ae217504dcbf824c77f70e429a # v3.0.0
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Wait for Keycloak service
env:
KUBECONFIG: ${{ steps.sandbox.outputs.kubeconfig }}
run: |
kubectl -n keycloak rollout status statefulset/keycloak-keycloakx --timeout=300s
kubectl -n keycloak get svc/keycloak-keycloakx-http
- name: Port-forward Keycloak admin
env:
KUBECONFIG: ${{ steps.sandbox.outputs.kubeconfig }}
run: |
# Background port-forward. The runner stops on workflow exit,
# so no explicit cleanup needed; ``timeout-minutes`` is the
# safety net.
nohup kubectl -n keycloak port-forward svc/keycloak-keycloakx-http \
18080:8080 >/tmp/kc-pf.log 2>&1 &
# Poll for the local port — port-forward exits non-zero on
# any error so we'd see it via the log dump on failure.
for i in $(seq 1 30); do
if curl -sf http://localhost:18080/realms/master/.well-known/openid-configuration >/dev/null; then
echo "KC reachable on :18080 (attempt $i)"
exit 0
fi
sleep 1
done
echo "::error::Keycloak port-forward never came up"
cat /tmp/kc-pf.log
exit 1
- name: Run integration tests
env:
KUBECONFIG: ${{ steps.sandbox.outputs.kubeconfig }}
KC_URL: http://localhost:18080
KC_ADMIN_PASSWORD: ${{ steps.sandbox.outputs.keycloak-admin-password }}
PYTHONUNBUFFERED: "1"
run: uvx pytest tests/integration -v --color=yes
- name: Keycloak logs on failure
if: failure()
env:
KUBECONFIG: ${{ steps.sandbox.outputs.kubeconfig }}
run: |
kubectl -n keycloak get pods
kubectl -n keycloak logs statefulset/keycloak-keycloakx --tail=200 || true
echo "--- port-forward log ---"
cat /tmp/kc-pf.log || true