Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 65 additions & 0 deletions .github/workflows/build-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ on:
required: false
default: ""
type: string
smoke_test_labextensions:
description: "After each arch build, run `jupyter labextension list` in the pushed image and assert the extensions pinned in images/jupyterlab/pixi.toml are enabled at their pinned versions. Enable for every image built from the jupyterlab Dockerfile target (including the GPU variant — it installs the same pixi.toml). Skipped on fork PRs (no pushed image to run)."
required: false
default: false
type: boolean
secrets:
QUAY_TOKEN:
required: false
Expand Down Expand Up @@ -96,6 +101,36 @@ jobs:
GPU=${{ inputs.gpu }}
NEBI_IMAGE=${{ inputs.nebi_image }}

- name: "Smoke test: labextensions enabled at pinned versions"
if: env.IS_FORK_PR != 'true' && inputs.smoke_test_labextensions
run: |
ref="ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}@${{ steps.build.outputs.digest }}"
# `jupyter labextension list` writes to stderr and colorizes the status
# words unconditionally (jupyter_server.extension.serverextension
# GREEN_ENABLED / RED_DISABLED / RED_X — guarded only by os.name != "nt"),
# so merge stderr and strip SGR sequences before matching. Keep grep -F
# substring matching: the real line has a trailing "(python, <pkg>)" and
# may carry "*" or a lock emoji after OK.
if ! out=$(docker run --rm "$ref" jupyter labextension list 2>&1); then
printf '%s\n' "$out"
echo "::error::could not run 'jupyter labextension list' in $ref"
exit 1
fi
plain=$(printf '%s\n' "$out" | sed -E 's/\x1b\[[0-9;]*m//g')
printf '%s\n' "$plain"
# Extensions whose pixi.toml pin name matches their labextension name.
# The list is hardcoded rather than derived from pixi.toml because the
# mapping isn't mechanical (jupyterlab_nvdashboard ships the extension
# as jupyterlab-nvdashboard; jhub-apps isn't a labextension at all).
for ext in jupyterlab-launchpad jupyterlab-nebari-mode jupyterlab-gallery jupyterlab-jhub-apps; do
expected=$(sed -nE "s/^${ext} = \"==([^\"]+)\"\$/\\1/p" images/jupyterlab/pixi.toml)
test -n "$expected" || { echo "::error::could not read exact ${ext} pin from images/jupyterlab/pixi.toml"; exit 1; }
grep -Fq " ${ext} v${expected} enabled OK" <<< "$plain" || {
echo "::error::${ext} v${expected} is not 'enabled OK' in $ref"
exit 1
}
done

- name: "Export digest"
if: env.IS_FORK_PR != 'true'
run: |
Expand Down Expand Up @@ -150,6 +185,36 @@ jobs:
GPU=${{ inputs.gpu }}
NEBI_IMAGE=${{ inputs.nebi_image }}

- name: "Smoke test: labextensions enabled at pinned versions"
if: env.IS_FORK_PR != 'true' && inputs.smoke_test_labextensions
run: |
ref="ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}@${{ steps.build.outputs.digest }}"
# `jupyter labextension list` writes to stderr and colorizes the status
# words unconditionally (jupyter_server.extension.serverextension
# GREEN_ENABLED / RED_DISABLED / RED_X — guarded only by os.name != "nt"),
# so merge stderr and strip SGR sequences before matching. Keep grep -F
# substring matching: the real line has a trailing "(python, <pkg>)" and
# may carry "*" or a lock emoji after OK.
if ! out=$(docker run --rm "$ref" jupyter labextension list 2>&1); then
printf '%s\n' "$out"
echo "::error::could not run 'jupyter labextension list' in $ref"
exit 1
fi
plain=$(printf '%s\n' "$out" | sed -E 's/\x1b\[[0-9;]*m//g')
printf '%s\n' "$plain"
# Extensions whose pixi.toml pin name matches their labextension name.
# The list is hardcoded rather than derived from pixi.toml because the
# mapping isn't mechanical (jupyterlab_nvdashboard ships the extension
# as jupyterlab-nvdashboard; jhub-apps isn't a labextension at all).
for ext in jupyterlab-launchpad jupyterlab-nebari-mode jupyterlab-gallery jupyterlab-jhub-apps; do
expected=$(sed -nE "s/^${ext} = \"==([^\"]+)\"\$/\\1/p" images/jupyterlab/pixi.toml)
test -n "$expected" || { echo "::error::could not read exact ${ext} pin from images/jupyterlab/pixi.toml"; exit 1; }
grep -Fq " ${ext} v${expected} enabled OK" <<< "$plain" || {
echo "::error::${ext} v${expected} is not 'enabled OK' in $ref"
exit 1
}
done

