Skip to content

fix(features): write Feature ENV lines and options in declared order - #68

Merged
aledbf merged 3 commits into
mainfrom
align-env-order
Nov 13, 2026
Merged

aledbf merged 3 commits into
mainfrom
align-env-order

Conversation

@aledbf

@aledbf aledbf commented Nov 13, 2026

Copy link
Copy Markdown
Member

Aligns key order with the official CLI. The ts-oracle goldens had recorded this drift as two "unresolved" divergences.

Problem. Go wrote these in sorted key order, where TS uses JS property order (integer-index keys ascending, then declaration order):

  • the ENV lines for a Feature's containerEnv;
  • the config's containerEnv;
  • the user's Feature options in devcontainer-features.env.

Besides the order, this changed the built image: an ENV line that uses a variable an earlier line sets expanded differently once sorting moved it first.

Image env
TS, and Go after this PR PATH=/usr/local/sdkman/bin:/usr/local/sbin:…, ZED=1, ALPHA=2
Go before PATH=/bin:/usr/local/sbin:… (SDKMAN_DIR not yet set), ALPHA=2, ZED=1

devcontainer-features.env: TS and now Go write VERSION, ZZ, AA; Go wrote VERSION, AA, ZZ.

Design. The member order is recorded where documents are parsed, following the pattern Feature.DependsOnOrder already uses:

  • devcontainer-feature.json;
  • devcontainer.json plus its override, merged as deepMergeConfig does;
  • --additional-features.

It is emitted through one helper, features.PropertyOrder, on top of the existing jsObject. jsonc.MemberNames gains a path argument.

Tests

  • ts-oracle: the two unresolved cases in imagemeta.json now match, and two new cases (dependent variable, integer keys first) are checked against TS.
  • New TS-generated golden ts-features-build/declaration-order/, made with the repo's gen.ts against v0.90.0. The existing goldens regenerate byte-identical.
  • TestFetchedFeaturesKeepTheDeclaredOrder: real local Features through to the Dockerfile.
  • Plumbing tests for the loader, --additional-features, PropertyOrder and MemberNames.
  • New runtime parity case build.features-env-declaration-order-success, which compares the built image env and devcontainer-features.env with TS. It fails with the pre-change binary.

Still sorted (follow-up): containerEnv inside the devcontainer.metadata label, the Feature value key order in read-configuration, installOrder options in features resolve-dependencies, and the merged containerEnv in the compose override.

Validation (local)

  • task lint, task spec:compliance, task testdata:ts-oracle:check: pass.
  • task test:race with TMPDIR outside /tmp: pass.
  • task parity:contract: only the 2 known /tmp/.git cases fail.
  • Runtime parity: the new case and the Features staging-layout cases match.
  • e2e: all 13 TestE2E pass in a full run with a longer timeout. The task test:e2e 5m budget ran out under load from parallel runs on the shared daemon.

🤖 Generated with Claude Code

aledbf and others added 2 commits November 12, 2026 23:03
The image's ENV lines for a Feature's containerEnv and the config's
containerEnv, and the user's options in devcontainer-features.env, were
written in sorted key order. The reference CLI writes them in JS
property order (integer-index keys ascending, then declaration order).
Besides the order itself, a Feature's ENV line that uses a variable an
earlier line sets, such as PATH=${SDKMAN_DIR}/bin:${PATH} after
SDKMAN_DIR, expanded to a different value when the sort put it first,
so the built image's environment differed.

The member order is now recorded where the documents are parsed
(devcontainer-feature.json, devcontainer.json and its override,
--additional-features) and carried to the generated Dockerfile through
features.PropertyOrder.

The two ts-oracle cases recorded as unresolved divergences now match
TS, a TS-generated ts-features-build golden pins the dependent ENV
lines, and a runtime parity case compares the built image's env and
devcontainer-features.env with the reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Comment thread internal/features/jsobject.go Fixed
Comment thread internal/features/jsobject.go Fixed
CodeQL's go/allocation-size-overflow flagged the sum of two lengths
used as a make capacity. The hint bought nothing measurable, so the
slice grows on append instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@aledbf
aledbf merged commit 9ef9782 into main Nov 13, 2026
13 checks passed
@aledbf
aledbf deleted the align-env-order branch November 13, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants