Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2de5490
fix(templates): substitute template options as the reference does
aledbf Oct 26, 2026
d4440bf
fix(config): load wrong-typed legacy and customizations properties
aledbf Oct 26, 2026
a58b59e
fix(imagemeta): merge forwardPorts as the reference does
aledbf Oct 26, 2026
3640339
fix(features): order lockfile entries as the reference does
aledbf Oct 26, 2026
20505ca
fix(cli): read the set-up config as the reference does
aledbf Oct 26, 2026
054f14f
docs: record that invalid JSONC configurations fail to load
aledbf Oct 26, 2026
d52c9f5
refactor(imagemeta): drop the integer forwardPorts branches
aledbf Oct 26, 2026
f4ca504
test: hold the edits mutation testing found unguarded
aledbf Oct 26, 2026
247b826
fix(log): write the text log in the reference's two formats
aledbf Oct 26, 2026
e06be4e
fix(cli): check the terminal size flags by presence, as yargs does
aledbf Oct 26, 2026
58281eb
fix(cli): report a compose build's image name as the reference does
aledbf Oct 26, 2026
6b3e87e
fix(oci): parse cross-origin auth hosts as WHATWG URL does
aledbf Oct 26, 2026
8f55a98
fix(oci)!: reach only a localhost registry over plain HTTP
aledbf Oct 26, 2026
23e29db
fix(cli): fail --cache-key on an unreadable features lockfile
aledbf Oct 26, 2026
8aaeb48
fix(pfs): extract template tarballs the way node-tar does
aledbf Oct 26, 2026
95056a1
fix(config): default a compose workspace folder to the root
aledbf Oct 26, 2026
ef41b6d
fix(cli): report frozen lockfile failures as the reference does
aledbf Oct 26, 2026
d830d6e
fix(cli): write command results in the reference's key order
aledbf Oct 26, 2026
72f3d8c
fix(features): fail a malformed lockfile with the reference's message
aledbf Oct 26, 2026
2598f62
fix(cli): fail outdated on a lockfile it cannot read
aledbf Oct 26, 2026
287b9a2
fix(cli): fail resolve-dependencies on a lockfile it cannot read
aledbf Oct 26, 2026
e15c00d
fix(cli): fail up and build on a lockfile they cannot read
aledbf Oct 26, 2026
5021a26
fix(cli): fail Compose up and build on a lockfile they cannot read
aledbf Oct 26, 2026
0cb2210
fix(cli): fail --cache-key on a Dockerfile it cannot read
aledbf Oct 26, 2026
e397251
fix(cli): write command results without escaping html characters
aledbf Oct 26, 2026
e596abe
fix(cli): write configFilePath's fsPath in up where the reference does
aledbf Oct 26, 2026
cab91a7
fix(cli): give configFilePath the file scheme only for --config
aledbf Oct 26, 2026
c831107
fix(cli): report the git worktree common dir mount in read-configuration
aledbf Oct 26, 2026
294611c
fix(cli)!: write the Feature lockfile by default as the reference does
aledbf Oct 26, 2026
86b9505
fix(cli): resolve an absolute Dockerfile and its context as the refer…
aledbf Oct 26, 2026
dbdf8e5
refactor(cli): report frozen-lockfile errors through the reference-er…
aledbf Oct 26, 2026
83b9ad4
test(parity): lock the cases whose setup writes Features
aledbf Oct 26, 2026
baf5cb0
test(cli): hold composeFeatureSets' lockfile read with a unit test
aledbf Oct 26, 2026
37f7fcd
build: pin go-tools with mutate's isolated test processes
aledbf Oct 26, 2026
400e1f6
fix(cli): reject a terminal size flag value int cannot hold
aledbf Oct 26, 2026
d832273
fix(oci): read an ASCII auth host as Node.js 24 does
aledbf Oct 26, 2026
0365a01
test(parity): run the ts-oracle in the pinned node image
aledbf Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,16 +249,16 @@ tasks:
- 'echo "Schema refreshed. Run ''task spec:compliance'' — any new spec property fails until modeled in internal/config/types.go."'

