Skip to content

fix(feature): match devcontainer CLI lockfile dependsOn format - #793

Merged
skevetter merged 4 commits into
mainfrom
fix/lockfile-dependson-array
Jul 28, 2026
Merged

fix(feature): match devcontainer CLI lockfile dependsOn format#793
skevetter merged 4 commits into
mainfrom
fix/lockfile-dependson-array

Conversation

@skevetter

@skevetter skevetter commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

The devcontainer lockfile generator emitted the dependsOn field as a JSON object mapping feature IDs to empty option objects:

"dependsOn": { "ghcr.io/skevetter/features/picolayer:1": {} }

The reference devcontainers CLI (generateLockfile) emits it as an array of feature IDs in JSON declaration order (Object.keys(dependsOn)):

"dependsOn": ["ghcr.io/skevetter/features/picolayer:1"]

This change makes devsy's generated devcontainer-lock.json byte-identical to the CLI.

Changes

  • LockedFeature.DependsOn retyped from map[string]any to []string.
  • FeatureConfig gains a custom UnmarshalJSON that captures the declaration order of dependsOn keys (a Go map cannot preserve it), exposed via DependsOnKeys(). Falls back to sorted keys for programmatically-built configs so output is always deterministic.
  • recordLockEntry now writes cfg.DependsOnKeys() directly.
  • Tests cover declaration-order preservation, the sorted fallback, and array serialization.

