Publish verified notebook environment images to GHCR; requirements.in submission contract - #180
Conversation
Each Colab-ready notebook group (directory + identical pin set) gets a container image with the pinned dependencies preinstalled, the notebooks and helper files baked in, and a JupyterLab entrypoint. The new workflow builds the image, runs every notebook in the group inside it with the unmodified run_notebook.py harness, and pushes to ghcr.io only when all of them pass, so published images are always verified snapshots. workflow_dispatch only for now, defaulting to the 001550/PaganLab pilot. Verified locally end to end: build (380 MB, linux/amd64), both pilot notebooks green inside the image, JupyterLab serves with the pinned kernel resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributors now commit a requirements.in listing only their notebook's direct dependencies, and lock_notebook.py assembles the rest: it compiles the full pinned set with uv against Colab's preinstalled versions and writes the four bootstrap cells into the notebook, prepending them when absent or refreshing the pin block in place (helper fetch lines are preserved, and nbformat_minor is bumped when cells are prepended). Committing the .in file also gives re-locking a source of truth: today the pins only exist inside the notebook JSON, so re-resolving after a Colab runtime bump means reverse-engineering the direct deps from a 160-line pin list. The container image pipeline rebuilds flow from the same file. README submission instructions now ask for requirements.in instead of a conda env export, docs/adding-notebooks.md documents the script and the one-file-per-directory convention, and the CI missing-install-cell error points at the new flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Preview for this PR has been removed (PR closed). |
Fleet Rollout ScorecardThe full fleet has now been built: 28 of 33 image groups are published as multi-arch (amd64 + arm64) manifests, with every notebook in every published group executed successfully inside its image on both architectures, on native runners with no emulation. Runs: 32320269117 (initial fleet) and 32323493938 (re-run after #183; completed builds skipped via hash tags as designed, so the re-run only performed the missing merges and one push retry). Two fixes landed during the rollout. #182 switched to multi-arch images after validation on Apple Silicon showed that Rosetta emulation can deadlock kernel subprocess spawns; it also added a non-root user and prebaked the matplotlib font cache. #183 fixed the merge job so one group's failure does not block every other group's manifest. Groups that did not publishThese five groups failed in-image verification, so nothing was pushed for them (by design; only verified images publish). The causes are notebook or upstream issues, not image issues, and the first three presumably affect Colab and the weekly sweep as well.
Action needed (org admin)GHCR has no API for visibility, so each new package needs a one-time manual flip to public at https://github.com/orgs/dandi/packages (select the package, then Package settings, then Change visibility). Once flipped, any of them runs with, for example: Follow-ups from here: fix or exclude the five failing groups, add the push-to-master and monthly cron triggers, and then index-page integration. 🤖 Generated with Claude Code |
This implements the "two-pronged" strategy we discussed at the DANDI meeting: keep the one-click Colab path exactly as it is, and add a stamped path where each notebook environment is published as a persistent, self-contained container image. The Colab flow works today, but it depends on Colab still shipping Python 3.12 and on PyPI still resolving the pins. A published image freezes the whole working environment, so the notebook stays runnable after both of those have moved on. As @yarikoptic pointed out, CI already effectively builds this environment on every run; this PR makes it a downloadable artifact.
What This Adds
A container-publishing workflow (
build-notebook-images.yml, manual dispatch only for now). Notebooks are grouped by directory and pin set (33 groups across the 51 Colab-ready notebooks), and each group gets one image atghcr.io/dandi/example-notebooks/<group>, for example:That opens JupyterLab on the notebook with all pinned dependencies preinstalled. The key property is that an image is only pushed after every notebook in the group has been executed successfully inside the candidate image, using the unmodified
run_notebook.pyharness.latestis therefore always a verified snapshot, and each push also getssha-<commit>,<YYYY-MM-DD>, andhash-<inputs>tags. The hash tag doubles as the rebuild-skip check: if an image built from identical inputs already exists, the job skips it, which keeps a future full-fleet cron cheap.Inside the image the kernel environment is the system Python with the pins installed via
uv pip install --system, matching the Colab bootstrap cell and the CI harness exactly. JupyterLab runs from an isolateduv toolenvironment so its own dependency tree can never perturb the pinned set. The base is a digest-pinnedpython:3.12-slim; theBASE_IMAGEarg is the knob for a possible future variant on Google's official Colab runtime image (deferred, since that image is around 24 GB against roughly 14 GB of free runner disk). Images are built forlinux/amd64, the platform the pins were resolved for. Details are in.github/docker/README.md.A simpler submission contract (
lock_notebook.py). Contributors now commit arequirements.innext to their notebook listing only its direct dependencies, and run:The script compiles the full pinned set with
uv pip compile, constrained to Colab's preinstalled versions, and writes the four bootstrap cells into the notebook, prepending them when absent or refreshing the pin block in place (helper!curllines are preserved, andnbformat_minoris bumped when needed). Committing the.infile also gives re-locking a source of truth: today the pins exist only inside the notebook JSON, so re-resolving after a Colab runtime bump means reverse-engineering the direct dependencies from a 160-line pin list. The README anddocs/adding-notebooks.mdare updated accordingly; the oldconda env exportinstructions are retired for new submissions.Verification
I ran the pipeline end to end locally on the
001550/PaganLabpilot group: the image builds at 380 MB, both notebooks execute green inside it (stage: done, ok: true), JupyterLab serves with the pinned kernel correctly resolved, and a second run of the pin-set grouping and build hash reproduces identical values. The lock script was tested on scratch copies for both the refresh and prepend paths, including the cell-source newline andnbformat_minorgotchas from the original #149 rollout. Nothing in the existing test, index, or preview workflows is touched, apart from the missing-install-cell error message inrun_notebook.py, which now points contributors at the new flow.Remaining Steps Before This Is Useful
GITHUB_TOKENpackage creation for this repo (org settings, Packages).Build notebook imagesworkflow with the default001550/PaganLabfilter.001550-paganlabpackage to public and pull-and-run it on a fresh machine.001636/TurnerLab/motor_cortex) to exercise helper baking.Index-page integration (a copyable
docker runcommand next to the Colab badge), push-to-master and monthly cron triggers, and converging the weekly test sweep onto these images are follow-ups once the pilot is validated end to end. Backfillingrequirements.infor the 51 existing notebooks is another open follow-up; it can happen gradually as notebooks need re-locking.🤖 Generated with Claude Code