testdata:ts-oracle:
desc: Rewrite every internal/**/testdata/ts-oracle/*.json with the results of the reference TypeScript functions (tools/tsoracle)
desc: Rewrite every internal/**/testdata/ts-oracle/*.json with the results of the reference TypeScript functions (tools/tsoracle), run in the node image versions.yaml pins (needs Docker)
deps: [reference:compile]
cmds:
- go run ./cmd/devtool ts-oracle
- go run ./cmd/devtool ts-oracle "$(go tool versions ref node)"

testdata:ts-oracle:check:
desc: Fail when a testdata/ts-oracle golden differs from what the reference TypeScript functions return
deps: [reference:compile]
cmds:
- go test ./internal/devtools -run '^TestTSOracleGoldensMatchTheReference$' -count=1 -v
- TS_ORACLE_IMAGE="$(go tool versions ref node)" go test ./internal/devtools -run '^TestTSOracleGoldensMatchTheReference$' -count=1 -v

reference:
desc: Install and compile the TypeScript parity oracle
Expand All @@ -280,7 +280,7 @@ tasks:
# must be in the inventory (needs reference/ checked out, so it lives here).
- go test ./internal/cli -run TestOracleFlagCoverage -count=1
# The ts-oracle goldens must be what the reference's functions return.
- go test ./internal/devtools -run '^TestTSOracleGoldensMatchTheReference$' -count=1
- TS_ORACLE_IMAGE="$(go tool versions ref node)" go test ./internal/devtools -run '^TestTSOracleGoldensMatchTheReference$' -count=1
- PARITY_REPORT_FILE={{.ROOT_DIR}}/artifacts/parity-contract.json PARITY_STRICT=true PARITY_LANE=contract PARITY_SKIP_DOCKER=true PARITY_SKIP_NETWORK=true go test ./internal/cli -run TestParityMatrix -count=1 -timeout 10m -v {{.CLI_ARGS}}

parity:semantic:
Expand Down
40 changes: 40 additions & 0 deletions docs/DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ touches a compared surface, reflected in the parity matrix.
"additional privileges requested". This CLI starts its `docker compose` processes with
`BUILDX_BAKE_ENTITLEMENTS_FS=0` unless the variable is already set
(`TestWithBakeFSEntitlement`, `TestE2E_ComposeBuildLabel`).
- **A devcontainer.json that is not valid JSONC fails to load.** TS parses the configuration
with jsonc-parser, whose error recovery keeps whatever it could read and reports nothing:
`{bad` loads as `{}`, a missing comma or trailing garbage is skipped, and
`{"image": , "remoteUser": "u"}` silently loses `image`. This CLI stops with the parse
error, naming the file, line and column (`parse config <file>: hujson: line 1, column 2:
...`), because running a configuration with properties silently dropped is worse than
refusing it. Valid JSONC (comments, trailing commas) loads as in TS
(`TestLoadRejectsAConfigThatIsNotValidJSONC`).
- **`--override-config` deep-merges** the override onto the base config, whereas TS
replaces the config wholesale (`readDocument(overrideConfigFile ?? configFile)`). With
no readable base, the override stands alone — identical to TS. This lets an orchestrator
Expand Down Expand Up @@ -72,6 +80,19 @@ touches a compared surface, reflected in the parity matrix.
reads only the first pair and takes `B`. Variable expansion, stage lookup and the final
stage name otherwise follow the reference (recorded in
`internal/{docker,cli}/testdata/ts-oracle/dockerfile.json`).
- **An unknown `${templateOption:NAME}` is always replaced with the empty string.** TS
`replaceTemplatedValues` reads `options[NAME]` from a plain object, so a name that is not
an option but an `Object.prototype` property (`constructor`, `toString`, …) becomes the
inherited function's source text (`function Object() { [native code] }`) in the applied
template. This CLI treats such a name like every other unknown option
(`internal/templates/testdata/ts-oracle/templates.json`).
- **A `devcontainer-lock.json` that parses but is not shaped like a lockfile is an
error.** The reference keeps whatever `JSON.parse` returns and looks each Feature up with
`lockfile?.features[id]?.…`: an array, a number or a string throws a `TypeError` once a
Feature is looked up, and a `features` value that is not an object pins nothing. This CLI
refuses such a file when it reads it (`parse lockfile: …`). JSON that does not parse, or a
file that cannot be read, fails with the reference's own message, and `null`, `false`, `0`
and `""` are no lockfile, as in TS (`internal/features/testdata/ts-oracle/lockfile.json`).
- **The `devcontainer.metadata` label writes object values with their keys sorted.** The
label is TS's `JSON.stringify` of the entries: each entry's properties in TS's pick
order (`pickFeatureProperties` for a Feature, `pickConfigProperties` for the
Expand All @@ -81,6 +102,18 @@ touches a compared surface, reflected in the parity matrix.
given in another order (a base image's label written by another tool) is likewise
rewritten in pick order. The parsed metadata is the same; only the label's bytes differ
(`internal/imagemeta/testdata/ts-oracle/imagemeta.json` records both cases).
- **Command results write nested objects with their keys sorted.** TS writes each result
with `JSON.stringify`, so properties come in insertion order. Go matches that order for
the results of `up`, `build`, `set-up`, `run-user-commands` and `read-configuration`,
for `featuresConfiguration`, for the top level of `configuration` (the document's
order, with `customizations` moved from deprecated properties and then `configFilePath`
last) and for the top level of `mergedConfiguration` (the configuration without
`mergeConfiguration`'s `replaceProperties`, then the properties it sets). Objects nested
deeper are written with sorted keys, because a decoded Go map keeps no order: a
configuration's `containerEnv` or `customizations`, merged objects such as `remoteEnv`
or `portsAttributes`, and each Feature in `featuresConfiguration.featureSets`, which
TS writes in its `devcontainer-feature.json` order. The parsed values are the same; the
parity matrix's `key_order` cases hold the orders that match.

## Firm decisions & scope

Expand Down Expand Up @@ -135,6 +168,13 @@ oversight.
config, metadata and merge; `TestHookErrorDescribesTheCommandAndItsOrigin` pins the
current choice.

- **A lone surrogate in a string is written as U+FFFD.** Command results are written as
`JSON.stringify` writes them: `&`, `<`, `>`, U+2028 and U+2029 as they are, not
escaped as `encoding/json` would (`read-configuration.json-stringify-escapes-success`).
A configuration string holding a lone UTF-16 surrogate escape (`"\ud800"`) is the
exception: a Go string cannot carry one, so the JSON decoder replaces it with U+FFFD
when the file is read, and the result holds that character where TS writes the
escape back.
- **Byte-for-byte tarball parity is unattainable** because of `mtime` differences; tarball
contents are compared by parsed structure, not raw bytes.
- **Cloud registry auth matrix is out of default CI scope.** The hermetic auth paths
Expand Down
6 changes: 6 additions & 0 deletions docs/go-only-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ digests, when committed) and the proxy environment (`HTTP(S)_PROXY`, `NO_PROXY`,
**hermetic** — no network, no registry resolution — so identical inputs yield the same key
on any host.

A lockfile that exists but cannot be read or parsed fails the command (exit code 1, error
result on stdout) instead of being left out of the key, as the upstream CLI fails every
command that reads it. An empty lockfile pins nothing and keys like no lockfile. Likewise, a
Dockerfile-based configuration whose Dockerfile cannot be read fails the command with
`compute cache key: read Dockerfile: …`, the file `build` would fail on.

The flag is **additive**: without it, the output is byte-for-byte identical to the upstream
CLI; with it, a `cacheKey` field is added.

Expand Down
27 changes: 27 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@ runs the official CLI is unaffected:
matching container.
- Features are staged under `<tmp>/devcontainercli-<user>/container-features/`,
one folder per build, and that folder is kept as the official CLI keeps it.
- `build` on a Docker Compose configuration without `--image-name` reports
`"imageName"` as the image's name, a string, as the official CLI does. Earlier
releases wrapped it in an array. With `--image-name`, and for image and
Dockerfile configurations, it stays an array.
- The text log on stderr follows the official CLI's two formats. With stdout
redirected or piped, each line starts with its UTC time
(`[2026-01-02T03:04:05.678Z] text`) and Stop lines appear at `--log-level debug`
and `trace`. With stdout on a terminal, lines keep the elapsed `[N ms]` prefix,
are colored and end in `\r\n`, and Stop lines appear only at `trace`. Earlier
releases always wrote `[N ms] text` without color. A script that parses the log
should read the timestamped form, or use `--log-format json`.
- `build` and `up` write the Feature lockfile by default whenever the configuration
uses Features, as the official CLI does: `devcontainer-lock.json` next to
`devcontainer.json` (`.devcontainer-lock.json` next to `.devcontainer.json`) is
created when missing and rewritten when the resolved Features change. Earlier
releases wrote it only with `--experimental-lockfile` or `--frozen-lockfile`, so
the file now appears in projects that never had one. Commit it to pin the
Features' versions and digests, or pass `--no-lockfile` to neither read nor write
it. `--experimental-lockfile` is accepted but does nothing more and prints
`Warning: --experimental-lockfile is deprecated. Lockfiles are now enabled by
default.` on stderr; `--experimental-frozen-lockfile` prints a warning to use
`--frozen-lockfile` instead.
- `upgrade` always rewrites the lockfile in its canonical form, even when the
existing one differs only in formatting.
- Only a registry whose host is `localhost` is reached over plain HTTP. Earlier
releases also used plain HTTP for `127.0.0.1`, which is now addressed over HTTPS
like any other host. Refer to a local plain-HTTP registry as `localhost:<port>`.

## Rollback

Expand Down
Loading
Loading