The regenerated repo lockfile (array format) ships separately on the devcontainer-features branch (#792).

Summary by CodeRabbit

  • Bug Fixes

    • Preserved the declared order of feature dependencies.
    • Updated lockfiles to store dependsOn as an array, matching expected devcontainer format.
    • Added deterministic ordering when dependency declaration order is unavailable.
  • Tests

    • Added coverage for dependency ordering and lockfile serialization.

The lockfile generator emitted `dependsOn` as a JSON object mapping
feature IDs to empty option objects. The reference devcontainer CLI
emits it as an array of feature IDs in declaration order
(`Object.keys(dependsOn)`).

Retype LockedFeature.DependsOn to []string and preserve the JSON
declaration order of dependsOn keys via a custom FeatureConfig
unmarshaler, so generated lockfiles are byte-identical to the CLI.

Signed-off-by: Samuel K <skevetter@pm.me>
@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 52eb634
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a68249aa0691d0008c3deea

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@skevetter, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 753610d4-2b36-476e-a18d-8ac1dd09b51c

📥 Commits

Reviewing files that changed from the base of the PR and between 01c3a58 and 52eb634.

📒 Files selected for processing (3)
  • pkg/devcontainer/config/feature.go
  • pkg/devcontainer/config/feature_test.go
  • pkg/devcontainer/feature/lockfile_test.go
📝 Walkthrough

Walkthrough

Feature dependency keys are captured in JSON declaration order, with sorted fallback for programmatic configs. Lockfile entries now store dependencies as string arrays, and lockfile generation uses the ordered keys. Tests cover ordering, fallbacks, empty configs, and serialization.

Changes

Feature dependency lockfile representation

Layer / File(s) Summary
Dependency order capture and fallback
pkg/devcontainer/config/feature.go, pkg/devcontainer/config/feature_test.go
FeatureConfig preserves JSON declaration order for dependsOn keys and falls back to sorted keys when order is unavailable.
Ordered lockfile serialization
pkg/devcontainer/feature/extend.go, pkg/devcontainer/feature/lockfile.go, pkg/devcontainer/feature/lockfile_test.go
Lockfile entries use dependency string arrays, populated from DependsOnKeys() and verified during JSON serialization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant FeatureConfig
  participant recordLockEntry
  participant LockedFeature
  participant WriteLockfile
  FeatureConfig->>recordLockEntry: Provide DependsOnKeys()
  recordLockEntry->>LockedFeature: Assign dependency string array
  LockedFeature->>WriteLockfile: Serialize dependsOn array
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: aligning devcontainer lockfile dependsOn handling with the CLI format.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for images-devsy-sh canceled.

Name Link
🔨 Latest commit 52eb634
🔍 Latest deploy log https://app.netlify.com/projects/images-devsy-sh/deploys/6a68249a8e57df0008a34ad0

Signed-off-by: Samuel K <skevetter@pm.me>
@skevetter
skevetter marked this pull request as ready for review July 28, 2026 03:19

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/devcontainer/config/feature.go`:
- Around line 153-154: Update the depends-on ordering logic around
dependsOnOrder so it retains the captured declaration order only when every
captured key still exists in DependsOn, not merely when the lengths match;
otherwise return the existing sorted fallback. Add a regression test that
unmarshals configuration, replaces one dependency key without changing the map
size, and verifies the fallback order is used.

In `@pkg/devcontainer/feature/lockfile.go`:
- Around line 19-22: Update ReadLockfile to decode persisted feature entries
through a flexible JSON representation before constructing LockedFeature.
Normalize legacy object-shaped dependsOn values into the []string form, while
preserving existing array-shaped values, so newLockfileState receives the
canonical representation used for pinning and writing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 871bb23d-41c2-412c-9496-d8abcbac7456

📥 Commits

Reviewing files that changed from the base of the PR and between dd2001f and 01c3a58.

📒 Files selected for processing (5)
  • pkg/devcontainer/config/feature.go
  • pkg/devcontainer/config/feature_test.go
  • pkg/devcontainer/feature/extend.go
  • pkg/devcontainer/feature/lockfile.go
  • pkg/devcontainer/feature/lockfile_test.go

Comment thread pkg/devcontainer/config/feature.go Outdated
Comment on lines +19 to +22
Version string `json:"version,omitempty"`
Resolved string `json:"resolved,omitempty"`
Integrity string `json:"integrity,omitempty"`
DependsOn []string `json:"dependsOn,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate lockfile readers and JSON decoding paths involving Lockfile types.
ast-grep outline pkg/devcontainer/feature/lockfile.go --items all
ast-grep run --lang go --pattern 'json.Unmarshal($DATA, $TARGET)' pkg
rg -nP --type=go -C3 '\b(?:Lockfile|LockedFeature)\b|json\.NewDecoder\(' pkg

Repository: devsy-org/devsy

Length of output: 35212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lockfile.go =="
sed -n '1,260p' pkg/devcontainer/feature/lockfile.go

echo "== call sites for newLockfileState / ReadLockfile / lockfileState =="
rg -n -C 4 'newLockfileState\(|ReadLockfile\(|\.loaded|checkFrozenPrecondition|lockfileMode|needsWrite|lockfileNeedsWrite' pkg/devcontainer pkg -g '*.go' | head -n 200

echo "== inspect devcontainer feature/extend relevant =="
sed -n '480,545p' pkg/devcontainer/feature/extend.go

Repository: devsy-org/devsy

Length of output: 21450


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Go JSON Unmarshal legacy object as []string behavior =="
go version 2>/dev/null || true
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat > "$tmpdir/unmarshal_probe.go" <<'GO'
package main

import (
	"encoding/json"
	"fmt"
)

type LockedFeature struct {
	Version   string   `json:"version,omitempty"`
	Resolved  string   `json:"resolved,omitempty"`
	Integrity string   `json:"integrity,omitempty"`
	DependsOn []string `json:"dependsOn,omitempty"`
}

type Lockfile struct {
	Features map[string]LockedFeature `json:"features"`
}

func main() {
	legacy := []byte(`{
  "features": {
    "ghcr.io/a/f:1": {"resolved": "sha256:00", "dependsOn": "nope"}
  }
}`)
	var lf Lockfile
	err := json.Unmarshal(legacy, &lf)
	fmt.Printf("legacy-object-string err=%v lf=%#v\n", err, lf)

	legacyMap := []byte(`{
  "features": {
    "ghcr.io/a/f:1": {"resolved": "sha256:00", "dependsOn": {"dep": true}}
  }
}`)
	err = json.Unmarshal(legacyMap, &lf)
	fmt.Printf("legacy-map err=%v lf=%#v\n", err, lf)

	current := []byte(`{
  "features": {
    "ghcr.io/a/f:1": {"resolved": "sha256:00", "dependsOn": ["dep"]}
  }
}`)
	err = json.Unmarshal(current, &lf)
	fmt.Printf("current-array err=%v lf=%#v\n", err, lf)
}
GO

go run "$tmpdir/unmarshal_probe.go"

echo "== inspect config DependsOn type for feature config keys =="
sed -n '1,60p' pkg/devcontainer/config/feature.go
rg -n -C 3 'DependsOn[A-Za-z0-9_]*|DependsOn' pkg/devcontainer/config pkg/devcontainer/feature/lockfile.go pkg/devcontainer/feature/extend.go

Repository: devsy-org/devsy

Length of output: 11881


Normalize legacy dependsOn on lockfile read.

ReadLockfile unmarshals persisted files into LockedFeature, whose DependsOn is now []string; object-shaped dependsOn entries fail to parse during fetch resolution (via newLockfileState). Decode lockfile entries as flexible JSON first/normatively, then normalize to the array shape used for pinning and writing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/devcontainer/feature/lockfile.go` around lines 19 - 22, Update
ReadLockfile to decode persisted feature entries through a flexible JSON
representation before constructing LockedFeature. Normalize legacy object-shaped
dependsOn values into the []string form, while preserving existing array-shaped
values, so newLockfileState receives the canonical representation used for
pinning and writing.

@skevetter
skevetter marked this pull request as draft July 28, 2026 03:29
Address review feedback:
- DependsOnKeys now retains captured declaration order only when every
  captured key still exists in DependsOn, guarding against post-unmarshal
  mutation that keeps the map size unchanged; otherwise it uses the sorted
  fallback.
- LockedFeature.UnmarshalJSON normalizes legacy object-shaped dependsOn
  into the []string form, so pre-fix lockfiles still load and pin correctly.

Signed-off-by: Samuel K <skevetter@pm.me>
@skevetter
skevetter marked this pull request as ready for review July 28, 2026 03:43
@skevetter
skevetter enabled auto-merge (squash) July 28, 2026 03:45
@skevetter
skevetter merged commit 34ad453 into main Jul 28, 2026
66 checks passed
@skevetter
skevetter deleted the fix/lockfile-dependson-array branch July 28, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant