Skip to content

feat(features): install features reachable only through dependsOn - #151

Merged
bilby91 merged 7 commits into
mainfrom
feat/feature-depends-on
Nov 5, 2026
Merged

bilby91 merged 7 commits into
mainfrom
feat/feature-depends-on

Conversation

@bilby91

@bilby91 bilby91 commented Nov 4, 2026

Copy link
Copy Markdown
Member

Closes #147. Stacked on #146 (lockfile): pulled-in features are locked too. The base is feat/feature-lockfile, so this diff shows only the dependsOn change. Retarget it to main once #146 merges.

Problem

dependsOn only ordered features that devcontainer.json already declared. An undeclared dependency was skipped silently, so a feature that relies on one was installed without it. The spec makes dependsOn a hard dependency, installed recursively with the options it names. The reference CLI does this in computeDependsOnInstallationOrder.

Design decisions

Recorded in design/features.md §5.1 and §8.2.

  1. No new structures. layerFeatures' fetch loop appends each fetched feature's dependsOn entries and fetches them in turn. Lockfile pinning, option validation and defaults apply exactly as for declared features. Pulled-in features are locked under their dependsOn key, as the CLI does.
  2. Duplicates follow the spec's feature equality: same digest and same options.
    • A repeated key with the same options is skipped before fetching, which also ends cycles.
    • The same content under another reference (:1 vs :1.0) is dropped after fetching.
    • The same feature with different options is installed once per option set. The ordering graph maps an id to all of its entries, and each dependsOn edge points at the entry with the requested options.
  3. Options are compared after defaults are applied. This follows the spec's "options executed against the Feature"; the CLI compares options as written. No file format depends on this.
  4. Behaviour change: two declared references to the same digest with the same options now install once instead of twice. This matches the spec and the CLI.
  5. Base-image matches: a dependency the base image's metadata label already satisfies is skipped without fetching. markAlreadyInstalled's match is factored into bakedFeature for this.
  6. Compose inputs hash: it now also covers a local dependency's content, which the declaring feature's content doesn't capture. Projects without a local dependency hash exactly as before.
  7. OCI references are lowercased before parsing, as the CLI's getRef does. Published dependsOn keys with uppercase letters (e.g. ghcr.io/codspace/dependson/A) failed to parse before.

Not in this PR

  • Uncompressed tarballs: the HTTPS fetcher doesn't accept an uncompressed tar named .tgz, which one of the CLI's fixtures uses. The integration test leaves that feature out.
  • overrideFeatureInstallOrder is ignored for fetched features, because layerFeatures re-sorts them with Order(…, nil). A throwaway test confirmed it. This is pre-existing and also affects declared features, so the README's ✅ for it is inaccurate. It's separate work.

