Skip to content

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

Merged
tylerpotts merged 5 commits into
mainfrom
bump-launchpad-1.1.1
Aug 31, 2026
Merged

build: bump jupyterlab-launchpad to 1.1.1 in jupyterlab image#241
tylerpotts merged 5 commits into
mainfrom
bump-launchpad-1.1.1

Conversation

@tylerpotts

@tylerpotts tylerpotts commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Bump jupyterlab-launchpad from 1.1.0 to 1.1.1 in images/jupyterlab/pixi.toml (upstream release: https://github.com/nebari-dev/jupyterlab-launchpad/releases/tag/v1.1.1). The whole content of 1.1.1 is Refresh Launchpad kernels after Nebi workspace jobs complete jupyterlab-launchpad#87: Launchpad listens for nebi:job-completed from the Nebi iframe and refreshes kernels immediately instead of waiting on the ~61s KernelSpecManager poll.
  • Regenerate pixi.lock with pixi update jupyterlab-launchpad (Linux container). requires_dist is identical to 1.1.0, so only the launchpad wheel entry changes.
  • Record the nebi >= v0.15 floor everywhere a deployer looks: comment above the pin in pixi.toml, comment at values.yaml nebi.image.tag, override guidance and a troubleshooting row in docs/nebi-integration.md.
  • Add tests/unit/test_image_ref_sync.py: the profiles' image refs and choice display_names must match singleuser.image, and hub.image.tag must match singleuser.image.tag — a half-bump now fails the unit job instead of e2e silently testing a stale image.
  • Add a post-build smoke step to build-image.yaml (enabled for the jupyterlab image): jupyter labextension list against the pushed digest, asserting the launchpad version pinned in pixi.toml is enabled.

Closes #227. The sender half is already in place: nebari-dev/nebi#538 shipped in nebi v0.15 and #240 pinned nebi.image.tag: sha-5ca877a on main. This PR completes the round trip.

Scope caveat: the listener only works while the Nebi tab is mounted (useHostJobNotifications gates on isEmbedded() && isLocalMode, and the poll runs only while the Nebi React tree is rendered). Start an env build, switch away so the tab unmounts, and the completion message is never posted — that flow still falls back to the ~61s poll. So this improves the tab-open path, which is the common one, but does not eliminate the delay in #227 for every flow.

What CI does and does not prove

  • The jupyterlab amd64/arm64 build legs run pixi install --locked, so they prove the lock resolves and installs. The new smoke step additionally proves the pixi.toml-pinned extensions (launchpad included) are enabled OK in the built image, and fails loudly if docker run itself fails.
  • The first e2e runs on this PR ran against sha-08ea5e4 (1.1.0) and did not exercise 1.1.1. values.yaml is now pointed at the PR build sha-7c0599c via scripts/bump_image_tags.py so subsequent e2e legs spawn the 1.1.1 image. The post-merge bump-image-tags workflow rewrites these tags anyway.
  • Nothing in e2e exercises the postMessage handshake itself (needs a browser, a deployed Nebi + Keycloak, and a real env-build job). The checklist below is the verification for the handshake — all three items have been run, with dated evidence on each.

Release note

Do not bump Chart.yaml until the post-merge chore: bump image tags commit lands. release.yaml packages values.yaml as it stands, so a chart bump before that commit would ship a chart still pinning 1.1.0 and #227 would not actually be closed.

Manual verification (nebi sha-5ca877a + jupyterlab sha-7c0599c)

  • jupyter labextension list reports launchpad 1.1.1 — verified 2026-08-31 via docker run against sha-7c0599c: jupyterlab-launchpad v1.1.1 enabled OK (python, jupyterlab_launchpad). Also enforced in CI by the new smoke step.
  • With the Nebi tab open, install a kernel not yet in the kernelspec list; the new kernel appears in Launchpad within seconds, no reload. Run twice. — verified 2026-08-31 on tyler-hetzner-dev (nebi sha-5ca877a + jupyterlab sha-7c0599c), two runs, kernel appeared within seconds both times.
  • Tab-closed path (still supported; unchanged by this PR): same flow with the Nebi tab closed (or on sha-08ea5e4) shows the ~61s delay. Reload the page right before installing so the KernelSpecManager poll clock resets — a kernel landing late in the 61s cycle looks instant by luck. — verified 2026-08-31 on the same deployment: page reloaded before install, Nebi tab closed before build completion, kernel appeared on the poll (~61s) as expected.

Picks up the latest upstream release. Dependency set is unchanged from
1.1.0, so only the launchpad wheel entry moves in the lock file.

@viniciusdc viniciusdc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went over this one carefully. The change itself is correct and correctly scoped — two files matches every sibling bump, and the post-merge tag automation owns values.yaml, so there's nothing to add there. Verified the sha256 against PyPI (24a7af66... matches the published 1.1.1 wheel) and requires_dist is byte-identical to 1.1.0, so your "nothing else in the resolution changes" holds. Nothing here blocks merging.

Two inline comments on the lock and the pin. The rest doesn't land on a changed line, so it's here.

Test plan item 2 won't do what it says. test.yaml:61 only reads singleuser.image.tag for the prep cache and the kind side-load — the pod that actually spawns takes its image from the default profile's kubespawner_override.image at values.yaml:507, because _hub.py:56 posts with no profile options so the default: true profile wins. So bumping only the singleuser tag side-loads the PR image and then spawns sha-08ea5e4 over the network anyway. scripts/bump_image_tags.py 435c121 syncs all of them (bare sha — the script adds the sha- prefix itself, passing sha-435c121 gives you sha-sha-435c121). And use sha-435c121 rather than pr-241: pr-241 is mutable and the e2e cache key is the ref string, so a later push to the branch rebuilds the tag while every leg happily restores the stale tar.

Worth saying in the body that the green e2e legs didn't test 1.1.1. They ran against sha-08ea5e4, which carries 1.1.0, and would be equally green if this bump were broken. Not your fault — it's just how the pipeline resolves the image — but 31 green checks read as coverage to anyone skimming. The genuine signal is the jupyterlab amd64/arm64 legs: Dockerfile:94 runs pixi install --locked, so those do prove the lock resolves and installs.

Can you link #227? Upstream launchpad#87 is the whole content of 1.1.1 and opens with "Launchpad side fix for data-science-pack issue 227", and @kcpevey called this bump the step before the ds-pack release on that thread. The part that's invisible right now and is really the point of the PR: the sender half is already deployed — nebi#538 merged 2026-08-27, v0.15 published at 5ca877a, and values.yaml:256 already pins sha-5ca877a. So once this lands the round trip is complete and actually testable. Release notes are --generate-notes, so the body is the only place this gets recorded.

Careful with the Chart.yaml bump. release.yaml fires on a push touching Chart.yaml and packages values.yaml as it stands, but the tag carrying 1.1.1 only gets written post-merge by bump-image-tags.yaml. If the chart goes to 0.1.6 before that bot commit lands, the released chart still pins sha-08ea5e4 — 1.1.0 — and #227 isn't actually closed. Leaving it at 0.1.5 here is right, just worth confirming the chart bump waits for the chore: bump image tags commit.

On testing it — IMO add nothing to this repo, verify by hand. nebi:job-completed doesn't appear anywhere in this repo, both halves of the contract live upstream, and #87 already ships 193 lines of unit coverage for the listener. Reproducing it here needs a browser driver (tests/e2e is HTTP + kubectl exec only), a deployed Nebi + Keycloak, and a real env-build job — that's a new test stack, not a new test. What I'd do manually, on nebi sha-5ca877a + jupyterlab sha-435c121:

  1. jupyter labextension list reports launchpad 1.1.1.
  2. Open the Nebi tab and leave it open — the listener only fires while the iframe is mounted.
  3. Trigger an install for a kernel that isn't already in the kernelspec list.
  4. Time it from the Nebi pill flipping to complete. Should be seconds, no page reload; >60s means the handshake didn't fire.
  5. Negative control, worth the 30 seconds: same thing with the tab closed, or on sha-08ea5e4, and confirm the ~61s wait is still there. Otherwise you haven't separated the fix from cache-timing luck.

Two follow-ups I'd open separately rather than bolt onto this bump. The e2e suite structurally can't run against the PR-built image today — worth fixing, though note a hand edit to values.yaml on main is transient since the bot rewrites it on the next build. And an extension-presence leg (kubectl exec, the pattern at test_shared_storage.py:75, assert launchpad shows enabled at the pinned version) would catch the one class nothing catches now: an extension pixi installs fine but that's broken or disabled at runtime. Costs a full matrix leg and doesn't touch the postMessage coupling, so only if you think that class is real. Also nebi-integration.md:207-214 has no troubleshooting row for #227's symptom — purely additive, nothing existing goes stale.

A couple of things I checked and deliberately don't think are worth raising: PyPI attestations are missing at 1.1.1 and 1.1.0 alike (upstream publishes through jupyter_releaser/twine, which doesn't mint PEP 740), so pre-existing and not a regression. The postMessage origin check is present and correct upstream and the refresh is flood-coalesced, so there's no exploit there. And the pin resolution is clean — jupyter_server 2.20.0 satisfies launchpad's >=2.0.1,<3.

