🐋♻️:replace the dev container with a stock image and features - #1775
Merged
Conversation
`engines.node` is an exact pin and .npmrc sets engine-strict, so a floating alias in .nvmrc stops matching the moment the next patch release lands in that line -- at which point nvm.fish activates a version that every pnpm command then rejects. nvm.fish resolves a bare `nvm use` from this file, so it has to name the same version the package manager enforces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dprint VS Code extension resolves its executable from node_modules/@dprint/<platform>-<arch>-<family>, walking up from the workspace. That package is an optionalDependency of dprint, so pnpm keeps it in the virtual store and the directory the extension looks in never exists. Failing that, the extension falls back to a bare `dprint` on PATH, which was only ever on fish's PATH, so it found nothing either. The workaround had been a separately installed dprint. That copy came from an install script that always fetches the latest release, so the editor could format with one version while CI checked with the pinned one. Hoisting publicly puts a real native binary at the path the extension prefers, at the version package.json pins. Note this belongs in pnpm-workspace.yaml rather than .npmrc: pnpm 11 moved these settings, and `public-hoist-pattern` in .npmrc is silently ignored. Verified: node_modules/@dprint/linux-arm64-glibc/dprint reports 0.55.2, matching the devDependency, and the toml and dockerfile tasks still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Swaps a bespoke Dockerfile, a .dockerignore and ~90 lines of fish across
two lifecycle scripts for a stock image, four features and one bash
script.
The old base image, openinf/grimesai-salvage-tex:lunar, was last published
in February 2024, so the daily development environment had been sitting on
a two-year-old base. Only two repositories in the org reference it, and
after this, one.
Dropped as dead, each checked before removing:
- ruby-dev, rbenv and the rbenv PATH entry. package-scripts.yml maps
verify.ruby and format.ruby to two task files that do not exist, and
there is no Gemfile, .ruby-version or Rakefile. The rubocop extension
and the [ruby] block in .vscode/settings.json were already commented
out.
- Ports 4000 and 35729, which are Jekyll's server and LiveReload. This
site is Eleventy behind browser-sync, on 3000 with its UI on 3001.
- The dprint install script, which fetched an unpinned second copy of a
tool already pinned in devDependencies. The previous commit fixes the
reason it was needed.
- build-essential, now that every install script is denied and nothing
compiles from source.
- The sshd on 2222. VS Code forwards the host's ssh-agent and gpg-agent
natively, which is what commit signing actually depends on.
Kept: default-jre, without which verify.svg and verify.htmlValidForVNU die
on `command not found: java`; git-lfs, which .gitattributes routes 19 file
patterns through; the extension list; and fish.
Two Node version managers are set up, because they do different jobs.
nvm.fish is the interactive one -- a bare `nvm use` reads .nvmrc, and it
switches that fish session only. The image's nvm sets the container-wide
baseline: the image puts nvm/current/bin on the container's PATH and sets
NVM_SYMLINK_CURRENT, so `nvm use` there repoints a symlink that every
non-fish process follows. Neither can do the other's job -- nvm.fish
prepends to fish's own PATH and keeps versions under XDG_DATA_HOME, so
nothing outside fish sees it.
No Node version appears in devcontainer.json. It is in package.json and
.nvmrc, each read by a different tool, and post-create.sh fails with an
explanation if the two disagree rather than adding a third copy.
node_modules/.bin now reaches the whole container through remoteEnv
instead of `fish_add_path (pnpm bin)` in config.fish, which left every
non-fish caller without those binaries.
The fish feature's own fisher install is disabled because it pulls from
main; post-create.sh installs a pinned tag instead.
Verified without a container rebuild, which is not possible here: the
fisher and nvm.fish installs, `nvm use` resolving .nvmrc, the universal
nvm_default_version that new interactive sessions read, agreement between
fish, bash and sh on the required version, and the full verify suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for gh-pages-openinf ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The meaningful-ooo fish feature adds an openSUSE Build Service apt source, and its v1 points at the fish 3 repository, which has no Debian 13 directory. The build failed on it: deb http://download.opensuse.org/repositories/shells:/fish:/release:/3/Debian_13/ / Err:8 ... shells:/fish:/release:/3/Debian_13 Release 404 Not Found Its v2 moved to the fish 4 repository, which does resolve for Debian 13, but that only postpones the problem: it leaves the container depending on OBS publishing a directory for every future Debian release, and this is what happens when they do not. Trixie ships fish 4.0.2 in its own repositories, which is also the build that the fisher and nvm.fish setup was tested against, so this drops the third-party apt source entirely. The other three features installed fine -- the failure was the last step of the build, with pnpm, git-lfs and java already cached above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The script failed with a bare `exit code 3` and printed nothing at all. Sourcing nvm.sh is not a neutral act: it auto-detects .nvmrc in the working directory and tries to activate that version as it loads. The postCreateCommand runs in the workspace, .nvmrc names an exact version, and in a freshly built container nothing is installed yet -- so that lookup resolves to N/A and returns 3, which under `set -e` killed the script before it reached the install a few lines below. It said nothing because nvm reports through nvm_echo, which writes to stdout, and the three nvm calls were redirecting stdout to /dev/null. So the one message that explained the failure was the one being thrown away. Sourcing may now fail without taking the script down, nothing is silenced, each phase announces itself, and the version is checked after the switch rather than assumed. Verified by running the script end to end against an empty NVM_DIR with no terminal attached, from the repository root, which is what the container does: it now reaches the end and exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things were being fetched under one banner, which read as fisher twice: fisher itself, and the nvm plugin. They are different packages, but fisher was being run twice to get them, once per plugin. It now installs both in a single `fisher install`, so there is one pass and one pair of fetches. The curl above it is fisher's own bootstrap -- it defines the function in memory so it can then install itself properly -- and is not removable without giving up a persistent fisher. The plugin now comes from jorgebucaran/nvm.fish rather than the OpenINF fork. The fork is byte-identical to upstream, zero commits ahead or behind, so it bought nothing and cost a second source to watch. It is also pinned now, where the fork was tracked at HEAD. Verified from a container-fresh state -- no ~/.config/fish, empty NVM_DIR, no terminal attached: two fetches, two plugins installed, exit 0, and a new interactive fish session lands on the pinned Node. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trixie packages fish 4.0.2, so installing it needs one apt call. The feature that was doing it, devcontainers-extra/fish-apt-get, downloads the 20 MB nanolayer binary and runs it as root to invoke a second feature which runs exactly that apt call -- more than twice the size of the 8.6 MB package it installs, two extra fetches, and a third-party binary executing during the image build, all to avoid writing four lines. Those four lines now live in a Dockerfile beside devcontainer.json. This is not a return to maintaining a base image: it is the same stock Microsoft image as before with one package added, and it bakes into a cached layer rather than re-running on every container create. Everything else still comes from features. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things deepsource caught, one of them mine. .deepsource.toml still aimed its docker analyzer at .devcontainer/experimental/Dockerfile, which this branch deletes. Pointed at the file that now exists. DL3008 wants the apt package pinned to an exact version. That is a different bargain from pinning an npm dependency, which this repository does everywhere: Debian drops superseded versions from the archive, so the first security update to fish would leave the build asking for a version that is no longer served and failing outright, and nothing watches apt pins to raise a PR the way renovate does for the rest. The previous Dockerfile suppressed this same rule for the same reason, so the suppression is carried over rather than invented, with the reasoning written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the digest each feature tag resolved to, so pnpm, java and git-lfs cannot shift underneath a rebuild.
The previous configuration named the profile as well as selecting it:
"terminal.integrated.profiles.linux": { "fish": { "path": "fish" } },
"terminal.integrated.defaultProfile.linux": "fish"
This one had kept only the second line. VS Code can usually discover fish
from /etc/shells by itself, which is why terminals still opened in it, but
that is discovery doing the work rather than the configuration, and there
is no reason to rely on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OpenINFbot
approved these changes
Aug 8, 2026
DerekNonGeneric
added a commit
that referenced
this pull request
Aug 9, 2026
PR URL: #1775 Reviewed-by: @OpenINFbot ------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🐋♻️ Replaces the dev container with a stock image plus features, and fixes
two things found while working out what the old one was actually doing.
The old base image,
openinf/grimesai-salvage-tex:lunar, was lastpublished in February 2024, so the daily development environment has
been sitting on a two-year-old base with no security patches since. Only
two repositories in the org reference it; after this, one
(
OpenINF/.githubhas the identical Dockerfile if you want the sametreatment there).
What was dropped, and why it was safe
Each of these was checked before removing, not assumed:
package-scripts.ymlmapsverify.rubyandformat.rubytoverify-ruby.mtsandformat-ruby.mts; neither file exists. NoGemfile, no
.ruby-version, no Rakefile. The rubocop extension and the[ruby]block in.vscode/settings.jsonwere already commented out. Soruby-dev,rbenvand the rbenv PATH entry were dead.Eleventy behind browser-sync, which serves on 3000 with its UI on 3001,
so those forwarded nothing.
fetched an unpinned second copy of a tool already pinned at 0.55.2 in
devDependencies. CI has always used the pinned one — both workflows
append
node_modules/.bintoGITHUB_PATH.compiles from source.
gpg-agent natively, which is what commit signing actually relies on.
Kept:
default-jre, without whichverify.svgandverify.htmlValidForVNUdie oncommand not found: java;git-lfs,which
.gitattributesroutes 19 file patterns through; the extensionlist; and fish.
Node versions
Two managers, doing different jobs:
nvm usereads.nvmrcandswitches that fish session only.
nvm/current/binon the container's PATH and setsNVM_SYMLINK_CURRENT, sonvm usethere repoints a symlink everynon-fish process follows.
Neither can do the other's job: nvm.fish prepends to fish's own PATH and
keeps versions under
XDG_DATA_HOME, so nothing outside fish sees it.No Node version appears in
devcontainer.json. It lives inpackage.jsonand.nvmrc, each read by a different tool, andpost-create.shfails with an explanation if the two disagree rather thanintroducing a third copy.
The two accompanying fixes
.nvmrcnow names the exact version rather than a floating alias, whichwould stop matching the exact
engines.nodepin the next time thatrelease line moves.
publicHoistPatternputs@dprint/*where the dprint VS Code extensionlooks for it. The extension resolves
node_modules/@dprint/<platform>-<arch>-<family>, which is anoptionalDependency of
dprintand therefore lives in pnpm's virtualstore — so the directory never existed, and the fallback to a bare
dprinton PATH failed too because that was only ever on fish's PATH.That is why a separately installed dprint was needed. This puts a real
native binary at the extension's preferred path, at the pinned version.
It has to go in
pnpm-workspace.yaml: pnpm 11 moved these settings andpublic-hoist-patternin.npmrcis silently ignored.node_modules/.binnow reaches the whole container throughremoteEnvrather than
fish_add_path (pnpm bin)inconfig.fish, which left everynon-fish caller — CI, editor tasks, agents — without those binaries.
Verification
A container rebuild was not possible in the environment this was written
in, so everything downstream of it was exercised directly instead: the
fisher and nvm.fish installs,
nvm useresolving.nvmrc, the universalnvm_default_versionthat new interactive fish sessions read, agreementbetween fish, bash and sh on the required version, ad-hoc switching in
fish leaving the baseline alone, the dprint binary resolving at the
extension's path and reporting the pinned version, and the full verify
suite.
That leaves one thing unproven: whether the four features install cleanly
onto a fresh image. It fails loudly and immediately on first rebuild if
not. Both SDKMAN Java 21 distributions were confirmed to have
linuxarm64builds, since the arm64 case was the one most likely tosurprise.