Repository navigation
Conversation
An unknown ${templateOption:NAME} now becomes the empty string, only a
word-character NAME (with JS whitespace around it) is matched, and an
option default is filled only for a "string" or "boolean" option whose
default is truthy, as TS replaceTemplatedValues and fetchTemplate do.
An Object.prototype name is the one documented divergence.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A falsy "extensions", a "devPort" that customizations.vscode already sets, or a "customizations" that is no object failed the typed decode (exit 1, "cannot unmarshal"). TS readDevContainerConfigFile validates no property type, so the configuration now loads and reports them as written. Co-Authored-By: Claude Opus 5.5 <[email protected]>
TS mergeForwardPorts tests /localhost:\d+/ anywhere in a port string and takes parseInt of its text from the tenth character on, so "localhost:3000abc" is 3000, "localhost:0x10" is 16 and "mylocalhost:1" is null. Numbers keep their fraction when they are de-duplicated, and booleans, null and objects are kept as they are instead of being turned into Go-formatted strings. Co-Authored-By: Claude Opus 5.5 <[email protected]>
TS generateLockfile sorts the features by localeCompare of their ids and keeps each dependsOn in the order the Feature's metadata lists it; Go sorted both by bytes, so "ghcr.io/a/b/Node" came before "ghcr.io/a/b/alpha" and a lockfile TS wrote could fail --frozen-lockfile here. The ids are now compared with the en-US collator Node uses and dependsOn keeps its metadata order. Co-Authored-By: Claude Opus 5.5 <[email protected]>
TS doSetUp reads --config with no workspace and before it looks for
the container. ${localWorkspaceFolder} and its basename now stay as
written, as does ${containerWorkspaceFolder} unless the config sets
workspaceFolder (Go resolved them to "." and "/workspaces/."). A
missing config is "Dev container config (...) not found." and any
other load error is reported instead of being ignored. configuration
and mergedConfiguration also keep the properties the typed config has
no field for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TS reads devcontainer.json with jsonc-parser's error recovery, which
keeps what it could read and reports nothing: {"image": , ...} loses
"image" silently. This CLI keeps failing with the parse error; the
divergence is now documented and held by a test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merged forwardPorts come from decoded JSON (float64 or string) or PortOrString values, never Go integers, so the int and int64 cases were unreachable. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cover parseInt's sign, white space, hexadecimal and -0 handling in the forwardPorts merge, DependsOnKeys with a repeated or absent key, and the dependsOn order a manifest annotation carries. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference picks its text log handler on process.stdout.isTTY (spec-node/devContainers.ts createLogFrom). Piped, it writes the plain log: `[<ISO time>] text` ending in os.EOL, raw output stamped and stripped of escape sequences, and Stop lines only at debug and trace. On a terminal, it writes colored `[N ms]` lines ending in \r\n, numbers highlighted, and Stop lines only at trace (spec-utils/log.ts). This CLI always wrote uncolored `[N ms] text\n` and showed a Stop line whenever its level passed, matching neither. The parity normalizer scrubbed both timestamp shapes to the same token, which hid it; it now scrubs only the ISO shape. --log-file and --terminal-log-file still receive the log without color or \r, as documented. The goldens in internal/log/testdata/ts-oracle record the reference handlers' output for each event. Scripts that parsed `[N ms]` from piped stderr must read the ISO timestamp instead (docs/migration.md). Co-Authored-By: Claude Opus 5.5 <[email protected]>
yargs .implies() fails when a flag is given without the flag it implies, whatever their values (validation.js keyExists tests hasOwnProperty). The reference declares --terminal-columns and --terminal-rows to imply each other on up, set-up, run-user-commands, read-configuration, outdated and exec (devContainersSpecCLI.ts). This CLI tested the values (> 0), so `--terminal-rows 0` alone passed where the reference exits 1 with "Implications failed", and outdated did not check the pair at all. The flags now record whether they were given, parsing their value as before. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without --image-name, the reference's doBuild sets imageNameResult = originalImageName for a Docker Compose configuration, so the result reads "imageName":"<name>", a string; with --image-name it is the array of the given names (devContainersSpecCLI.ts). This CLI always wrote an array. A consumer that read the compose case as an array must read a string now, as it already had to with the reference (docs/migration.md). Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference normalizes --allow-cross-origin-auth-host authorities with
new URL(`https://${authority}`) and compares registry and realm hosts by
their WHATWG `host`. net/url kept an explicit :443, refused forms Node
accepts (a trailing "/", "\", dot segments, a bare "?" or "#", empty
userinfo, tabs) and did no IDNA, IPv4 or IPv6 normalization, so a
mapping TS trusts could fail to parse or never match the realm.
Port the WHATWG host rules for http(s) URLs and use them both to
normalize the configured entries and to key the registry and realm
lookups, so default ports drop on both sides. The ts-oracle golden pins
parseCrossOriginAuthHosts and isAllowedTokenServiceRealm.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference picks the registry scheme with getRegistryScheme: plain HTTP when the WHATWG hostname is localhost, HTTPS otherwise. The Go client also sent 127.0.0.1 registries over plain HTTP, although Ref.Scheme, and with it the read-configuration featureRef.scheme, said https, and the auth policy's localhost-only HTTP realm rule assumed the reference's split. Derive PlainHTTP from Ref.Scheme. The hermetic registry fixtures now address httptest servers as localhost:<port>, as the parity matrix already does for the reference. BREAKING CHANGE: a plain-HTTP registry referred to as 127.0.0.1:<port> is now reached over HTTPS; refer to it as localhost:<port>. Co-Authored-By: Claude Opus 5.5 <[email protected]>
read-configuration --cache-key ignored every ReadLockfile error, so a malformed devcontainer-lock.json keyed as if no lockfile were committed and the key stopped tracking the pinned digests without a word. The reference's readLockfile rethrows everything but ENOENT, failing each command that reads the lockfile. Fail the key with the lockfile path and the parse or read error, and report it as read-configuration reports its other runtime failures: the error result on stdout, exit code 1. Without --cache-key the command still does not read the lockfile, as the reference does not. Co-Authored-By: Claude Opus 5.5 <[email protected]>
ExtractTarGz re-rooted every entry under the destination before its path-traversal guard ran, so the guard never fired: "../up.txt" and "a/../mid.txt" were written as up.txt and mid.txt, and Windows roots stayed in the name. Writes also followed a symlink already inside the destination, so an entry beneath it landed outside. Treat entry paths as node-tar, which extracts the reference's blobs, does: skip an entry with a ".." segment (either separator) and strip '/', '\' and drive-letter roots. Write through an os.Root on the destination, so a path that would escape it through a symlink fails the extraction, and report directory and chmod errors instead of dropping them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Docker Compose configuration without workspaceFolder now works in `/`,
as the reference does (getWorkspaceConfiguration returns
getRemoteWorkspaceFolder, `config.workspaceFolder || '/'`). Go derived
/workspaces/<basename>, a folder the compose service never mounts, for
read-configuration's workspace, ${containerWorkspaceFolder}, up's
remoteWorkspaceFolder, the lifecycle commands' cwd and exec's cwd.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--frozen-lockfile without a lockfile now fails with "Lockfile does not exist." instead of passing unchecked, and both frozen failures report the reference's envelope: the lockfile error as the message and the command's "An error occurred building/setting up the container." as the description (lockfile.ts writeLockfile, wrapped as the originalError of the command's ContainerError). Go wrote "lockfile: lockfile does not match" into both fields. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The results of up, build, set-up, run-user-commands and read-configuration now list their properties as the reference's JSON.stringify does, in the insertion order of the object literals that build them: outcome first, read-configuration's configuration, workspace, featuresConfiguration, mergedConfiguration, then ociAuthDiagnostics. A configuration keeps its document's top-level order with configFilePath last, and a merged configuration the order mergeConfiguration produces. Go wrote every object with sorted keys, which the parity matrix could not see because it sorts keys; its new key_order field compares the order against the reference. Objects nested deeper still have sorted keys, a decoded Go map keeping no order; docs/DIVERGENCES.md records it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference's readLockfile rethrows every failure but ENOENT: a file
that cannot be read fails with the Node.js fs error ("EISDIR: illegal
operation on a directory, read", "EACCES: permission denied, open
'<path>'") and one that is not JSON with V8's JSON.parse SyntaxError
("Unexpected token 'o', "not json" is not valid JSON", "Expected ','
or '}' after property value in JSON at position 72 (line 2 column 1)").
ReadLockfile returned Go's own decoding error, and its callers dropped
it.
Return a *LockfileError carrying the class, message and fs properties
TS reports. The V8 parser's error reporting is ported over the UTF-16
text Buffer.toString decodes (WHATWG UTF-8); the empty-marker check
trims as String.prototype.trim does, and null, false, 0 and "" are no
lockfile, as TS reads the parsed value through `?.`. JSON shaped unlike
a lockfile stays an error (documented in DIVERGENCES.md). A ts-oracle
golden runs the reference's readLockfile over the same files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
outdated dropped every ReadLockfile error and reported versions as if no lockfile were committed. In the reference, loadVersionInfo calls readLockfile, which rethrows all but ENOENT, and outdated's catch logs the error's stack (output.write, debug level) and exits 1 with nothing on stdout. Do the same. A unit test and the contract parity case outdated.malformed-lockfile-error hold it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
features resolve-dependencies dropped every ReadLockfile error and
printed the graph as if no lockfile were committed. In the reference,
readLockfile rethrows all but ENOENT and runAsyncHandler reports the
uncaught error with console.error and exit code 1: the SyntaxError's
stack, or for a file system error Node's "[Error: ...] { errno, code,
syscall, path }".
Print the same on stderr (frames left out) and exit 1. A unit test and
the contract parity case
features.resolve-dependencies-malformed-lockfile-error hold it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
extendImageWithFeatures dropped every ReadLockfile error, so up and build went on unpinned when devcontainer-lock.json was malformed or unreadable. The reference's generateFeaturesConfig calls readLockfile, which rethrows all but ENOENT, and up and build wrap the error in a ContainerError: message the error's own, description "An error occurred setting up/building the container.", exit code 1. --no-lockfile still skips the file. Return the error, and let commandErrorOutput report an error carrying the reference's own message (errors.ReferenceError, which LockfileError implements) the way TS reports any error it wraps. Unit tests and the runtime parity cases build.malformed-lockfile-error and up.malformed-lockfile-error hold it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
composeFeatureSets dropped every ReadLockfile error, so a Compose configuration's Features were resolved unpinned when devcontainer-lock.json was malformed or unreadable. The reference rethrows readLockfile's error there too, and up and build report it as their error result: message the error's own, description "An error occurred setting up/building the container.", exit code 1. --no-lockfile still skips the file. Return the error. A unit test and the runtime parity cases build.compose-malformed-lockfile-error and up.compose-malformed-lockfile-error hold it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
read-configuration --cache-key folded the Dockerfile of a Dockerfile-based configuration into the key only when it could read it, so a missing or unreadable Dockerfile keyed as if the configuration had none and the key stopped tracking the image's recipe without a word. Fail the key with "read Dockerfile: <error>", the file build reads and would fail on, reported as read-configuration's other runtime failures are (error result on stdout, exit code 1). Without --cache-key the command still does not read the Dockerfile, as the reference does not. Documented in docs/go-only-features.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference writes every command result with JSON.stringify, which writes &, <, > and U+2028/U+2029 in strings as they are. encoding/json escaped them, so a result holding `a && b <x>` was not byte-compatible with the reference's. Results and error results now undo those escapes after encoding, which also covers strings nested json.Marshaler implementations write. The parity matrix gains a stdout_text assert that compares stdout as text, since stdout_normalized parses the JSON and cannot see escaping. Co-Authored-By: Claude Opus 5.5 <[email protected]>
vscode-uri's toJSON writes fsPath only once something read that getter, so the reference's up writes it in configFilePath when its code path read it: without --id-label (findContainerAndIdLabels), for a --config file read without --override-config (readDocument), and when Features are processed with the lockfile on (getLockfilePath). Go never wrote it in up. read-configuration and set-up always read it and already wrote it; both now build the URI through the same helper. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference makes a file URI of --config, and of --override-config only when there is no workspace; a configuration found in the workspace keeps the vscode-fileHost scheme whatever else is given. Go wrote the file scheme whenever --override-config was given (up and read-configuration) or --container-id was (read-configuration). The unit tests that held the old schemes now hold the reference's, which both CLIs were run to confirm. Without a workspace the override is the document up reads, so its URI also has fsPath. Co-Authored-By: Claude Opus 5.5 <[email protected]>
With --mount-git-worktree-common-dir and a worktree whose .git file names its gitdir by a relative path, the reference's workspace configuration carries additionalMountString, the common dir's bind mount, and read-configuration writes it after workspaceFolder and workspaceMount. Go mounted it in up but left it out of the result. The workspace object is now also written in the reference's key order. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reference CLI (v0.89.0) writes devcontainer-lock.json on every build and up that has Features unless --no-lockfile is given (containerFeaturesConfiguration.ts generateFeaturesConfig -> lockfile.ts writeLockfile, which creates a missing file). build and up, single-container and Compose, only wrote it with --experimental-lockfile or --frozen-lockfile. They now create or update it by default; --no-lockfile disables reading and writing it, and --frozen-lockfile is unchanged. --experimental-lockfile and --experimental-frozen-lockfile now print the reference's deprecation warnings on stderr. upgrade truncates the lockfile before writing it, as upgradeCommand.ts does, so a lockfile that differs only in formatting is rewritten in canonical form. BREAKING CHANGE: build and up create devcontainer-lock.json (or .devcontainer-lock.json next to .devcontainer.json) in projects that use Features. Commit it to pin the Features, or pass --no-lockfile to neither read nor write it. See docs/migration.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ence does build and up joined build.dockerfile onto the configuration's folder, so an absolute path became '<config dir>/<absolute path>' and the build failed to read it; --cache-key did the same. The reference resolves it against the configuration's folder, which keeps an absolute path (configuration.ts getDockerfilePath). Without build.context the context is the Dockerfile's folder (spec-node/utils.ts getDockerContextPath), where Go used the configuration's folder, and an absolute context was mangled the same way. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ror marker commandErrorOutput recognised the reference's own message two ways: a loop over the frozen-lockfile sentinels and the ReferenceError marker the lockfile read errors carry. The sentinels now carry the marker, so one check reports every such error as its message under "<description>.", with the same output as before. Co-Authored-By: Claude Opus 5.5 <[email protected]>
TestParityMatrixLocksTheFeatureContentImage read Features only from fixture configurations, so 13 cases whose setup_cmd writes a configuration with Features and builds it without BuildKit ran without the feature-content-image lock and could take each other's Features through dev_container_feature_content_temp. The check now also reads the configuration a setup_cmd writes, and those cases hold the lock. Co-Authored-By: Claude Opus 5.5 <[email protected]>
composeFeatureSets carried a mutate-lane marker for the runtime parity case up.compose-config-file-fspath-features-success, which kept every line of it out of mutation testing although unit tests hold all of them but the lockfileRead flag. TestComposeFeatureSets now checks the flag (set only when Features are processed with the lockfile on), and the marker is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
go-tools 5fc0868 runs each mutate test process in a private copy of the module with a temporary directory of its own (spin-stack/go-tools#69). Here, mutants of internal/cli wrote Dockerfile-with-features and a timestamped cache folder into shared places, so `mutate -j 4` reported survivors that a serial run refused, a different set each run. The pin also brings testquality's check for tests that sleep a second or more; it finds none here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
givenInt parsed --terminal-columns/--terminal-rows as int64 and converted to int, so on a 32-bit platform 2147483648 became -2147483648 and was taken as given (code scanning: incorrect conversion between integer types). It now parses at int's size and fails on such a value. The new case in TestGivenIntParsesLikeIntVar fails before the fix under GOARCH=386; on 64-bit platforms nothing changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Node.js 24's URL parser only lower-cases a host that is all ASCII; it no longer checks its xn-- labels as Punycode, so xn--a, xn-- and xn--.example are hosts where Node.js 22 rejected them. The ts-oracle golden recorded Node.js 22's answers on one machine while CI ran Node.js 24, so the oracle gate failed there. domainToASCII now lower-cases any ASCII domain, and five more golden cases pin the rule. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The goldens recorded what the host's Node.js answered, so the same reference functions gave other answers on another machine (URL parsing, IDNA and Node.js error messages change between releases). The oracle now bundles and runs in node:24-bookworm-slim, pinned by digest in versions.yaml and handed in by the Taskfile, with no network: Node.js 24 is what CI installs and runs the reference on, and the newest release the reference's lockfile builds on (its nan 2.25.0 does not compile node-pty against Node.js 26). A bump that changes an answer fails the oracle gate until the goldens are refreshed. The readLockfile oracle writes its temporary folder as <dir>, so a message naming the file stays the same from run to run. TestTSOracleGoldensMatchTheReference skips without Docker or without TS_ORACLE_IMAGE, which task parity:contract sets. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Summary
This round closes the parity follow-ups left open by #40 and #41, plus what turned up while checking them. Every behavior was first checked against the TS reference (v0.89.0) source and by running both CLIs. Each fix comes with a regression test that fails before the fix and passes after it: a TS-recorded ts-oracle golden, a parity matrix case or a unit test.
Breaking changes (each commit has a
BREAKING CHANGE:footer and a note in docs/migration.md)fix(cli)!:build and up writedevcontainer-lock.jsonby default, as the reference does (containerFeaturesConfiguration.ts:517). The file is created when missing and updated when stale.--no-lockfileturns off both reading and writing, and--experimental-lockfilenow only prints the reference's deprecation warning.fix(oci)!:onlylocalhostregistries are reached over plain HTTP, as in TSgetRegistryScheme.127.0.0.1now uses HTTPS like any other host; refer to a local plain-HTTP registry aslocalhost:<port>.Output that changes for scripts (migration notes added)
\r\n, and Stop lines show only at trace. Earlier releases always wrote[N ms] text.&,<,>, U+2028 and U+2029 are no longer escaped (as with JSON.stringify).buildwithout--image-namereportsimageNameas a string, as TS does.Fixes
${templateOption:x}becomes '' (TS regex port).--frozen-lockfile.set-upreads--configwithout a workspace and fails on a missing config before looking for the container.workspaceFolderuses/.--terminal-columns/--terminal-rowsare checked by presence, as yargs does, on all six commands.--frozen-lockfileerrors use TS's text. A missing lockfile now fails; it used to succeed silently.configFilePathgetsfsPathexactly where TS reads it.filescheme is used only for--config.additionalMountStringfor git worktrees.:443and the realm host now compare as TS compares them...entries are skipped and roots stripped.os.Root, which closes a symlink escape the old extractor had.--cache-keyfails on an unreadable lockfile or Dockerfile instead of silently keying without it (docs/go-only-features.md).build.dockerfileand its context resolve as in TS. They used to be joined onto the config folder.Deliberate divergences (docs/DIVERGENCES.md)
Object.prototypeproperty becomes ''. TS writes the function's source text.Parity harness
stdout_textassert compares raw bytes, which shows escaping.key_orderfield compares key order against TS.feature-content-imagelock: 13 more cases whosesetup_cmdwrites a configuration with Features now hold it, and the contract test sees them.composeFeatureSetslost amutate-lanemarker, because unit tests now hold it.-j 4reported here.Added after review
node:24-bookworm-slim, pinned by digest inversions.yamland handed in by the Taskfile, with no network.[email protected]does not compilenode-ptyagainst Node.js 26's V8.xn--labels are no longer checked as Punycode. Five golden cases pin this.intcannot hold (code scanning alert 16). On 32-bit platforms such a value was truncated and taken as given; the new test fails before the fix underGOARCH=386.Validation
go vet ./...on every commitgo test -race ./...(Docker unreachable)task lint(with the go-tools bump too)task vulngo tool mutate -base origin/main -j 1task spec:compliancetask parity:contracttask parity:semantictask parity:runtime(Docker, full)task test:e2eLeft for a next round
--override-configwithout a base config (configFilePathand the base for relative paths).--container-idand--override-configbut no workspace.workspaceMountis set.&<>..devcontainer/.upgrade.🤖 Generated with Claude Code