Comment thread images/jupyterlab/pixi.lock Outdated
Comment thread images/jupyterlab/pixi.toml
- regenerate pixi.lock with pixi update jupyterlab-launchpad so entries
  keep pixi's sort order
- document that launchpad >=1.1.1 depends on the nebi binary >= v0.15
Temporary: the post-merge bump-image-tags workflow rewrites these.
@tylerpotts

tylerpotts commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review-body items in 793ba18 and 26b59e9:

Not done here: the two follow-ups (e2e can't run against a PR-built image without a hand edit; extension-presence leg / troubleshooting row for #227). Those are separate issues; happy to open them if you want them tracked.

@viniciusdc viniciusdc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock came back genuinely clean this time and the tag sync is byte-identical to script output — I re-ran scripts/bump_image_tags.py 7c0599c over the head and it's a no-op with an empty diff, so all 9 refs agree. One thing I'd settle before merging, and it isn't the code.

The fix only works while the Nebi tab is mounted

useHostJobNotifications gates on isEmbedded() && isLocalMode, and the poll only runs while the Nebi React tree is rendered. So: start an env build, switch away so the tab unmounts, and the completion message is never posted — you're back to the flat 61s KernelSpecManager cycle.

#227 asks about "delay between environment completing and showing up as available", unqualified, and merging auto-closes it. That's a partial fix landing as a complete one. One sentence in the body would cover it.

It also reframes the checklist: item 3's "Nebi tab closed" isn't only a negative control, it's a still-supported path this PR doesn't improve.

The manual verification is the test suite here, and it hasn't been run

Body says "Verified by hand instead, see below" — but the three items below are all - [ ] and nothing records a result. Your own comment says they're "a checklist to run", so I think the sentence is just stale tense. Worth fixing either way since nothing in CI touches the handshake.

Two things while you're in there: there are three items, not five (the "five" is in your comment). And item 1 doesn't need a human at all — jupyter labextension list is a one-line docker run against the built image, which would sit nicely as a post-build smoke step in build-image.yaml, since that workflow has no post-build verification of any kind today.

On item 3 — the confound to rule out isn't the image, it's phase in the 61s cycle. A kernel landing 55s in looks instant on 1.1.1 by luck. Reload the page right before installing so the poll clock resets and the no-fix floor really is ~61s, then repeat item 2 twice.

The nebi >= v0.15 floor is only recorded where no deployer looks

images/jupyterlab/pixi.toml:81-82 is the only record of the coupling in the whole repo. Meanwhile nebi-integration.md:77-79 tells deployers to override nebi.image.tag "to test a PR build or to roll forward" with no floor named, and values.yaml:250-256 says sha-5ca877a == the nebi v0.15 tag commit without saying what breaks below it. Roll nebi back and the 61s poll returns silently — no log line anywhere.

A numeric gate is impossible since that's a content sha, but we already have the pattern for exactly this ungateable case with jhub-app-proxy-version: floor at the value site (values.yaml:393-396), prose at the override site (nebi-integration.md:193-195), and a symptom row (nebi-integration.md:214). Three lines, same shape.

Worth folding in one more: isLocalMode() comes from nebi's /version, and nebi's modeStore falls back to team mode after 3 failed attempts (~900ms). NEBI_MODE: "local" is hardcoded at images/nebi/jupyter_server_config.py:37 so this only bites on a slow nebi start, but the handshake can no-op after a transient startup race with nothing in the logs.

A ~20-line unit assert would guard the hand edit

e2e's cache key and kind side-load come from jupyterhub.singleuser.image (test.yaml:61, :150), but the pod that actually spawns comes from the default profile — _hub.py:56-65 POSTs /server with no body, so kubespawner falls through to the default: true profile and its profile_options default choice overwrite the image. Today they agree, so this is latent rather than broken. But a bump that moves singleuser.image.tag and misses a profile ref would have e2e report the new ref while the pod pulls the old image — green on 1.1.0.

Nothing guards it: test_hub_ca_bundle.py:99-115 covers only the hub/init pair and there are no hardcoded shas in tests/. A yaml.safe_load assert following the existing _hub_values() pattern catches it and joins the existing unit job — no kind, no pull, no matrix leg. Add assert jh["hub"]["image"]["tag"] == jh["singleuser"]["image"]["tag"] too, otherwise a half-bump that moves the hub pair and leaves the seven jupyterlab refs still passes everything.

Not suggesting the extension-presence e2e leg — max-parallel is 10 and the matrix already collects exactly 10 node IDs, so an 11th test buys a whole second wave.

Smaller things

  • values.yaml:492-495 undercounts what a hand-editor has to bump — it names only the choices.default image lines, omitting the outer kubespawner_override.image and display_name, while :502-506 correctly says the script syncs all three. Pre-existing, not from this PR, but :492 is the comment you hit first and this PR is exactly a hand edit.
  • images/Dockerfile:17 installs pixi unpinned (PIXI_VERSION appears nowhere). Pre-existing and not a security issue — I checked, pixi install --locked does verify the lock's PyPI sha256 on both cold and warm cache paths — but a pixi minor bump can change lock handling with no repo change, so pinning it is worth an issue.
  • Follow-up, not this PR: the sha- tag on a pull_request build comes from the merge commit, so a pin can never name the build of the commit containing it. Any push touching images/** after the pin lands means e2e goes green against stale image content, silently. You dodged it here by ordering the pin last.

Checked and not raising

I read launchpad#87 — v1.1.0...v1.1.1 is 2 commits, 8 files, +284/-15, all on-topic, no new deps, 172 lines of tests included. The lock's sha256 matches PyPI and my own independent pixi lock. quay has sha-7c0599c multi-arch with 4/4 children, and its base is e898d86, which is current main head, so the image content equals post-merge main.

Also: the hub does roll on upgrade (the pod-template image itself changes); both profiles' display_name matches their image so the selector shows what it runs; a fork PR pinning an unpushed tag fails loudly at docker pull, not silently; and there's no GPU-ref skew since values.yaml has no GPU ref at all.

Two things I'm explicitly withdrawing from my earlier review. The Chart.yaml release-window risk I raised is much smaller than I said — 0.1.5 is already released and every packaging step in release.yaml is gated on exists == 'false', so a Chart.yaml push that doesn't bump the version is a no-op. And Closes #227 is fine as-is: #236 closed #191 the identical way, one second after merge, with its fix still unreleased four days later. Body line 14 is the right disclosure, not a contradiction.

…I smoke test

- Document the nebi >= v0.15 floor where deployers look: comment at
  values.yaml nebi.image.tag, override prose in nebi-integration.md, and
  a troubleshooting row for the silent ~61s fallback (including the
  slow-start mode fallback race).
- Fix the profiles NOTE comment to name all three refs a hand-bump must
  move (outer kubespawner_override.image, choices.default image, and its
  display_name).
- Add tests/unit/test_image_ref_sync.py: profile images and choice
  display_names must match singleuser.image, and hub.image.tag must match
  singleuser.image.tag, so a half-bump fails the unit job instead of e2e
  passing against a stale image.
- Add a post-build smoke step to build-image.yaml (opt-in via
  smoke_test_labextensions, enabled for the jupyterlab image): run
  jupyter labextension list in the pushed digest and assert the
  jupyterlab-launchpad version pinned in pixi.toml is enabled.
@github-actions

Copy link
Copy Markdown
Contributor

Docs preview for bump-launchpad-1.1.1:
https://bump-launchpad-1-1-1.data-science-pack.pages.dev

@tylerpotts

tylerpotts commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in c0a72e3 plus a PR body edit. Point by point:

Nebi-tab-mounted scope: added a "Scope caveat" paragraph to the body stating the listener only works while the Nebi tab is mounted and that the tab-closed flow still falls back to the ~61s poll, so #227 is improved for the common path rather than eliminated. Checklist item 3 is reworded as "Tab-closed path (still supported; unchanged by this PR)" rather than just a negative control.

Manual verification: the stale "Verified by hand instead" tense is fixed — the body now says items 2 and 3 still need to be run before merge. Item 1 is done and checked: docker run against sha-7c0599c shows jupyterlab-launchpad v1.1.1 enabled OK. It's also now a CI smoke step in build-image.yaml (opt-in smoke_test_labextensions, enabled for the jupyterlab image): after each arch build it runs jupyter labextension list against the pushed digest and asserts the launchpad version pinned in pixi.toml is enabled — first post-build verification in that workflow. Items 2-3 have now been run and pass — verified 2026-08-31 on a live deployment (tyler-hetzner-dev, nebi sha-5ca877a + jupyterlab sha-7c0599c), following your phase-reset protocol. Item 2 ran twice with the Nebi tab open: the new kernel appeared in Launchpad within seconds both times, no reload. Item 3: page reloaded right before installing (poll clock reset), Nebi tab closed before the build completed — the kernel took the full ~61s poll to appear, confirming the no-fix floor. All three checklist items are checked off in the PR body. ("five" in my earlier comment was a miscount, it's three.)

nebi >= v0.15 floor: recorded in the three places following the jhub-app-proxy pattern — comment at values.yaml nebi.image.tag, override prose in nebi-integration.md ("How the binary reaches user pods"), and a troubleshooting row for "new kernels take ~61s". The row also folds in the modeStore team-mode fallback race you flagged (slow nebi start makes the handshake no-op silently).

Image-ref sync unit test: added tests/unit/test_image_ref_sync.py following the _hub_values() pattern — every profile's outer kubespawner_override.image and every profile_options choice image must equal singleuser.image, choice display_names must name the image they spawn, and hub.image.tag must equal singleuser.image.tag. Negative-tested by mutating one ref: it fails with a pointed message. Full suite passes (131 tests) in the CI-pinned env.

values.yaml profiles NOTE: fixed to name all three refs a hand-bump must move (outer image, choices.default image, and its display_name), and points at the new test.

Issues filed: pixi pinning → #243; PR-build sha tags naming the merge commit → #244.

@viniciusdc viniciusdc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tylerpotts — the manual items now carry dated evidence, the floor is recorded where the jhub-app-proxy precedent puts it, and filing #243/#244 rather than bolting them on was the right call. Two things I'd fix before merge, both with tested patches below.

First, a correction I owe you from my last review. I led with the Nebi-tab-mounted scope as if it were a newly-revealed gap. It wasn't — your 2026-08-26 comment on #227 already said "Known limit: if the user closes the Nebi tab before the job finishes, no message is sent and they fall back to the 61s poll. That's acceptable." The Scope caveat you added documents a limit the thread had already accepted. My framing overstated it.

The smoke step passes on a disabled extension

build-image.yaml:104-111 and the twin at :167-174. Nothing regresses here — the step is new and green — but the body cites it as evidence, so it needs to hold.

I disabled launchpad inside the real published image and the step still goes green:

        jupyterlab-launchpad v1.1.1 ^[[31mdisabled^[[0m ^[[32mOK^[[0m 🔒 (all plugins locked) (python, jupyterlab_launchpad)
CURRENT_GREP=rc0 (FALSE PASS)

grep -F "jupyterlab-launchpad v${expected}" stops at the version, so disabled OK and enabled X (validation failure — the class this step exists to catch) both pass. That matters because body line 13 says the step "proves launchpad 1.1.1 is enabled in the built image" and line 21 cites it as the CI half of checklist item 1. A verification claim in a merged body gets quoted back later.

The catch is that the obvious tightening breaks a healthy image: the status words are ANSI-colored unconditionally. The constants live in jupyter_server/extension/serverextension.py:146-149 (imported at jupyterlab/commands.py:32) and the only guard is if os.name != "nt" — no isatty, no NO_COLOR. RED_X is '\x1b[31m X\x1b[0m', with the space inside the escape, which is where the double space in enabled X comes from.

Second, separate bug in the same eight lines: out=$(docker run … 2>&1) captures docker's stderr into $out, and bash -e aborts at the assignment before the printf on the next line. So the likeliest real failure — registry, auth, network — gives a red X with an empty log. Reproduced: ORIGINAL: EXIT=125 stdout=[] stderr=[].

Both fixed together, verified against the real image, a bogus digest, and disabled/enabled X/missing fixtures:

          expected=$(sed -nE 's/^jupyterlab-launchpad = "==([^"]+)"$/\1/p' images/jupyterlab/pixi.toml)
          test -n "$expected" || { echo "could not read exact jupyterlab-launchpad pin from pixi.toml"; exit 1; }
          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 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"
          grep -Fq " jupyterlab-launchpad v${expected} enabled OK" <<< "$plain" || {
            echo "::error::jupyterlab-launchpad v${expected} is not 'enabled OK' in $ref"
            exit 1
          }

GNU sed 4.9 on both runner images handles \x1b in -E, so that part is safe.

If you'd rather not strengthen it, the honest alternative is to rename the step to "launchpad present at pinned version" and cut "is enabled" from the :41 description — keeps the passing shell, drops the claim. I'd strengthen, since an enabled-check is what the step was added for.

The new test rejects a profile shape our own docs document

tests/unit/test_image_ref_sync.py:44, :51, :68.

server-profiles.md:72-75 teaches a second image choice:

              rlang:
                display_name: "R"
                kubespawner_override:
                  image: quay.io/nebari/nebari-data-science-pack-jupyterlab-r:sha-16c1922

Drop that into values.yaml and bump_image_tags.py correctly leaves it alone — all three of its writes are guarded on startswith(JUPYTERLAB_IMAGE + ":") / startswith(JUPYTERLAB_DISPLAY_PREFIX + ":") (:79, :89, :93), and the trailing colon is exactly what excludes -jupyterlab-r: and -jupyterlab-gpu:. The new test fails it:

E  AssertionError: profile 'small-instance' choice 'rlang': display_name 'R' does not match
   the image it spawns (nebari-data-science-pack-jupyterlab:sha-7c0599c)

So the test is strictly stronger than the writer it guards, and the extra strength forbids a documented configuration while blaming the profile for it. The root cause is a docstring/code gap: :35-37 and :60-61 both say "the default choice", but both bodies loop for name, choice in choices.items() over every choice.

To be fair on blast radius — the test reads REPO_ROOT / "values.yaml" only, so a deployer's own mirror or -gpu override can never trip it. This is about what this repo can ship, not about breaking anyone's CI.

Mirroring the script's guards fixes it, and the same change stops the tests erroring on #237's variant profiles (those get no kubespawner_override.image in values.yaml at all, so today you'd get KeyError: 'image', which reads as "the test is broken" rather than "your profile is wrong"):

        if not profile.get("kubespawner_override", {}).get("image", "").startswith(JUPYTERLAB_IMAGE + ":"):
            continue

and for the display assert use JUPYTERLAB_DISPLAY_PREFIX + ":" — a plain image-prefix guard never matches there, since display names carry no registry host. I ran the guarded version: all four shapes pass, and all five mutations still fire (singleuser.image.tag → 3 tests, outer image → :44, display_name → :68, choice image → :51, hub.image.tag:81). Worth updating those two docstrings to say "every jupyterlab-tagged choice" while you're in there.

Unrelated but same file: git merge-tree reports values.yaml conflicts between this head and #237, around the NOTE block — #237 inserts above it, you rewrote it. Whoever lands second gets that.

A deployer tuning the tag from the reference table still gets no warning

values-reference.md:145 reads "Pinned per chart release. Empty disables the init container." — no floor. Eleven rows down, :193 is the precedent you followed and it does carry one: "Must be ≥ v0.2.3 for apps to run inside a Nebi (pixi) environment; older versions only activate conda and fall back to the base env." That table is what someone reads when changing nebi.image.tag, so it's the one place the floor is load-bearing and missing.

Same edit, one more spot: server-profiles.md:84-85 still ends "hand edits have to keep up on their own" — which is precisely what your new test now falsifies. The PR only touched nebi-integration.md under docs/.

The GPU image ships launchpad and is never checked

build-images.yaml:46 enables the smoke test for jupyterlab only, but jupyterlab-gpu (:62-72) uses target: jupyterlab, which is FROM jupyterlab-base, whose pixi install … -e ${DEFAULT_ENV} --locked consumes the same images/jupyterlab/pixi.toml that pins launchpad. DEFAULT_ENV is hardcoded to default even on the GPU path, so it provably carries the identical extension.

Cost is near nil — the amd64 step took 103s, and the GPU image is 1.01 GiB vs 0.94 GiB compressed on a job that runs in parallel. smoke_test_labextensions: true on that job closes it.

Related: the :41 description says "Only meaningful for jupyterlab-target images", which is true but self-undermining — jupyterlab-gpu is one, so the sentence describes coverage the workflow doesn't give.

Smaller things

  • One stale sentence in the body: "What CI does and does not prove" still ends with "items 2 and 3 still need to be run before merge", directly above three [x] items with 2026-08-31 evidence. Pure deletion.
  • test_image_ref_sync.py:1-2 says "every hand-editable image reference" — nebi.image.tag (values.yaml:260) and nfs.image (:222-224) are both hand-editable and legitimately differ. "every hand-editable jupyterlab image reference" makes it true.
  • The new troubleshooting row lists four causes and the kubectl block at :222-231 resolves one of them. Launchpad is a two-liner, and it's the same assertion the CI step makes: kubectl -n data-science exec <user-pod> -- jupyter labextension list 2>&1 | grep launchpad. For the tab-not-open and team-mode-race causes there's no signal in this repo — worth saying so in the row rather than listing them as checkable.
  • The step name says "labextensions" but checks one. pixi.toml:80-85 also ==-pins jupyterlab-nebari-mode 0.3.0, jupyterlab-gallery 0.6.3, jupyterlab-jhub-apps 0.3.1 — all present and enabled OK in the real image. Looping the same grep -Fq over a hardcoded list is nearly free. Hardcode it: jupyterlab_nvdashboard ships as jupyterlab-nvdashboard, and jhub-apps isn't a labextension at all, so the name isn't derivable from the pin.

Checked and not raising

131 unit tests pass at this head in a CI-equivalent venv, and all five ref mutations go red naming the offending profile and choice — not coverage theater. The 9th ref this file omits, merge-ca-bundle (values.yaml:710), is already covered by test_hub_ca_bundle.py. The docstring's e2e/default-profile mechanism is accurate, verified line-by-line against kubespawner 7.0.0 (_get_profile:3600-3602, _load_profile:3658-3693, _apply_overrides:3637-3640 — scalars setattr, so a str image replaces).

The step's duplication across the two arch jobs is house style, not oversight — the login blocks and both digest steps are duplicated identically, and there's no step-level reuse in a reusable workflow. Digest pinning genuinely proves the shipped artifact and there's no manifest-list gap. Gating is right, including the fork path. Nothing exploitable: bare docker run --rm, no socket, no ~/.docker visible inside the container, and your RUN lines already execute on that runner during docker build anyway.

I also went looking for a contradiction between the new "must be ≥ v0.15" text and the older "override to roll forward" sentence in both values.yaml and nebi-integration.md, and there isn't one — the old sentence invites moving forward, the new one forbids moving backward, and "When overriding, keep…" scopes itself. Four copies of the floor is fine for the same reason: a floor doesn't move when nebi ships v0.16.

…ecord nebi floor in reference docs

- Smoke step: fail on 'disabled'/'enabled X' (strip unconditional ANSI
  from jupyter labextension list, match ' <ext> v<pin> enabled OK'),
  surface docker run failures instead of dying at the assignment with an
  empty log, and check all four pixi.toml-pinned extensions whose pin
  name matches the labextension name. Enable it for jupyterlab-gpu too
  (same pixi.toml via jupyterlab-base) and fix the input description.
- test_image_ref_sync.py: mirror bump_image_tags.py's guards (imported
  JUPYTERLAB_IMAGE / JUPYTERLAB_DISPLAY_PREFIX) so documented non-default
  choices (e.g. the R image in server-profiles.md) and imageless variant
  profiles pass instead of failing or raising KeyError. New anchor test
  fails loudly if singleuser.image.name diverges from the script constant
  so the guards can't go vacuous. All six mutations still fire.
- values-reference.md: nebi.image.tag row now carries the >= v0.15 floor
  (matching the jhub-app-proxy-version precedent in the same table).
- server-profiles.md: 'hand edits have to keep up on their own' replaced
  with a pointer to the unit test; notes non-jupyterlab choices are left
  alone by both script and test.
- nebi-integration.md troubleshooting row: split causes into checkable
  (with kubectl one-liners) vs no-signal (tab unmounted, team-mode race).
@tylerpotts

Copy link
Copy Markdown
Contributor Author

All addressed in 67fa2f0. By section:

Smoke step: took your patch as written — error-handled docker run (no more empty-log 125s), stderr merged, SGR stripped before matching, and the assertion is now <ext> v<pin> enabled OK, so disabled OK and enabled X both fail. I went with strengthening rather than renaming, and folded in your smaller suggestion: the step loops over the four pixi.toml pins whose name matches the labextension name (launchpad, nebari-mode, gallery, jhub-apps), reading each version from pixi.toml rather than hardcoding it so the check tracks future bumps; the comment records why nvdashboard and jhub-apps (the hub package) aren't derivable. Verified locally against the real sha-7c0599c image (all four pass), your disabled and enabled X fixtures (both fail), and a bogus digest (prints docker's error and exits 1).

GPU image: smoke_test_labextensions: true on the jupyterlab-gpu job too — you're right that it consumes the identical pixi.toml via jupyterlab-base. The self-undermining "Only meaningful for jupyterlab-target images" sentence in the input description is replaced with "Enable for every image built from the jupyterlab Dockerfile target (including the GPU variant)".

Unit test scope: guards now mirror the script exactly, by importing JUPYTERLAB_IMAGE / JUPYTERLAB_DISPLAY_PREFIX from scripts.bump_image_tags rather than restating them — if the script's guards move, the test moves with them. Non-jupyterlab choices (your rlang shape) pass, imageless variant profiles (the #237 shape) pass instead of KeyError, and all six mutations still fire — six, because I added an anchor test asserting singleuser.image.name == JUPYTERLAB_IMAGE: with everything guarded, a rename would otherwise make the whole file pass vacuously. Docstrings updated ("every hand-editable jupyterlab image reference", "every jupyterlab-tagged choice"). Noted on the #237 conflict — the NOTE-block rewrite was this PR, so whichever lands second takes the trivial rebase.

Reference docs: values-reference.md nebi.image.tag row now carries the floor in the same shape as the jhub-app-proxy-version row eleven rows down. server-profiles.md's "hand edits have to keep up on their own" now points at the unit test instead, and says explicitly that non-jupyterlab choices are left alone by both script and test.

Troubleshooting row: split into checkable causes (with the nebi --version and jupyter labextension list kubectl one-liners — the latter is indeed the same assertion the CI step makes) and no-signal causes (tab unmounted, team-mode race), ending with "re-test with the Nebi tab open" rather than implying they're diagnosable.

Body: the stale "items 2 and 3 still need to be run before merge" sentence above three checked items is gone, and the smoke-step claim now says "enabled OK" rather than just "enabled", matching what the step actually asserts.

And thanks for the correction on the scope framing — appreciated, though the Scope caveat paragraph earns its place in the body either way, since the merge auto-closes #227 and the body outlives the comment thread.

Full suite: 132 passed in the CI-pinned venv.

@viniciusdc viniciusdc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tylerpotts! I willbe aproving this since this is an image bump

@tylerpotts
tylerpotts merged commit 740eafb into main Aug 31, 2026
30 checks passed
@tylerpotts
tylerpotts deleted the bump-launchpad-1.1.1 branch August 31, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] - Delay between environment completing and showing up as available

3 participants