Skip to content

🐋♻️:replace the dev container with a stock image and features - #1775

Merged
OpenINFbot merged 10 commits into
livefrom
chore/modernize-devcontainer
Aug 8, 2026
Merged

🐋♻️:replace the dev container with a stock image and features#1775
OpenINFbot merged 10 commits into
livefrom
chore/modernize-devcontainer

Conversation

@DerekNonGeneric

Copy link
Copy Markdown
Member

🐋♻️ 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 last
published 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/.github has the identical Dockerfile if you want the same
treatment there).

What was dropped, and why it was safe

Each of these was checked before removing, not assumed:

  • Ruby. package-scripts.yml maps verify.ruby and format.ruby to
    verify-ruby.mts and format-ruby.mts; neither file exists. No
    Gemfile, no .ruby-version, no Rakefile. The rubocop extension and the
    [ruby] block in .vscode/settings.json were already commented out. So
    ruby-dev, rbenv and the rbenv PATH entry were dead.
  • Ports 4000 and 35729 — Jekyll's server and LiveReload. This site is
    Eleventy behind browser-sync, which serves on 3000 with its UI on 3001,
    so those forwarded nothing.
  • The dprint install script, which ran on every container start and
    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/.bin to GITHUB_PATH.
  • build-essential, now that every install script is denied and nothing
    compiles from source.
  • The sshd on port 2222. VS Code forwards the host's ssh-agent and
    gpg-agent natively, which is what commit signing actually relies 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.

Node versions

Two managers, doing different jobs:

  • nvm.fish is the interactive one. A bare nvm use reads .nvmrc and
    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 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 lives 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
introducing a third copy.

The two accompanying fixes

  • .nvmrc now names the exact version rather than a floating alias, which
    would stop matching the exact engines.node pin the next time that
    release line moves.
  • publicHoistPattern puts @dprint/* where the dprint VS Code extension
    looks for it. The extension resolves
    node_modules/@dprint/<platform>-<arch>-<family>, which is an
    optionalDependency of dprint and therefore lives in pnpm's virtual
    store — so the directory never existed, and the fallback to a bare
    dprint on 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 and
    public-hoist-pattern in .npmrc is silently ignored.

node_modules/.bin now reaches the whole container through remoteEnv
rather than fish_add_path (pnpm bin) in config.fish, which left every
non-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 use resolving .nvmrc, the universal
nvm_default_version that new interactive fish sessions read, agreement
between 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
linuxarm64 builds, since the arm64 case was the one most likely to
surprise.

DerekNonGeneric and others added 3 commits August 8, 2026 02:25
`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>
@netlify

netlify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploy Preview for gh-pages-openinf ready!

Name Link
🔨 Latest commit aca0ca7
🔍 Latest deploy log https://app.netlify.com/projects/gh-pages-openinf/deploys/6a76c6303ac16c0008940dfa
😎 Deploy Preview https://deploy-preview-1775--gh-pages-openinf.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

DerekNonGeneric and others added 7 commits August 8, 2026 02:51
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
OpenINFbot merged commit 1d05c6c into live Aug 8, 2026
13 checks passed
@OpenINFbot
OpenINFbot deleted the chore/modernize-devcontainer branch August 8, 2026 06:09
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>
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.

2 participants