- name: "Export digest"
if: env.IS_FORK_PR != 'true'
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
with:
image: nebari-data-science-pack-jupyterlab
target: jupyterlab
smoke_test_labextensions: true
secrets:
QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}

Expand All @@ -67,6 +68,7 @@ jobs:
build_arm64: false
base_image: "nvidia/cuda:12.8.1-base-ubuntu24.04"
gpu: true
smoke_test_labextensions: true
secrets:
QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}

Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/nebi-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ Leaving `tag` empty disables the init container entirely — no Nebi in user pod
pinned per chart release (`scripts/bump_image_tags.py` only handles the JupyterLab images),
so override it to test a PR build or to roll forward between chart releases.

When overriding, keep the binary **≥ v0.15**: `jupyterlab-launchpad` (≥ 1.1.1, pinned in
the JupyterLab image) listens for `nebi:job-completed` from this binary to refresh the
kernel list immediately after an environment build. Older nebi never sends the message,
so kernels silently fall back to the ~61s `KernelSpecManager` poll with no error logged.

## The token exchange

Nebi needs a per-user credential, and the hub is the only component holding the user's
Expand Down Expand Up @@ -212,6 +217,7 @@ CA merge step is ordered before it, so the bundle is ready. See
| Empty environment dropdown in jhub-apps | Exchange failure, or `auth_state` missing — `kubectl logs deploy/hub \| grep nebi-envs`. |
| Registry changes not visible | Hub not restarted since the change. |
| Apps missing packages in a Nebi env | `jhub-app-proxy-version` below v0.2.3. |
| New kernels take ~61s to appear after an env build | Checkable: `nebi.image.tag` below v0.15 (no `nebi:job-completed` sender — `kubectl -n data-science exec <user-pod> -- nebi --version`), or launchpad below 1.1.1 / disabled (`kubectl -n data-science exec <user-pod> -- jupyter labextension list 2>&1 \| grep launchpad`). Not checkable — no signal anywhere in this stack: the Nebi tab wasn't open when the build finished, or nebi fell back to team mode after a slow start (`/version` unreachable for ~900ms at handshake time). Re-test with the Nebi tab open. |

```bash
# Is the binary in the pod?
Expand Down
6 changes: 4 additions & 2 deletions docs/src/content/docs/server-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ image field; the inner `profile_options.image.choices.default` is what the Jupyt
profile selector shows. Both must be bumped alongside `jupyterhub.singleuser.image.tag`.

z2jh values cannot reference other values, so the duplication is unavoidable.
`scripts/bump_image_tags.py` syncs all three on an automated bump — hand edits have to keep
up on their own.
`scripts/bump_image_tags.py` syncs all three on an automated bump, and
`tests/unit/test_image_ref_sync.py` fails CI if a hand edit lets any jupyterlab-tagged ref
in this repo's `values.yaml` drift from `singleuser.image`. Choices pointing at other
images (like the R image above) are left alone by both.
:::

## Gating profiles by group
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/values-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Admin guide: [Nebi integration](/nebi-integration/).
| Field | Default | What it does |
|---|---|---|
| `nebi.image.repository` | `quay.io/nebari/nebi` | Binary copied into user pods by an init container. |
| `nebi.image.tag` | `sha-5ca877a` | Pinned per chart release. Empty disables the init container. |
| `nebi.image.tag` | `sha-5ca877a` | Pinned per chart release. Must be ≥ v0.15 for `jupyterlab-launchpad` (≥ 1.1.1) to receive `nebi:job-completed`; older nebi never sends it and kernel refresh silently falls back to the ~61s poll. Empty disables the init container. |
| `nebi.image.pullPolicy` | `IfNotPresent` | — |
| `nebi.remoteURL` | `""` *(derived)* | Browser-facing Nebi URL, used for the OIDC redirect. |
| `nebi.internalURL` | `""` *(derived)* | In-cluster URL for the token-exchange path. |
Expand Down
20 changes: 10 additions & 10 deletions images/jupyterlab/pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion images/jupyterlab/pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ jupyterlab_nvdashboard = "==0.12.0"
# argo-jupyter-scheduler = "==2024.6.1" # disabled until service is configured
jhub-apps = "==2026.8.1"
jupyterlab-nebari-mode = "==0.3.0"
jupyterlab-launchpad = "==1.1.0"
# >=1.1.1 listens for nebi:job-completed to refresh kernels; the sender is the
# nebi binary pinned at values.yaml nebi.image.tag, which must be >= v0.15.
jupyterlab-launchpad = "==1.1.1"
Comment thread
tylerpotts marked this conversation as resolved.
jupyterlab-gallery = "==0.6.3"
jupyterlab-jhub-apps = "==0.3.1"
Loading
Loading