build: bump jupyterlab-launchpad to 1.1.1 in jupyterlab image - #241
Conversation
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
left a comment
There was a problem hiding this comment.
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:
jupyter labextension listreports launchpad 1.1.1.- Open the Nebi tab and leave it open — the listener only fires while the iframe is mounted.
- Trigger an install for a kernel that isn't already in the kernelspec list.
- Time it from the Nebi pill flipping to complete. Should be seconds, no page reload; >60s means the handshake didn't fire.
- 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.
- 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.
|
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
left a comment
There was a problem hiding this comment.
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-495undercounts what a hand-editor has to bump — it names only thechoices.defaultimage lines, omitting the outerkubespawner_override.imageanddisplay_name, while:502-506correctly 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:17installs pixi unpinned (PIXI_VERSIONappears nowhere). Pre-existing and not a security issue — I checked,pixi install --lockeddoes 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 apull_requestbuild comes from the merge commit, so a pin can never name the build of the commit containing it. Any push touchingimages/**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.
|
Docs preview for |
|
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: nebi >= v0.15 floor: recorded in the three places following the jhub-app-proxy pattern — comment at Image-ref sync unit test: added 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
left a comment
There was a problem hiding this comment.
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-16c1922Drop 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 + ":"):
continueand 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-2says "every hand-editable image reference" —nebi.image.tag(values.yaml:260) andnfs.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
kubectlblock at:222-231resolves 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-85also==-pinsjupyterlab-nebari-mode 0.3.0,jupyterlab-gallery 0.6.3,jupyterlab-jhub-apps 0.3.1— all present andenabled OKin the real image. Looping the samegrep -Fqover a hardcoded list is nearly free. Hardcode it:jupyterlab_nvdashboardships asjupyterlab-nvdashboard, andjhub-appsisn'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).
|
All addressed in 67fa2f0. By section: Smoke step: took your patch as written — error-handled GPU image: Unit test scope: guards now mirror the script exactly, by importing Reference docs: Troubleshooting row: split into checkable causes (with the 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. |
There was a problem hiding this comment.
Thanks @tylerpotts! I willbe aproving this since this is an image bump
Summary
jupyterlab-launchpadfrom1.1.0to1.1.1inimages/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 fornebi:job-completedfrom the Nebi iframe and refreshes kernels immediately instead of waiting on the ~61s KernelSpecManager poll.pixi.lockwithpixi update jupyterlab-launchpad(Linux container).requires_distis identical to 1.1.0, so only the launchpad wheel entry changes.pixi.toml, comment atvalues.yamlnebi.image.tag, override guidance and a troubleshooting row indocs/nebi-integration.md.tests/unit/test_image_ref_sync.py: the profiles' image refs and choice display_names must matchsingleuser.image, andhub.image.tagmust matchsingleuser.image.tag— a half-bump now fails the unit job instead of e2e silently testing a stale image.build-image.yaml(enabled for the jupyterlab image):jupyter labextension listagainst the pushed digest, asserting the launchpad version pinned inpixi.tomlis 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-5ca877aon main. This PR completes the round trip.Scope caveat: the listener only works while the Nebi tab is mounted (
useHostJobNotificationsgates onisEmbedded() && 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
jupyterlabamd64/arm64 build legs runpixi install --locked, so they prove the lock resolves and installs. The new smoke step additionally proves the pixi.toml-pinned extensions (launchpad included) areenabled OKin the built image, and fails loudly ifdocker runitself fails.sha-08ea5e4(1.1.0) and did not exercise 1.1.1.values.yamlis now pointed at the PR buildsha-7c0599cviascripts/bump_image_tags.pyso subsequent e2e legs spawn the 1.1.1 image. The post-mergebump-image-tagsworkflow rewrites these tags anyway.postMessagehandshake 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.yamluntil the post-mergechore: bump image tagscommit lands.release.yamlpackagesvalues.yamlas 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+ jupyterlabsha-7c0599c)jupyter labextension listreports launchpad 1.1.1 — verified 2026-08-31 viadocker runagainstsha-7c0599c:jupyterlab-launchpad v1.1.1 enabled OK (python, jupyterlab_launchpad). Also enforced in CI by the new smoke step.sha-5ca877a+ jupyterlabsha-7c0599c), two runs, kernel appeared within seconds both times.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.