Graduate lockfile from experimental to stable#1212
Conversation
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
chrmarti
left a comment
There was a problem hiding this comment.
Great, thanks! Really just one question: The two experimental properties are no longer needed in code, it seems?
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
|
@chrmarti please look at my most recent commit that adds This could cause confusion for developers who run a test and see the lockfile changed, giving a false indication that their code change caused the lockfile to change. I found this when running tests unrelated to the lockfile tests. The downside is that intentionally adding a lockfile to these folders requires an explicit |
chrmarti
left a comment
There was a problem hiding this comment.
LGTM! Could you resolve the merge conflict? Otherwise ready for merging. Thanks!
…ate-lockfile Bring in the 0.86.1 fix that excludes `--additional-features`-only features from the lockfile, and integrate it with the lockfile graduation work on this branch. Conflict resolution: - src/spec-configuration/containerFeaturesConfiguration.ts: Drop the `initLockfile` argument from the `writeLockfile` call. The graduation work removed `forceInitLockfile` from `writeLockfile` and `initLockfile` from `readLockfile`'s return, so the incoming reference no longer compiles. The remaining `generateLockfile(featuresConfig, config, additionalFeatures)` call from main is preserved inside the `!params.noLockfile` guard. Other reconciliations: - package.json: keep 0.87.0 (graduation release) instead of 0.86.1 so the version reflects the graduation feature set being shipped. - CHANGELOG.md: move the 0.87.0 graduation entry under "May 2026" and nest the 0.86.1 bullet beneath it for historical accuracy. Signed-off-by: Brooke Hamilton <Brooke.Hamilton+microsoft@microsoft.com>
Signed-off-by: Brooke Hamilton <Brooke.Hamilton+microsoft@microsoft.com>
Done. I also made an additional edit to the |
|
@brooke-hamilton I missed that there is a test failing. Could you take a look? |
Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com>
I pushed a fix. I was able to duplicate the failure and fix on my local dev environment and on a fork. |
Phase 1 of lockfile graduation per docs/ROADMAP_TO_1.0.md. Ships the schema parity fixes, the user-visible CLI flag surface, the writer helper, and upstream-aligned error messages. The actual writer integration (building a Lockfile from resolved features and writing it after `up`/`build`) is tracked as PR-4b at issue #32. ## Schema parity (deacon-core/lockfile.rs) - LockfileFeature.depends_on now serializes as `dependsOn` (camelCase) via #[serde(rename)], matching upstream `devcontainers/cli`'s generateLockfile in src/spec-configuration/lockfile.ts. Deserializer accepts the camelCase form on read. - write_lockfile() emits a trailing newline to match upstream `JSON.stringify(..., 2) + '\n'`. Byte-identical output keeps the --frozen-lockfile content comparison stable. - LockfileValidationResult::format_error() now emits upstream-aligned strings: "Lockfile does not exist." / "Lockfile does not match." as the leading summary line; trailing actionable guidance references the graduated --frozen-lockfile flag rather than the deprecated --experimental-frozen-lockfile. - New LockfileFeature::from_resolved() helper constructs entries in the upstream canonical form (resolved = "{registry}/{repo}@{digest}", integrity = "{digest}"). This is the writer entry point PR-4b uses. ## CLI surface graduation up + build both gain: - --no-lockfile (visible): skip lockfile generation and verification. - --frozen-lockfile (visible): require an up-to-date lockfile; fail if resolution would change it. Mutual exclusivity (--no-lockfile xor --frozen-lockfile) enforced in the CLI layer, mirroring upstream's pre-parse validation. Deprecation: - --experimental-lockfile and --experimental-frozen-lockfile remain accepted as hidden aliases through the 1.x line. The CLI emits a WARN on use directing users to the graduated flags. - effective_frozen = frozen_lockfile || experimental_frozen_lockfile matches upstream's effectiveFrozenLockfile coalescing. ## Downstream consumers - up/mod.rs frozen-validation path now reads args.frozen_lockfile (the effective value) instead of args.experimental_frozen_lockfile. No behavior change — only the variable name moves. - build/mod.rs args struct gains no_lockfile + frozen_lockfile fields (carrier-only for PR-4b; #[allow(dead_code)] until then). ## Tests Added in deacon-core/lockfile.rs: - test_depends_on_serializes_as_camel_case - test_write_lockfile_emits_trailing_newline - test_from_resolved_constructs_upstream_form Updated in deacon/tests/up_lockfile_frozen.rs (5 assertions): align with the new upstream-format error messages ("Lockfile does not exist." / "Lockfile does not match." / capital "F" on "Features ..." substrings). ## Verified - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo nextest run --profile dev-fast --no-default-features: 1930/1930 pass - cargo test --doc --workspace: 130/130 pass ## Follow-up tracked - #32 — PR-4b: wire writer in up + build feature pipeline. - Build-command lockfile wiring is gated on the pre-existing TODO at build/mod.rs:1285 (build doesn't install features today). Will land with #32 or as a sibling PR. ## Refs - docs/ROADMAP_TO_1.0.md Tier 1 item "Lockfile graduation" - Upstream: devcontainers/cli#1212 (graduated lockfile in v0.87.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of lockfile graduation per docs/ROADMAP_TO_1.0.md. Ships the schema parity fixes, the user-visible CLI flag surface, the writer helper, and upstream-aligned error messages. The actual writer integration (building a Lockfile from resolved features and writing it after `up`/`build`) is tracked as PR-4b at issue #32. ## Schema parity (deacon-core/lockfile.rs) - LockfileFeature.depends_on now serializes as `dependsOn` (camelCase) via #[serde(rename)], matching upstream `devcontainers/cli`'s generateLockfile in src/spec-configuration/lockfile.ts. Deserializer accepts the camelCase form on read. - write_lockfile() emits a trailing newline to match upstream `JSON.stringify(..., 2) + '\n'`. Byte-identical output keeps the --frozen-lockfile content comparison stable. - LockfileValidationResult::format_error() now emits upstream-aligned strings: "Lockfile does not exist." / "Lockfile does not match." as the leading summary line; trailing actionable guidance references the graduated --frozen-lockfile flag rather than the deprecated --experimental-frozen-lockfile. - New LockfileFeature::from_resolved() helper constructs entries in the upstream canonical form (resolved = "{registry}/{repo}@{digest}", integrity = "{digest}"). This is the writer entry point PR-4b uses. ## CLI surface graduation up + build both gain: - --no-lockfile (visible): skip lockfile generation and verification. - --frozen-lockfile (visible): require an up-to-date lockfile; fail if resolution would change it. Mutual exclusivity (--no-lockfile xor --frozen-lockfile) enforced in the CLI layer, mirroring upstream's pre-parse validation. Deprecation: - --experimental-lockfile and --experimental-frozen-lockfile remain accepted as hidden aliases through the 1.x line. The CLI emits a WARN on use directing users to the graduated flags. - effective_frozen = frozen_lockfile || experimental_frozen_lockfile matches upstream's effectiveFrozenLockfile coalescing. ## Downstream consumers - up/mod.rs frozen-validation path now reads args.frozen_lockfile (the effective value) instead of args.experimental_frozen_lockfile. No behavior change — only the variable name moves. - build/mod.rs args struct gains no_lockfile + frozen_lockfile fields (carrier-only for PR-4b; #[allow(dead_code)] until then). ## Tests Added in deacon-core/lockfile.rs: - test_depends_on_serializes_as_camel_case - test_write_lockfile_emits_trailing_newline - test_from_resolved_constructs_upstream_form Updated in deacon/tests/up_lockfile_frozen.rs (5 assertions): align with the new upstream-format error messages ("Lockfile does not exist." / "Lockfile does not match." / capital "F" on "Features ..." substrings). ## Verified - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo nextest run --profile dev-fast --no-default-features: 1930/1930 pass - cargo test --doc --workspace: 130/130 pass ## Follow-up tracked - #32 — PR-4b: wire writer in up + build feature pipeline. - Build-command lockfile wiring is gated on the pre-existing TODO at build/mod.rs:1285 (build doesn't install features today). Will land with #32 or as a sibling PR. ## Refs - docs/ROADMAP_TO_1.0.md Tier 1 item "Lockfile graduation" - Upstream: devcontainers/cli#1212 (graduated lockfile in v0.87.0) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4b) (#42) * feat: lockfile graduation — schema parity + CLI surface (PR-4a) Phase 1 of lockfile graduation per docs/ROADMAP_TO_1.0.md. Ships the schema parity fixes, the user-visible CLI flag surface, the writer helper, and upstream-aligned error messages. The actual writer integration (building a Lockfile from resolved features and writing it after `up`/`build`) is tracked as PR-4b at issue #32. ## Schema parity (deacon-core/lockfile.rs) - LockfileFeature.depends_on now serializes as `dependsOn` (camelCase) via #[serde(rename)], matching upstream `devcontainers/cli`'s generateLockfile in src/spec-configuration/lockfile.ts. Deserializer accepts the camelCase form on read. - write_lockfile() emits a trailing newline to match upstream `JSON.stringify(..., 2) + '\n'`. Byte-identical output keeps the --frozen-lockfile content comparison stable. - LockfileValidationResult::format_error() now emits upstream-aligned strings: "Lockfile does not exist." / "Lockfile does not match." as the leading summary line; trailing actionable guidance references the graduated --frozen-lockfile flag rather than the deprecated --experimental-frozen-lockfile. - New LockfileFeature::from_resolved() helper constructs entries in the upstream canonical form (resolved = "{registry}/{repo}@{digest}", integrity = "{digest}"). This is the writer entry point PR-4b uses. ## CLI surface graduation up + build both gain: - --no-lockfile (visible): skip lockfile generation and verification. - --frozen-lockfile (visible): require an up-to-date lockfile; fail if resolution would change it. Mutual exclusivity (--no-lockfile xor --frozen-lockfile) enforced in the CLI layer, mirroring upstream's pre-parse validation. Deprecation: - --experimental-lockfile and --experimental-frozen-lockfile remain accepted as hidden aliases through the 1.x line. The CLI emits a WARN on use directing users to the graduated flags. - effective_frozen = frozen_lockfile || experimental_frozen_lockfile matches upstream's effectiveFrozenLockfile coalescing. ## Downstream consumers - up/mod.rs frozen-validation path now reads args.frozen_lockfile (the effective value) instead of args.experimental_frozen_lockfile. No behavior change — only the variable name moves. - build/mod.rs args struct gains no_lockfile + frozen_lockfile fields (carrier-only for PR-4b; #[allow(dead_code)] until then). ## Tests Added in deacon-core/lockfile.rs: - test_depends_on_serializes_as_camel_case - test_write_lockfile_emits_trailing_newline - test_from_resolved_constructs_upstream_form Updated in deacon/tests/up_lockfile_frozen.rs (5 assertions): align with the new upstream-format error messages ("Lockfile does not exist." / "Lockfile does not match." / capital "F" on "Features ..." substrings). ## Verified - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo nextest run --profile dev-fast --no-default-features: 1930/1930 pass - cargo test --doc --workspace: 130/130 pass ## Follow-up tracked - #32 — PR-4b: wire writer in up + build feature pipeline. - Build-command lockfile wiring is gated on the pre-existing TODO at build/mod.rs:1285 (build doesn't install features today). Will land with #32 or as a sibling PR. ## Refs - docs/ROADMAP_TO_1.0.md Tier 1 item "Lockfile graduation" - Upstream: devcontainers/cli#1212 (graduated lockfile in v0.87.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: lockfile graduation — wire writer in up's feature pipeline (PR-4b) Follow-up to PR-4a (#33): the user-visible flag surface, schema parity fix, and upstream-aligned error strings landed there, but the actual writer was never invoked. This wires it in. After feature resolution + download, build a Lockfile from the resolved feature set (`build_lockfile_from_features` mirrors upstream `generateLockfile`), thread it back via FeatureBuildOutput, then: - default: write `{config_dir}/devcontainer-lock.json` (sorted by key, trailing newline — byte-identical to upstream `writeLockfile`) - `--frozen-lockfile`: byte-compare against on-disk; fail with the upstream summary strings (`"Lockfile does not exist."` / `"Lockfile does not match."`) so existing CI scripts keep working - `--no-lockfile`: skip entirely - deprecated `--experimental-lockfile <PATH>`: still honored for the custom-path form (hidden alias path through the 1.x line) Wired into both single-container (`container.rs`) and compose (`compose.rs`) flows via shared `handle_lockfile_post_build` helper. EROFS/EACCES on the write path downgrades to a WARN so read-only workspaces (CI mounts, read-only volumes) don't break `up`. Lockfile keys are the user-provided feature ID (e.g. `ghcr.io/devcontainers/features/node:1`), not the canonical no-tag form — matching upstream and keeping the existing pre-build structural validation aligned. Build (`crates/deacon/src/commands/build/mod.rs`) is intentionally out of scope — see issue #32 and the standing TODO at build/mod.rs:1285. That's PR-4c. Closes #32 (PR-4 phase 2 of the lockfile graduation track). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (PR-4c) (#41) * feat: lockfile graduation — schema parity + CLI surface (PR-4a) Phase 1 of lockfile graduation per docs/ROADMAP_TO_1.0.md. Ships the schema parity fixes, the user-visible CLI flag surface, the writer helper, and upstream-aligned error messages. The actual writer integration (building a Lockfile from resolved features and writing it after `up`/`build`) is tracked as PR-4b at issue #32. ## Schema parity (deacon-core/lockfile.rs) - LockfileFeature.depends_on now serializes as `dependsOn` (camelCase) via #[serde(rename)], matching upstream `devcontainers/cli`'s generateLockfile in src/spec-configuration/lockfile.ts. Deserializer accepts the camelCase form on read. - write_lockfile() emits a trailing newline to match upstream `JSON.stringify(..., 2) + '\n'`. Byte-identical output keeps the --frozen-lockfile content comparison stable. - LockfileValidationResult::format_error() now emits upstream-aligned strings: "Lockfile does not exist." / "Lockfile does not match." as the leading summary line; trailing actionable guidance references the graduated --frozen-lockfile flag rather than the deprecated --experimental-frozen-lockfile. - New LockfileFeature::from_resolved() helper constructs entries in the upstream canonical form (resolved = "{registry}/{repo}@{digest}", integrity = "{digest}"). This is the writer entry point PR-4b uses. ## CLI surface graduation up + build both gain: - --no-lockfile (visible): skip lockfile generation and verification. - --frozen-lockfile (visible): require an up-to-date lockfile; fail if resolution would change it. Mutual exclusivity (--no-lockfile xor --frozen-lockfile) enforced in the CLI layer, mirroring upstream's pre-parse validation. Deprecation: - --experimental-lockfile and --experimental-frozen-lockfile remain accepted as hidden aliases through the 1.x line. The CLI emits a WARN on use directing users to the graduated flags. - effective_frozen = frozen_lockfile || experimental_frozen_lockfile matches upstream's effectiveFrozenLockfile coalescing. ## Downstream consumers - up/mod.rs frozen-validation path now reads args.frozen_lockfile (the effective value) instead of args.experimental_frozen_lockfile. No behavior change — only the variable name moves. - build/mod.rs args struct gains no_lockfile + frozen_lockfile fields (carrier-only for PR-4b; #[allow(dead_code)] until then). ## Tests Added in deacon-core/lockfile.rs: - test_depends_on_serializes_as_camel_case - test_write_lockfile_emits_trailing_newline - test_from_resolved_constructs_upstream_form Updated in deacon/tests/up_lockfile_frozen.rs (5 assertions): align with the new upstream-format error messages ("Lockfile does not exist." / "Lockfile does not match." / capital "F" on "Features ..." substrings). ## Verified - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo nextest run --profile dev-fast --no-default-features: 1930/1930 pass - cargo test --doc --workspace: 130/130 pass ## Follow-up tracked - #32 — PR-4b: wire writer in up + build feature pipeline. - Build-command lockfile wiring is gated on the pre-existing TODO at build/mod.rs:1285 (build doesn't install features today). Will land with #32 or as a sibling PR. ## Refs - docs/ROADMAP_TO_1.0.md Tier 1 item "Lockfile graduation" - Upstream: devcontainers/cli#1212 (graduated lockfile in v0.87.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: lockfile graduation — wire writer in up's feature pipeline (PR-4b) Follow-up to PR-4a (#33): the user-visible flag surface, schema parity fix, and upstream-aligned error strings landed there, but the actual writer was never invoked. This wires it in. After feature resolution + download, build a Lockfile from the resolved feature set (`build_lockfile_from_features` mirrors upstream `generateLockfile`), thread it back via FeatureBuildOutput, then: - default: write `{config_dir}/devcontainer-lock.json` (sorted by key, trailing newline — byte-identical to upstream `writeLockfile`) - `--frozen-lockfile`: byte-compare against on-disk; fail with the upstream summary strings (`"Lockfile does not exist."` / `"Lockfile does not match."`) so existing CI scripts keep working - `--no-lockfile`: skip entirely - deprecated `--experimental-lockfile <PATH>`: still honored for the custom-path form (hidden alias path through the 1.x line) Wired into both single-container (`container.rs`) and compose (`compose.rs`) flows via shared `handle_lockfile_post_build` helper. EROFS/EACCES on the write path downgrades to a WARN so read-only workspaces (CI mounts, read-only volumes) don't break `up`. Lockfile keys are the user-provided feature ID (e.g. `ghcr.io/devcontainers/features/node:1`), not the canonical no-tag form — matching upstream and keeping the existing pre-build structural validation aligned. Build (`crates/deacon/src/commands/build/mod.rs`) is intentionally out of scope — see issue #32 and the standing TODO at build/mod.rs:1285. That's PR-4c. Closes #32 (PR-4 phase 2 of the lockfile graduation track). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: build — install features + write lockfile for Dockerfile builds (PR-4c) Removes the long-standing "Feature installation during build is not yet implemented" fail-fast for the Dockerfile-based build path, and wires the PR-4b lockfile writer into `build`'s post-build flow. ## What this PR does When `deacon build` is invoked against a Dockerfile-based config with features declared: 1. Run the user's `docker build` as before (Dockerfile, build args, cache, labels — all unchanged). 2. **NEW** — layer features on top of the just-built image: synthesize a single-container config with `image: <built_image_id>` and call `up`'s existing `build_image_with_features` helper. That helper resolves + downloads features, generates a BuildKit RUN-mount stage per feature, and produces a feature-extended image + a `Lockfile`. 3. **NEW** — write the lockfile next to `devcontainer.json` via `deacon_core::lockfile::write_lockfile(force_init = true)`. Read-only workspaces (EROFS/EACCES) downgrade to a WARN so CI mounts don't fail. 4. Update `final_result.image_id` to the feature-extended image so downstream consumers (cache, scan, output, push) see the right tag. Compose-based and image-reference builds still fail fast with features — their integration patterns differ enough to warrant separate follow-ups. ## Cache invalidation When features are present, the cache check is skipped (`!features_present` gate added). The current `config_hash` derives from `build_config` only — it does not fold in feature digests, so a cached hit would point at the base image without feature layers. Skipping the cache when features are present trades a rebuild for correctness; a later refinement can include feature digests in the hash for cacheable feature builds. ## Module visibility `crates/deacon/src/commands/up/mod.rs` — `features_build` module was private; promoted to `pub(crate)` so the build command can call `build_image_with_features` without copying ~500 LoC of feature pipeline into `commands/build/`. A future cleanup can lift the helper into a shared `commands/shared/` module. ## Tests 3 new unit tests in `build::tests`: - `is_readonly_filesystem_error_detects_permission_denied` - `is_readonly_filesystem_error_ignores_unrelated_io_errors` (NotFound etc. must propagate so real bugs aren't hidden) - `is_readonly_filesystem_error_ignores_non_io_errors` These pin the WARN-vs-error decision for the lockfile write path. The actual feature-install + lockfile-write integration tests require Docker + BuildKit + an OCI registry; they belong in an integration test suite that I haven't added here (test grouping under `docker-shared` is the right home). Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 229 pass (no regression; 3 new tests) ## Base branch Based on `pr4b-lockfile-writer-wiring` (#35) since this PR consumes `FeatureBuildOutput.lockfile` (the field PR-4b added). Auto-rebases to `main` once #33 and #35 land in order. With this PR, the `build` command supports features for Dockerfile-based configs and writes a lockfile alongside — closes the lockfile track's remaining 1.0 work (PR-4c on issue #34). Refs: issue #34 (Tier 1 progress tracker); PR-4b (#35). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: lockfile graduation — schema parity + CLI surface (PR-4a) Phase 1 of lockfile graduation per docs/ROADMAP_TO_1.0.md. Ships the schema parity fixes, the user-visible CLI flag surface, the writer helper, and upstream-aligned error messages. The actual writer integration (building a Lockfile from resolved features and writing it after `up`/`build`) is tracked as PR-4b at issue #32. ## Schema parity (deacon-core/lockfile.rs) - LockfileFeature.depends_on now serializes as `dependsOn` (camelCase) via #[serde(rename)], matching upstream `devcontainers/cli`'s generateLockfile in src/spec-configuration/lockfile.ts. Deserializer accepts the camelCase form on read. - write_lockfile() emits a trailing newline to match upstream `JSON.stringify(..., 2) + '\n'`. Byte-identical output keeps the --frozen-lockfile content comparison stable. - LockfileValidationResult::format_error() now emits upstream-aligned strings: "Lockfile does not exist." / "Lockfile does not match." as the leading summary line; trailing actionable guidance references the graduated --frozen-lockfile flag rather than the deprecated --experimental-frozen-lockfile. - New LockfileFeature::from_resolved() helper constructs entries in the upstream canonical form (resolved = "{registry}/{repo}@{digest}", integrity = "{digest}"). This is the writer entry point PR-4b uses. ## CLI surface graduation up + build both gain: - --no-lockfile (visible): skip lockfile generation and verification. - --frozen-lockfile (visible): require an up-to-date lockfile; fail if resolution would change it. Mutual exclusivity (--no-lockfile xor --frozen-lockfile) enforced in the CLI layer, mirroring upstream's pre-parse validation. Deprecation: - --experimental-lockfile and --experimental-frozen-lockfile remain accepted as hidden aliases through the 1.x line. The CLI emits a WARN on use directing users to the graduated flags. - effective_frozen = frozen_lockfile || experimental_frozen_lockfile matches upstream's effectiveFrozenLockfile coalescing. ## Downstream consumers - up/mod.rs frozen-validation path now reads args.frozen_lockfile (the effective value) instead of args.experimental_frozen_lockfile. No behavior change — only the variable name moves. - build/mod.rs args struct gains no_lockfile + frozen_lockfile fields (carrier-only for PR-4b; #[allow(dead_code)] until then). ## Tests Added in deacon-core/lockfile.rs: - test_depends_on_serializes_as_camel_case - test_write_lockfile_emits_trailing_newline - test_from_resolved_constructs_upstream_form Updated in deacon/tests/up_lockfile_frozen.rs (5 assertions): align with the new upstream-format error messages ("Lockfile does not exist." / "Lockfile does not match." / capital "F" on "Features ..." substrings). ## Verified - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo nextest run --profile dev-fast --no-default-features: 1930/1930 pass - cargo test --doc --workspace: 130/130 pass ## Follow-up tracked - #32 — PR-4b: wire writer in up + build feature pipeline. - Build-command lockfile wiring is gated on the pre-existing TODO at build/mod.rs:1285 (build doesn't install features today). Will land with #32 or as a sibling PR. ## Refs - docs/ROADMAP_TO_1.0.md Tier 1 item "Lockfile graduation" - Upstream: devcontainers/cli#1212 (graduated lockfile in v0.87.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: upgrade subcommand — regenerate-lockfile MVP (PR-5a) Implements the regenerate path of `deacon upgrade` per `docs/subcommand-specs/upgrade/SPEC.md`: re-resolve every Feature against the OCI registry, build a fresh `Lockfile`, and either print it to stdout (`--dry-run`) or write it to disk via `write_lockfile(force_init = true)`. ## CLI surface `deacon upgrade [--dry-run] [--docker-path <path>] [--docker-compose-path <path>] [-f <id>] [-v <ver>]` Workspace + `--config` come from the global flags (parity with other consumer subcommands). `-f`/`--feature` and `-v`/`--target-version` are hidden (Dependabot-only). ## What this PR includes - Validation (fail-fast per spec §2/§3): - `--feature` and `--target-version` must both be set or both absent; error message matches upstream: `"The '--target-version' and '--feature' flag must be used together."` - `--target-version` must match `^\d+(\.\d+(\.\d+)?)?$`; error matches upstream: `"Invalid version 'X'. Must be in the form of 'x', 'x.y', or 'x.y.z'"` - Config load via the shared `load_config` helper (extends chain honored). - Feature resolution via the existing OCI fetcher (`default_fetcher()` + `fetch_feature`), which returns each feature's manifest digest + metadata version. - Lockfile assembly keyed by user-provided feature ID (matches upstream `generateLockfile` + PR-4b's helper in `up`). Metadata-version missing → falls back to the user-requested tag (e.g. `"1"`) with a WARN. - `--dry-run`: pretty-printed canonical JSON to stdout (sorted keys, trailing newline) — byte-identical to what `write_lockfile` would emit. - Default: `write_lockfile(force_init = true)` per spec §5 phase 4. ## Tests 13 new unit tests in `upgrade::tests`: - Pin-flag pairing: both-set, both-absent, only-feature, only-target - Target-version regex: 5 valid forms, 8 invalid forms (incl. `latest`, `v1`, `1.x.3`) - Spec-exact error message on invalid `--target-version` - Lockfile entry assembly: metadata-version vs tag fallback, sorted `dependsOn` - Empty-features short-circuit (two flavors — missing object, empty object) - Argument defaults Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 226 pass (no regression; baseline includes PR-4a since this branch is based on `pr4-lockfile-graduation`) ## Deferred to PR-5b - `--feature` / `--target-version` config-pin behavior (modifies `devcontainer.json` in place). The flags are accepted today so the CLI surface is stable, but using them returns `"--feature/--target-version pinning is not yet implemented (PR-5b)"` rather than silently doing nothing. ## Code-dedup follow-up The lockfile-entry assembly logic is intentionally duplicated from PR-4b (`crates/deacon/src/commands/up/features_build.rs::build_lockfile_from_features`) so PR-5a doesn't depend on PR-4b's diff. Once both land on `main`, a small follow-up PR can lift the shared logic into `deacon_core::lockfile`. ## Base branch Based on `pr4-lockfile-graduation` (#33) since upgrade needs `deacon_core::lockfile::{Lockfile, LockfileFeature, get_lockfile_path, write_lockfile}` — all added in PR-4a. After #33 merges, this branch auto-rebases onto `main`. Closes part of the "Implement `upgrade`" Tier 1 blocker (issue #34). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5b) (#40) * feat: lockfile graduation — schema parity + CLI surface (PR-4a) Phase 1 of lockfile graduation per docs/ROADMAP_TO_1.0.md. Ships the schema parity fixes, the user-visible CLI flag surface, the writer helper, and upstream-aligned error messages. The actual writer integration (building a Lockfile from resolved features and writing it after `up`/`build`) is tracked as PR-4b at issue #32. ## Schema parity (deacon-core/lockfile.rs) - LockfileFeature.depends_on now serializes as `dependsOn` (camelCase) via #[serde(rename)], matching upstream `devcontainers/cli`'s generateLockfile in src/spec-configuration/lockfile.ts. Deserializer accepts the camelCase form on read. - write_lockfile() emits a trailing newline to match upstream `JSON.stringify(..., 2) + '\n'`. Byte-identical output keeps the --frozen-lockfile content comparison stable. - LockfileValidationResult::format_error() now emits upstream-aligned strings: "Lockfile does not exist." / "Lockfile does not match." as the leading summary line; trailing actionable guidance references the graduated --frozen-lockfile flag rather than the deprecated --experimental-frozen-lockfile. - New LockfileFeature::from_resolved() helper constructs entries in the upstream canonical form (resolved = "{registry}/{repo}@{digest}", integrity = "{digest}"). This is the writer entry point PR-4b uses. ## CLI surface graduation up + build both gain: - --no-lockfile (visible): skip lockfile generation and verification. - --frozen-lockfile (visible): require an up-to-date lockfile; fail if resolution would change it. Mutual exclusivity (--no-lockfile xor --frozen-lockfile) enforced in the CLI layer, mirroring upstream's pre-parse validation. Deprecation: - --experimental-lockfile and --experimental-frozen-lockfile remain accepted as hidden aliases through the 1.x line. The CLI emits a WARN on use directing users to the graduated flags. - effective_frozen = frozen_lockfile || experimental_frozen_lockfile matches upstream's effectiveFrozenLockfile coalescing. ## Downstream consumers - up/mod.rs frozen-validation path now reads args.frozen_lockfile (the effective value) instead of args.experimental_frozen_lockfile. No behavior change — only the variable name moves. - build/mod.rs args struct gains no_lockfile + frozen_lockfile fields (carrier-only for PR-4b; #[allow(dead_code)] until then). ## Tests Added in deacon-core/lockfile.rs: - test_depends_on_serializes_as_camel_case - test_write_lockfile_emits_trailing_newline - test_from_resolved_constructs_upstream_form Updated in deacon/tests/up_lockfile_frozen.rs (5 assertions): align with the new upstream-format error messages ("Lockfile does not exist." / "Lockfile does not match." / capital "F" on "Features ..." substrings). ## Verified - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo nextest run --profile dev-fast --no-default-features: 1930/1930 pass - cargo test --doc --workspace: 130/130 pass ## Follow-up tracked - #32 — PR-4b: wire writer in up + build feature pipeline. - Build-command lockfile wiring is gated on the pre-existing TODO at build/mod.rs:1285 (build doesn't install features today). Will land with #32 or as a sibling PR. ## Refs - docs/ROADMAP_TO_1.0.md Tier 1 item "Lockfile graduation" - Upstream: devcontainers/cli#1212 (graduated lockfile in v0.87.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: upgrade subcommand — regenerate-lockfile MVP (PR-5a) Implements the regenerate path of `deacon upgrade` per `docs/subcommand-specs/upgrade/SPEC.md`: re-resolve every Feature against the OCI registry, build a fresh `Lockfile`, and either print it to stdout (`--dry-run`) or write it to disk via `write_lockfile(force_init = true)`. ## CLI surface `deacon upgrade [--dry-run] [--docker-path <path>] [--docker-compose-path <path>] [-f <id>] [-v <ver>]` Workspace + `--config` come from the global flags (parity with other consumer subcommands). `-f`/`--feature` and `-v`/`--target-version` are hidden (Dependabot-only). ## What this PR includes - Validation (fail-fast per spec §2/§3): - `--feature` and `--target-version` must both be set or both absent; error message matches upstream: `"The '--target-version' and '--feature' flag must be used together."` - `--target-version` must match `^\d+(\.\d+(\.\d+)?)?$`; error matches upstream: `"Invalid version 'X'. Must be in the form of 'x', 'x.y', or 'x.y.z'"` - Config load via the shared `load_config` helper (extends chain honored). - Feature resolution via the existing OCI fetcher (`default_fetcher()` + `fetch_feature`), which returns each feature's manifest digest + metadata version. - Lockfile assembly keyed by user-provided feature ID (matches upstream `generateLockfile` + PR-4b's helper in `up`). Metadata-version missing → falls back to the user-requested tag (e.g. `"1"`) with a WARN. - `--dry-run`: pretty-printed canonical JSON to stdout (sorted keys, trailing newline) — byte-identical to what `write_lockfile` would emit. - Default: `write_lockfile(force_init = true)` per spec §5 phase 4. ## Tests 13 new unit tests in `upgrade::tests`: - Pin-flag pairing: both-set, both-absent, only-feature, only-target - Target-version regex: 5 valid forms, 8 invalid forms (incl. `latest`, `v1`, `1.x.3`) - Spec-exact error message on invalid `--target-version` - Lockfile entry assembly: metadata-version vs tag fallback, sorted `dependsOn` - Empty-features short-circuit (two flavors — missing object, empty object) - Argument defaults Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 226 pass (no regression; baseline includes PR-4a since this branch is based on `pr4-lockfile-graduation`) ## Deferred to PR-5b - `--feature` / `--target-version` config-pin behavior (modifies `devcontainer.json` in place). The flags are accepted today so the CLI surface is stable, but using them returns `"--feature/--target-version pinning is not yet implemented (PR-5b)"` rather than silently doing nothing. ## Code-dedup follow-up The lockfile-entry assembly logic is intentionally duplicated from PR-4b (`crates/deacon/src/commands/up/features_build.rs::build_lockfile_from_features`) so PR-5a doesn't depend on PR-4b's diff. Once both land on `main`, a small follow-up PR can lift the shared logic into `deacon_core::lockfile`. ## Base branch Based on `pr4-lockfile-graduation` (#33) since upgrade needs `deacon_core::lockfile::{Lockfile, LockfileFeature, get_lockfile_path, write_lockfile}` — all added in PR-4a. After #33 merges, this branch auto-rebases onto `main`. Closes part of the "Implement `upgrade`" Tier 1 blocker (issue #34). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: upgrade — wire --feature / --target-version config pinning (PR-5b) Follow-up to PR-5a (#39). Implements spec §5 phase 2: when both `--feature` and `--target-version` are set, rewrite the matching feature key in `devcontainer.json` in place BEFORE the lockfile regeneration phase. ## Implementation **Text-level surgical edit** — we read the config file as a string, find the literal quoted JSON key for `--feature`, and replace its tag with `--target-version`. We never parse-and-re-emit the JSON, so: - Comments survive (`//` and `/* */` in JSONC/JSON5) - Whitespace and key order are preserved - No dependency on a JSON5-AST library Trade-offs (all acceptable given the edit is bounded to one key): - Registry-port colons are correctly distinguished from tag colons via the rule "tag separator is the last `:` after the last `/`" - The substring search is precise enough to ignore escaped-quote occurrences inside string values (the byte sequences differ: `\"feature:1\"` vs `"feature:1"`) - Multiple unescaped matches (key appears both as a JSON key AND as an unescaped string value) is detected and reported as ambiguous, with the user asked to resolve manually ## What this PR changes In `crates/deacon/src/commands/upgrade.rs`: - `pin_feature_in_config_file()` — reads, rewrites, writes; bubbles context on IO failures and errors on missing/ambiguous matches. - `pinned_feature_key()` — pure helper that handles tag replacement, no-tag append, and registry-port disambiguation. - `rewrite_feature_key()` — pure helper that surgically replaces the literal `"<feature>"` substring and reports the occurrence count. - The `execute_upgrade` flow now (a) edits the config when both flags are set, then (b) re-reads via the shared loader so the resolution phase sees the pinned form. Spec §5: "Re-read config for consistency after edit". - The previous "not yet implemented" surface-parity error is removed. In `crates/deacon/src/cli.rs`: - Doc strings on `--feature` / `--target-version` no longer flag them as deferred. ## Tests 10 new unit tests: - `pinned_feature_key_*` (3): tag replacement, tag append when absent, registry-port disambiguation - `rewrite_feature_key_*` (4): single-match replace, unrelated keys preserved, zero-match reports 0, ambiguous reports the actual count, escaped-quote substring is correctly skipped - `pin_feature_in_config_file_*` (2): round-trip through disk preserves comments + sibling keys; missing-feature surfaces a clear error Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 236 pass (no regression; 23 upgrade tests total — 13 from PR-5a + 10 new) ## Base branch Based on `pr5-upgrade-subcommand` (#39) so the diff shows only PR-5b's additions. Auto-rebases to `main` once #33 and #39 land in order. With this PR, the `upgrade` subcommand is functionally complete for spec §5. Refs: issue #34 (Tier 1 progress tracker); PR-5a (#39). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This pull request graduates the lockfile feature from experimental to stable. Lockfiles are now generated by default during
buildandupcommands to ensure reproducible builds. New CLI flags (--no-lockfileand--frozen-lockfile) are introduced for more explicit control, while the old experimental flags are deprecated but still accepted with warnings.Lockfile Feature Graduation and CLI Changes:
.devcontainer-lock.json) are now generated by default onbuildandup, pinning feature versions for reproducibility. [1] [2]--no-lockfile(disables lockfile generation/verification) and--frozen-lockfile(enforces lockfile existence and immutability). Deprecated--experimental-lockfileand--experimental-frozen-lockfilewith warnings. [1] [2] [3] [4] [5] [6]Documentation and Tooling:
README.mdandCHANGELOG.mdto reflect lockfile graduation and new CLI behavior. [1] [2]docs/contributing-code.mdto clarify packaging requirements before running tests.This PR closes issue #1195
There are no spec changes in this PR, but there is a related proposal at devcontainers/spec#727