fix: rewrite config rootfs.diff_ids when injecting layers (#5)#6
Merged
Conversation
Deploying an image with layersFrom (rowdy runtime injection) produced an invalid OCI image: the injected layers were appended to the manifest but the image config's rootfs.diff_ids was left unchanged, so len(manifest.layers) != len(config.rootfs.diff_ids). Registries/runtimes then drop the app layer and the function crashes with ENOENT on its entrypoint. This affected both multi-arch and single-arch images. When a layersFrom match is found, fetch both the app and runtime image configs, append the runtime config's diff_ids (the uncompressed digests, only derivable from that config) and history to the app config, re-serialize it to a new digest/size, re-point data.config, and push the rewritten config blob instead of streaming the stale original. Assert layers.length === diff_ids.length before push. Also handle a bare image manifest (e.g. a single-platform build pushed without buildx attestations) by treating it as a one-entry index, reading the platform from its config, so `provenance: false` builds can be deployed. Add types: ["node", "jest"] to tsconfig so editors resolve node globals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Editor surfaced rootDir + deprecation errors on tests/tsconfig.json: - noEmit + rootDir ".." resolve the "not under rootDir" / common-source-dir errors caused by including ../src/**/*.ts without an explicit rootDir. - ignoreDeprecations silences the node10 moduleResolution / baseUrl deprecation notices pending a future TS migration. This config is type-check/editor only; emit goes through tsup (root tsconfig) and tests run via jest, both unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Fixes #5.
Problem
Deploying an image with
layersFrom(rowdy runtime injection) produced an invalid OCI image: the injected layers were appended to the manifest but the image config'srootfs.diff_idswas left unchanged, solen(manifest.layers) != len(config.rootfs.diff_ids). Registries/runtimes drop the app layer and the function crashes withENOENTon its entrypoint (spawn /usr/local/bin/score ENOENT).Per the issue's follow-up, this affected both multi-arch and single-arch images.
A second, related issue: a bare image manifest (a single-platform build pushed with
provenance: false, i.e. no buildx attestation index) was rejected incollect()withUnsupported mediaType on index: application/vnd.docker.distribution.manifest.v2+json.Fix
In
Transfer.collect():Config rewrite on injection — when a
layersFromplatform match is found, fetch both the app and the runtime image configs, append the runtime config'srootfs.diff_ids(the uncompressed layer digests — only derivable from that config, not from the compressed layer descriptors) andhistoryto the app config, re-serialize to a new digest/size, re-pointdata.config, and push the rewritten config blob instead of streaming the stale original. Assertlayers.length === diff_ids.lengthbefore push.Bare manifest support — a top-level
oci.image.manifest.v1+json/docker.distribution.manifest.v2+jsonis now treated as a one-entry index, with platform read from the image config, so the rest of the pipeline stays platform-agnostic andprovenance: falsebuilds deploy.Uploadnow uploads pre-supplied config bytes when present (rather than always re-streaming the original from the source registry).Also added
types: ["node", "jest"]totsconfig.jsonso editors resolve node globals (crypto,Buffer, …).Tests
New non-AWS regression test in
tests/api/internal/transfer.test.ts— collectsbusyboxwithlayersFrom: ghcr.io/scaffoldly/rowdy:betaand asserts, for each injected platform, that the rewritten config's digest/size matchdata.configandmanifest.layers.length === config.rootfs.diff_ids.length.yarn build:app✅yarn lint✅yarn jest tests/api/internal/transfer.test.ts✅ (issue Multi-arch image: injected rowdy layer not added to config.rootfs.diff_ids → invalid image, app layer dropped (ENOENT) #5 test green against live registries)🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.