Testing

  • Unit tests cover:

    • a chain and a diamond;
    • dedupe against a declared feature (4 cases);
    • a cycle through a pulled-in feature;
    • lockfile entries and frozen mode;
    • a dependency already in the base image;
    • a local→local dependency;
    • the compose inputs hash.

    Each fails with the change disabled.

  • Integration: TestDependsOn_MatchesCLILockfile builds the CLI's own lockfile-dependson fixture in frozen mode against real ghcr.io, checked against the CLI's expected lockfile.

  • go vet, lint and go test -race ./... pass. The integration suite passes, excluding the Docker-in-Docker tests that were broken on main at the time (since fixed in test(integration): fix docker-in-docker tests on ubuntu-26.04 runners #149).

🤖 Generated with Claude Code

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — b4a57e2 · ADVISORY

The new recursive dependsOn path can lose declared dependency aliases during content deduplication, and bypasses lockfile artifact validation or transitive expansion when the base image label marks a dependency baked. These leave lockfile pins incomplete and can omit required dependency features on upgrade-era base images.

Finding Anchor
🟡 MEDIUM Dedupe drops the dependsOn alias before it can be locked
core/version-compatibility
up.go:1415
🟡 MEDIUM Baked dependencies can bypass lockfile digest validation
core/version-compatibility
up.go:473
🟡 MEDIUM A baked dependency prevents discovery of its transitive dependencies
core/correctness
up.go:474
What I reviewed

Coverage: Reviewed the exact head b4a57e2a28eed794fc64f8bab6e484af8c5859d5 (one commit; 16 changed files relative to lockfile base 45c0015c262fb870cef8107c4a3e7c31d581b7a8). Worked correctness, interface-contract, version-compatibility, and repository mechanisms R5 (reference-CLI divergence) and R8 (tests), plus compose reattach hash behavior. Checked feature resolution, option merging, DAG ordering, build-context use, lockfile pin/commit flow, and baked-label handling. The baked transitive omission is also explicitly described in the new design text as intentional, but that assumption fails for base images made before this behavior shipped. No .dap/code-review.yml or AGENTS.md exists at directives base; applied the repository's .dap/review/engineering.md at that SHA and the core review dimensions. Did not run tests or execute the code. access-control, untrusted-input, and secret-exposure mechanisms were not implicated by the diff; state-integrity and redelivery are not applicable.


ADVISORY — findings worth reading, none of them blocking. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

Comment thread up.go Resolved
Comment thread up.go Resolved
Comment thread up.go Resolved
@bilby91
bilby91 force-pushed the feat/feature-depends-on branch from b4a57e2 to 419fdce Compare November 4, 2026 17:39
@bilby91
bilby91 changed the base branch from feat/feature-lockfile to main November 4, 2026 17:39

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 419fdce · ADVISORY

No newly changed behavior was found: the reviewed head has the same tree as the prior head that produced the three already-open findings, so those findings remain open and were not resubmitted.

Finding Anchor
🟡 MEDIUM Dedupe drops the dependsOn alias before it can be locked
core/version-compatibility
up.go:1415
🟡 MEDIUM Baked dependencies can bypass lockfile digest validation
core/version-compatibility
up.go:473
🟡 MEDIUM A baked dependency prevents discovery of its transitive dependencies
core/correctness
up.go:474
What I reviewed

Coverage: Verified the exact requested SHA (419fdcefb53254a7054c6162d21004c754671b7c), current PR scope (one commit, 16 files), and the governing repository guidance at base SHA 864709a381a47798a949e16138d066cbd9f4916f; .dap/code-review.yml is absent, and .dap/review/engineering.md supplies the repository-specific mechanisms. The relevant feature-resolution, dependency-graph, lockfile, base-image-label, and compose reattach paths and their tests were read. Correctness and version-compatibility were considered; the prior three findings remain open on unchanged code. No visual evidence applies, and no tests or system code were executed.


ADVISORY — findings worth reading, none of them blocking. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

@bilby91
bilby91 changed the base branch from main to fix/lockfile-review-findings November 4, 2026 20:07
@bilby91
bilby91 force-pushed the feat/feature-depends-on branch from 419fdce to 680f353 Compare November 4, 2026 20:07
* fix(docker): pre-pull the Dockerfile syntax frontend image

BuildImage builds through ImageBuild with no BuildKit session, and
BuildKit before v0.31 (Docker Engine before 29.6) refuses registry
access without one, even for anonymous public pulls: a bearer
challenge with no session fails with "no active sessions". FROM
images were already pre-pulled through the classic pull API for this
reason; a `# syntax=` frontend was not, so any user Dockerfile naming
one failed before parsing (#148).

Pre-pull the frontend too: the BUILDKIT_SYNTAX build arg, else the
syntax parser directive, read as BuildKit's parser.DetectSyntax reads
it (leading directive lines only, # or // comments, after an optional
BOM and shebang; the image is the value's first word).

Verified against a Docker 29.5.0 daemon (containerd image store):
the new integration test fails there with the reported error without
this change and passes with it. Docker 29.6+ resolves anonymously
without a session, so it passes there either way.

Fixes #148.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(docker): pre-pull a frontend named by a JSON syntax document

BuildKit's parser.DetectSyntax falls back, after the # and // directive
forms, to a JSON object with a string "syntax" member, the input of
custom frontends that take JSON, and uses the whole value as the
frontend image. syntaxFrontend left that form out, so its frontend was
not pre-pulled and hit the session-less resolution failure on engines
before 29.6 (DAP review finding 3891 on #150).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bilby91 <2201079+bilby91@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 680f353 · ADVISORY

The implementation fixes the three carried findings: duplicate aliases are retained for lockfile commits, baked artifact digests are checked against pins, and dependencies of baked features are traversed. One correctness issue remains: a baked dependency can be treated as satisfying a request even when the request's options differ from those used to build the base image, so those options are silently not applied.

Finding Anchor
🟡 MEDIUM Baked dependsOn features can be reused with the wrong options
core/correctness
up.go:475
What I reviewed

Coverage: Reviewed the exact head 680f353bb8f092e44cce80b29ba63e83739a0201 (2 commits, 21 changed files relative to the PR base); its merge base with main is 25711e1bb5b50e4438cf3e05d3ef993f6a7a4e1a. Checked out the exact SHA and verified HEAD. Read the PR rationale, the governing .dap/review/engineering.md at directives base 25711e1bb5b50e4438cf3e05d3ef993f6a7a4e1a, and its README, CONTRIBUTING, and feature design guidance; .dap/code-review.yml is absent. Reviewed feature dependency resolution, graph ordering, baked-image matching and metadata labels, lockfile acceptance/commit behavior, and consumers that skip baked feature installation. Verified and resolved all three carried findings. Relevant mechanisms: correctness, version-compatibility, interface-contract, test-coverage, and operability. No backend-specific path change warranted a path-parity finding; conditional state-integrity and redelivery do not apply. I did not reach unrelated feature/cache and runtime paths.


ADVISORY — findings worth reading, none of them blocking. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

Comment thread up.go Resolved
bilby91 added a commit that referenced this pull request Nov 4, 2026
The DAP review of #151 (finding 3903) notes a baked dependency is reused
whatever options it was requested with. The pre-baked match has never
compared options, for declared features either; record the limitation
in §5.1 and track the fix to the match itself in #154.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bilby91 and others added 4 commits November 4, 2026 17:32
…igs (#153)

* fix(features): enforce lockfile pins on baked features and empty configs

Two findings from the review of #146:

- A feature the base image's label satisfied was skipped and its
  existing lock entry copied, so frozen mode accepted a base image
  built from another artifact, or one the lockfile had no entry for.
  The label already records each feature's resolvedRef, but
  ParseMetadataLabel dropped it (and so did carrying base entries into
  a rebuilt label). Keep it on FeatureMetadata, read the lockfile
  before matching the label, and accept a baked feature only when its
  artifact is the pinned one or, with no pin, a recorded one, which is
  then locked. Anything else is fetched at its pin and installed.
  LockfileDisabled keeps the label-only match.
- layerFeatures returned before the lockfile when no features were
  declared, so removing the last feature left its entry behind and
  frozen mode accepted it. Reconcile an existing lockfile then too:
  default and upgrade empty it, frozen reports the stale entries.
  Without a lockfile nothing is created, as with the reference CLI.

The pre-baked hot path is unchanged for images this library built,
whose labels have recorded resolvedRef since v0.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(features): re-resolve baked features in lockfile upgrade mode

acceptsBaked found no pin in LockfileUpgrade (pin ignores the lockfile
there) and so accepted any recorded baked artifact, which commit then
locked: an upgrade could replace a newer pin with the older artifact
the base image carries (DAP review finding 3902 on #153). Upgrade mode
now accepts no baked feature, so each reference is resolved afresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bilby91 <2201079+bilby91@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dependsOn only ordered features that devcontainer.json declared anyway:
an undeclared dependency was silently skipped, so a feature relying on
one installed without it (#147). The spec makes dependsOn a hard
dependency, installed recursively with the options it names.

- layerFeatures' fetch loop appends each fetched feature's dependsOn
  requirements and fetches them in turn: lockfile pinning, option
  validation and defaults apply as for declared features, and they are
  locked under their dependsOn key as the reference CLI does.
- Spec equality decides duplicates: same content (digest) and same
  options once defaults apply. A repeat of a key with the same options
  is skipped before fetching, which also ends cycles; the same content
  under another reference is dropped after. One feature with different
  options is installed once per set, and topoSort maps an id to all
  its entries, pointing dependsOn edges at the one with the requested
  options.
- A dependency the base image's label satisfies is not fetched or
  installed again: bakedFeature, factored from markAlreadyInstalled,
  includes the lockfile's check of the artifact the label records.
  Its own dependencies are still followed, as are a declared baked
  feature's, since an image built before this change lacks them.
  Labels now record each feature's dependsOn ({} when none); for a
  label written before that, the baked feature's metadata is fetched
  at its recorded artifact to find them.
- A dependency dropped as a duplicate under another reference is still
  locked under its own key.
- The compose inputs hash also covers a local dependency's content,
  which the declaring feature's content does not; projects without one
  hash as before.
- OCI references are lowercased before parsing, as the CLI's getRef
  does: published dependsOn keys such as ghcr.io/codspace/dependson/A
  failed to parse.

The integration test builds the CLI's own lockfile-dependson fixture in
frozen mode against its expected lockfile.

Closes #147.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DAP review of #151 (finding 3903) notes a baked dependency is reused
whatever options it was requested with. The pre-baked match has never
compared options, for declared features either; record the limitation
in §5.1 and track the fix to the match itself in #154.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DAP review of #153 (finding 3904) noted a baked feature accepted
without a pin was locked from its label entry, which then had no
dependsOn. This change fills it in before commit; pin it with a test
for a label that records dependsOn and for an older one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bilby91
bilby91 changed the base branch from fix/lockfile-review-findings to main November 4, 2026 20:33
@bilby91
bilby91 force-pushed the feat/feature-depends-on branch from a5d2c99 to 9a4e5c2 Compare November 4, 2026 20:33

bilby91 commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

@dap-code-review-by-crunchloop review

@dap-code-review-by-crunchloop

Copy link
Copy Markdown

On it. A review of the current head is on its way. This branch's automatic review rounds are back, so I'll review new pushes again.

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 9a4e5c2 · BLOCKING

The change introduces a baked-dependency option mismatch and a compose reattach hash gap for local dependencies of remote features; both can produce or retain behavior inconsistent with the newly resolved dependencies. I also found an untrusted-input path where fetched feature metadata can cause local directories outside the project to be read into the build context.

Finding Anchor
🔴 CRITICAL Remote feature metadata can traverse into local files during feature installation
core/untrusted-input
up.go:472
🟡 MEDIUM A baked dependency is reused despite different requested options
core/correctness
up.go:475
🟡 MEDIUM Compose reattach misses local dependencies of remote features
core/correctness
up_compose_reattach.go:143
🟡 MEDIUM Remote dependency metadata can read and package local feature directories outside the project
core/untrusted-input
up.go:472
What I reviewed

Coverage: Reviewed the exact head 9a4e5c2b3176d7a990ddaeba61a7f22cb6cc6577: 3 commits and 21 changed files relative to main at ceaba2070fa6783221a83bdebf3a142470b3e066. .dap/code-review.yml is absent at directives base 67a01cce99a1b727d28a081efbe87caf375d9e52; used the repository mechanism catalogue at .dap/review/engineering.md and read the related design and contribution guidance. Worked correctness, untrusted-input, version-compatibility, and test-coverage while tracing dependency expansion, baked metadata and lock checks, duplicate aliases, the feature build context, and compose reattach hashing. Verified the old baked lock-pin mismatch and transitive-dependency discovery findings are fixed in this head; reviewed the option-mismatch rebuttal and found the new dependsOn path exposes it. Did not run the system or tests; examined tests as source. Did not independently review unrelated low-risk README/changelog and mechanical API-renaming hunks in depth. No visual evidence applies.


BLOCKING — at least one finding is at or above this repository's blocking severity. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

Comment thread up.go Resolved
Comment thread up.go Resolved
Comment thread up_compose_reattach.go Resolved
Comment thread up.go Resolved
bilby91 and others added 2 commits November 4, 2026 21:20
The DAP review of #151 (findings 3912, 3919, 3917) showed a fetched
feature's dependsOn could name a local path, absolute or escaping with
.., which layerFeatures resolved against devcontainer.json's directory,
copied into the build context and ran: published metadata choosing a
host directory to execute. The compose inputs hash also missed such a
dependency, since it follows local dependencies from declared local
features only.

A local dependsOn key is now followed only from a local feature, and
only as a relative path that resolves, symlinks included, inside the
directory of devcontainer.json, as the reference CLI requires of local
features; anything else fails Up and Build. With remote features unable
to reach local dependencies, the compose hash's traversal covers every
one. Declared local features keep their existing resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DAP review of #151 (finding 3913) showed a dependency the base
image's label listed was reused whatever options its dependsOn entry
asked for: the pre-baked match compares id and version only, and those
options are the feature author's, which no user config could route
through that match before. Dependencies are now always fetched and
installed; a declared baked feature still is not reinstalled, and its
dependencies are installed. Until #154 adds option matching, a
pre-baked image whose features have dependencies reinstalls them on
every build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 4811e0a · CLEAN

No new defect is established in the final commit; the prior path-confinement, baked-dependency-option and compose-hash claims no longer hold. I dismissed or resolved the carried findings after checking the relevant code at 4811e0a; the two open arguments without an explicit reply were adjudicated against the code as well.

What I reviewed

Coverage: Reviewed the 5-commit, 21-file change against exact head 4811e0aecb4e81f0166eb4316efd1c84480500c1 (same SHA as HEAD), focusing on correctness, untrusted-input, version-compatibility, compose reattach hashing and dependency graph/lock behavior. Confirmed local dependency confinement and transitive local-content hashing, baked dependencies now fetched rather than accepted from base-image labels, and dedupe aliases retained for lockfile commit. Read applicable repository guidance at directives base 67a01cce99a1b727d28a081efbe87caf375d9e52; .dap/code-review.yml is absent, and the applicable standard is .dap/review/engineering.md plus CONTRIBUTING.md and design/features.md. The eight supplied GitHub comment IDs returned 404 individually; the available pull-request review comments and their replies were read via the PR comments endpoint and used to check the carried claims. No system code or tests were executed. Other behavior outside these paths was not exhaustively reviewed.


CLEAN — this round found nothing. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No open findings on 4811e0a.

The DAP Code Review check run remains the verdict of record. This approval is dismissed if a finding reopens or a later head is not clean.

@bilby91
bilby91 merged commit db80dd8 into main Nov 5, 2026
10 checks passed
@bilby91
bilby91 deleted the feat/feature-depends-on branch November 5, 2026 13:52
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.

Features: install features reachable only through dependsOn

1 participant