Repository navigation
fix(feature): match devcontainer CLI lockfile dependsOn format #793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
24d01b1
01c3a58
675cd27
52eb634
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "reflect" | ||
| "testing" | ||
| ) | ||
|
|
||
| const ( | ||
| depZeta = "ghcr.io/x/zeta:1" | ||
| depAlpha = "ghcr.io/x/alpha:1" | ||
| depMid = "ghcr.io/x/mid:1" | ||
| ) | ||
|
|
||
| func TestFeatureConfig_DependsOnKeysPreservesDeclarationOrder(t *testing.T) { | ||
| // Keys are intentionally not alphabetical to prove declaration order is | ||
| // preserved rather than sorted, matching the reference devcontainer CLI. | ||
| data := []byte(`{ | ||
| "id": "example", | ||
| "dependsOn": { | ||
| "ghcr.io/x/zeta:1": {}, | ||
| "ghcr.io/x/alpha:1": {}, | ||
| "ghcr.io/x/mid:1": {} | ||
| } | ||
| }`) | ||
|
|
||
| var cfg FeatureConfig | ||
| if err := json.Unmarshal(data, &cfg); err != nil { | ||
| t.Fatalf("unmarshal: %v", err) | ||
| } | ||
|
|
||
| want := []string{depZeta, depAlpha, depMid} | ||
| if got := cfg.DependsOnKeys(); !reflect.DeepEqual(got, want) { | ||
| t.Errorf("DependsOnKeys() = %v, want %v", got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestFeatureConfig_DependsOnKeysFallsBackToSorted(t *testing.T) { | ||
| // A programmatically constructed config has no captured order; keys must be | ||
| // returned deterministically (sorted). | ||
| cfg := FeatureConfig{DependsOn: DependsOnField{ | ||
| depZeta: map[string]any{}, | ||
| depAlpha: map[string]any{}, | ||
| }} | ||
|
|
||
| want := []string{depAlpha, depZeta} | ||
| if got := cfg.DependsOnKeys(); !reflect.DeepEqual(got, want) { | ||
| t.Errorf("DependsOnKeys() = %v, want %v", got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestFeatureConfig_DependsOnKeysEmpty(t *testing.T) { | ||
| var cfg FeatureConfig | ||
| if got := cfg.DependsOnKeys(); len(got) != 0 { | ||
| t.Errorf("DependsOnKeys() = %v, want empty", got) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,10 +16,10 @@ import ( | |
| // LockedFeature is a single pinned entry in a devcontainer-lock.json file. It | ||
| // mirrors the structure produced by the reference devcontainer CLI. | ||
| type LockedFeature struct { | ||
| Version string `json:"version,omitempty"` | ||
| Resolved string `json:"resolved,omitempty"` | ||
| Integrity string `json:"integrity,omitempty"` | ||
| DependsOn map[string]any `json:"dependsOn,omitempty"` | ||
| Version string `json:"version,omitempty"` | ||
| Resolved string `json:"resolved,omitempty"` | ||
| Integrity string `json:"integrity,omitempty"` | ||
| DependsOn []string `json:"dependsOn,omitempty"` | ||
|
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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\(' pkgRepository: 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.goRepository: 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.goRepository: devsy-org/devsy Length of output: 11881 Normalize legacy
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // Lockfile mirrors the devcontainer-lock.json structure: a map of feature | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.