fix: the prebuild tag moved for files the image cannot see - #590
Merged
blooop merged 1 commit intoSep 9, 2026
Merged
Conversation
Reviewer's GuideThe PR fixes unnecessary devcontainer prebuild cache misses by adding a context Flow diagram for devcontainer prebuild hashingflowchart LR
Files[".devcontainer files"] --> Ignore[".dockerignore"]
Ignore --> Context["Image build context"]
Config["Parsed devcontainer config"] --> Hash["Devpod prebuild hash"]
Context --> Hash
Hash --> Lookup["GHCR tag lookup"]
Lookup -->|found| Pull["Pull published image"]
Lookup -->|missing| Build["Build locally"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Opening this repository with `dl` pulls a published container instead of building one, and it had gone back to building. The prebuild tag is a hash of the build context as well as of the build config, and a Dockerfile with no COPY leaves devpod's `includeFiles` empty -- at which point it hashes the *whole* context directory, keyed by path and CRC32, with only its own feature-staging folder excluded (pkg/util/hash.DirectoryHash, from config.CalculatePrebuildHash). There was no `.dockerignore` inside `.devcontainer/`, so the context half of the hash was "every byte in this directory". Four things in there cannot reach the image: the feature's README, its troubleshooting guide, the host-side `initializeCommand`, and this manifest's own comments -- and the manifest is mostly comments. Measured on devpod 0.26.1, amd64, at 4db3427: appending one comment line to `claude-code/README.md` and one to `claude-code/init-host.sh` moved the tag from devpod-5bf7be3e3e7e1b3f4fbb01a9b3ab88e7, which CI had published and every launch pulled, to devpod-90e9641b9d3661f681e640eeedb3a410, which nothing had ever built. Several minutes per launch, on every branch carrying the edit, and the symptom is only that opening a container is slow again -- devpod treats an unmatched tag as a cache miss and builds locally without complaint. `.devcontainer/.dockerignore` narrows the hash to the files the image is built from. Excluding `init-host.sh` is what makes the two halves of the hash agree rather than a new judgement call: `normalizeConfigForHash` already clears `DevContainerActions`, where `initializeCommand` lives, out of the config half, and the script was then hashed straight back in as context. The manifest goes the same way and for the same reason -- devpod read it to get here and does not read it out of the context, and the config half already carries every build input it declares. Checked in both directions, because an exclusion too wide is the worse bug: a launch that pulls an image built from a Dockerfile the branch no longer has comes up fine and is the wrong container. Prose, the host hook and comment-only manifest edits no longer move the tag; the Dockerfile, `claude-code/install.sh`, `claude-code/devcontainer-feature.json`, a changed `build.cacheFrom` and an added feature all still do. The resulting image was diffed against one built from the same tree without the `.dockerignore`: same 19 layers, same env, same metadata labels, `claude` and `dockerd` both present. Five guards, four of them because the premise can move under this file. test_the_dockerfile_copies_nothing_out_of_the_build_context is the first one to read -- add a COPY and the excluded files stop being invisible to the image and start being missing from it. The classification test makes a new file under `.devcontainer/` a failure until somebody says which side of the line it is on, since the silent default is the expensive one. One rebuild is owed: the `.dockerignore` is itself part of the context, so this commit moves the tag once, and launches build locally until devcontainer-prebuild.yml republishes on main.
blooop
force-pushed
the
fix/prebuild-context-hashes-only-what-reaches-the-image
branch
from
September 9, 2026 10:58
40a5b7f to
cf1675a
Compare
blooop
deleted the
fix/prebuild-context-hashes-only-what-reaches-the-image
branch
September 9, 2026 11:30
Merged
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.
Opening this repository with
dlpulls a published container instead of building one, and it had gone back to building.What was wrong
The prebuild mechanism itself is fine. Running devpod's own hash-and-lookup against
mainat 4db3427 computesdevpod-5bf7be3e3e7e1b3f4fbb01a9b3ab88e7and finds it in GHCR. Package public, workflow green, local and CI hashes identical on several separate trees.What was wrong is what the tag is allowed to notice.
build.contextis.devcontainer, and a Dockerfile with noCOPYleaves devpod'sincludeFilesempty — at which point it hashes the whole context directory, keyed by path and CRC32, with only its own feature-staging folder excluded. There was no.dockerignoreinside.devcontainer/, so the context half of the hash was "every byte in this directory". Four things in there cannot reach the image:claude-code/README.mdandclaude-code/TROUBLESHOOTING.md— prose, present because the Features spec wants it beside its featureclaude-code/init-host.sh— theinitializeCommand, which runs on the host.normalizeConfigForHashalready clearsDevContainerActionsout of the config half of the hash for exactly that reason; the script was then hashed straight back in as contextdevcontainer.json— devpod read it to get here and never reads it out of the context, and the config half already carries every build input it declares. This manifest is mostly comments, and abf6d9b moved the tag with ~30 lines of them beside a one-line mount changeMeasured on devpod 0.26.1, amd64, at 4db3427: appending one comment line to
claude-code/README.mdand one toclaude-code/init-host.shmoves the tag fromdevpod-5bf7be3e3e7e1b3f4fbb01a9b3ab88e7, which CI has published and every launch pulls, todevpod-90e9641b9d3661f681e640eeedb3a410, which nothing has ever built. Several minutes per launch, on every branch carrying the edit — and the only symptom is that opening a container is slow again, because devpod treats an unmatched tag as a cache miss and builds locally without complaint.#588 is a worked example: of its four
.devcontainer/files, three were prose or the host hook.The fix
.devcontainer/.dockerignorenarrows the context half of the hash to the files the image is actually built from.Excluding
init-host.shis what makes the two halves of the hash agree rather than a new judgement call. The manifest goes the same way and for the same reason.Checked in both directions, because an exclusion too wide is the worse bug — a launch that pulls an image built from a Dockerfile the branch no longer has comes up fine and is the wrong container. Re-measured on this branch's tree:
README.md/TROUBLESHOOTING.mdclaude-code/init-host.shdevcontainer.json, comment-onlyclaude-code/install.shclaude-code/devcontainer-feature.jsonbuild.cacheFromchangedThe resulting image was then diffed against one built from the same tree without the
.dockerignore: same 19 layers, same env, same metadata labels,claudeanddockerdboth present. The local feature still resolves from the filesystem, not from the context tar.Guards
Five, four of them because the premise can move under this file.
test_the_dockerfile_copies_nothing_out_of_the_build_contextis the one to read first — it is the claim the exclusions are downstream of. Add aCOPYand the excluded files stop being invisible to the image and start being missing from it.test_every_file_in_the_build_context_is_classified_as_a_build_input_or_notmakes a new file under.devcontainer/a failure until somebody says which side of the line it is on, because the silent default is the expensive one.Each was mutation-tested to fail for the right reason: dropping
**/*.md, over-excludinginstall.sh, adding an unclassified file, adding aCOPY, and writing a glob form the matcher cannot evaluate.One rebuild is owed
The
.dockerignoreis itself part of the context, so this moves the tag once. Launches build locally untildevcontainer-prebuild.ymlrepublishes onmain.Not covered
devpod's hashing here is the
blooop/devpodfork's, and it differs from loft-sh's in a way that matters: loft-sh skips every file whenincludeFilesis empty and returns an empty context hash, so under that buildclaude-code/install.shcould change without moving the tag, CI would find the tag already published, and a stale image would be served indefinitely. The fork errs the other way, which is slow rather than wrong, and devsy-org/devsy — the live continuation — keeps the fork's semantics. Nothing here guards against running a build that does not, and a guard for it would need a real devpod, so it belongs in the e2e suite.🤖 Generated with Claude Code