Commit c3c5407
feat: lockfile graduation — wire writer in up's feature pipeline (PR-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>1 parent cca87c3 commit c3c5407
4 files changed
Lines changed: 699 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
253 | 254 | | |
254 | 255 | | |
255 | 256 | | |
256 | | - | |
257 | | - | |
258 | 257 | | |
259 | | - | |
| 258 | + | |
260 | 259 | | |
261 | 260 | | |
262 | 261 | | |
| |||
265 | 264 | | |
266 | 265 | | |
267 | 266 | | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
268 | 275 | | |
269 | 276 | | |
270 | 277 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
219 | 225 | | |
220 | 226 | | |
221 | 227 | | |
| |||
0 commit comments