Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2aa466f
test(doctor): hold every check result without a docker daemon
aledbf Oct 25, 2026
43b40cf
test(pfs): hold the modes and root entry ExtractTarGz writes
aledbf Oct 25, 2026
016bb5d
test(httpx): hold the extra CA on top of the system roots
aledbf Oct 25, 2026
f79d5ef
fix(oci): stop falling back to credsStore after a registry's helper
aledbf Oct 25, 2026
d99ffa4
test(oci): hold the auth policy, ref and client edits without docker
aledbf Oct 25, 2026
297ca34
fix(docker): gate compose features on the whole version like the refe…
aledbf Oct 25, 2026
21a9676
test(docker): hold the engine, dockerfile and context edits without d…
aledbf Oct 25, 2026
6327389
test(log): hold the logger defaults and terminal formatting
aledbf Oct 25, 2026
7ea7e43
fix(imagemeta): read sizes and safe ids as the TS functions do
aledbf Oct 25, 2026
8c819c1
fix(features): write and compare the lockfile as TS does
aledbf Oct 25, 2026
a8de53f
fix(features): keep control manifest entries as TS sanitizes them
aledbf Oct 25, 2026
4f9b10a
test(features): hold the install order, id and diagram edits
aledbf Oct 25, 2026
f75f5b9
fix(lifecycle): clone an absolute dotfiles path as written
aledbf Oct 25, 2026
a880313
fix(lifecycle): log initializeCommand output instead of printing it
aledbf Oct 25, 2026
58356d1
test(config): hold the edits mutate makes in types, loader and varsub
aledbf Oct 25, 2026
b06f65f
test(lifecycle): hold the hook error, shell and env probe edits
aledbf Oct 25, 2026
c5f6358
test(cli): hold positional limits, terminal implications and cleanup
aledbf Oct 25, 2026
48b9732
style: gofmt three files
aledbf Oct 25, 2026
66b27e8
test(lifecycle): lock the mock executor against parallel commands
aledbf Oct 25, 2026
979f2bf
test(lifecycle): expect executor calls with a gomock mock
aledbf Oct 25, 2026
bcec9e3
test(oci): hold the empty-registry skip of ResolveBuildAuth
aledbf Oct 25, 2026
163f0ea
test(parity): serialize the cases that share the Feature content image
aledbf Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/parity/parity-matrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,7 @@ initial_cases:
current_status: match

- id: build.buildkit-never-platform-failure
lock: feature-content-image
lane: contract
command: build
priority: p1
Expand Down Expand Up @@ -1593,6 +1594,7 @@ initial_cases:
current_status: match

- id: exec.workspace-image-with-features-hello-success
lock: feature-content-image
lane: runtime
command: exec
priority: p1
Expand All @@ -1606,6 +1608,7 @@ initial_cases:
current_status: match

- id: exec.workspace-dockerfile-with-features-hello-success
lock: feature-content-image
lane: runtime
command: exec
priority: p1
Expand All @@ -1619,6 +1622,7 @@ initial_cases:
current_status: match

- id: exec.workspace-dockerfile-with-target-marker-success
lock: feature-content-image
lane: runtime
command: exec
priority: p1
Expand All @@ -1632,6 +1636,7 @@ initial_cases:
current_status: match

- id: exec.workspace-compose-image-with-features-hello-success
lock: feature-content-image
no_compose_isolation: true
lane: runtime
command: exec
Expand Down Expand Up @@ -1971,6 +1976,7 @@ initial_cases:
current_status: match

- id: up.image-with-features-omit-config-remote-env-from-metadata-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand Down Expand Up @@ -2009,6 +2015,7 @@ initial_cases:
current_status: match

- id: up.compose-image-with-features-omit-config-remote-env-from-metadata-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand All @@ -2024,6 +2031,7 @@ initial_cases:
notes: "Unblocked by pinning the harness setup with `docker pull --platform linux/amd64 redis:alpine` (the fixture's db service) before compose."

- id: up.compose-dockerfile-with-features-omit-config-remote-env-from-metadata-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand Down Expand Up @@ -2051,6 +2059,7 @@ initial_cases:
current_status: match

- id: up.image-metadata-container-env-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand All @@ -2063,6 +2072,7 @@ initial_cases:
current_status: match

- id: up.image-with-features-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand All @@ -2077,6 +2087,7 @@ initial_cases:
current_status: match

- id: up.image-with-git-feature-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand Down Expand Up @@ -2182,6 +2193,7 @@ initial_cases:
current_status: match

- id: up.compose-dockerfile-with-features-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand All @@ -2196,6 +2208,7 @@ initial_cases:
notes: "Closed out by fixing the compose entrypoint generated by Go so that it uses real line breaks and keeps the container alive"

- id: up.compose-dockerfile-with-features-reuse-stopped-container-with-persisted-overrides-success
lock: feature-content-image
no_compose_isolation: true
serial: true
lane: runtime
Expand All @@ -2213,6 +2226,7 @@ initial_cases:
notes: "Reuse of a stopped compose container with persisted overrides. The divergence observed before (TS failed the second `docker compose up --no-recreate`) was an artifact of the setup with the old TS path; with the setup pointing to the TS submodule, Go and TS match."

- id: up.compose-dockerfile-with-features-reuse-stopped-container-without-persisted-overrides-success
lock: feature-content-image
no_compose_isolation: true
serial: true
lane: runtime
Expand Down Expand Up @@ -2292,6 +2306,7 @@ initial_cases:
current_status: match

- id: up.compose-dockerfile-with-target-post-commands-success
lock: feature-content-image
lane: runtime
command: up
priority: p1
Expand Down Expand Up @@ -2458,6 +2473,7 @@ initial_cases:
current_status: match

- id: build.image-metadata-local-features-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand All @@ -2471,6 +2487,7 @@ initial_cases:
current_status: match

- id: build.skip-persisting-customizations-from-features-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand All @@ -2484,6 +2501,7 @@ initial_cases:
current_status: match

- id: build.labels-success
lock: feature-content-image
lane: runtime
command: build
priority: p0
Expand All @@ -2495,6 +2513,7 @@ initial_cases:
current_status: match

- id: build.local-feature-failure
lock: feature-content-image
lane: runtime
command: build
priority: p0
Expand Down Expand Up @@ -2531,6 +2550,7 @@ initial_cases:
current_status: match

- id: build.dockerfile-multi-image-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand Down Expand Up @@ -2607,6 +2627,7 @@ initial_cases:
notes: "Passes in parity runtime by pinning DOCKER_DEFAULT_PLATFORM=linux/amd64 from the harness"

- id: build.apply-build-options-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand Down Expand Up @@ -2635,6 +2656,7 @@ initial_cases:
notes: "Closed out using a valid cache source (`ubuntu:latest`); the nonexistent remote ref blocked TS before comparing Go."

- id: build.image-metadata-container-env-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand All @@ -2647,6 +2669,7 @@ initial_cases:
current_status: match

- id: build.compose-image-extra-cache-from-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand Down Expand Up @@ -2746,6 +2769,7 @@ initial_cases:
expect_infra_failure the harness compares exit codes instead of skipping via isInfraError."

- id: build.compose-image-with-features-success
lock: feature-content-image
lane: runtime
command: build
priority: p1
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/Masterminds/semver/v3 v3.5.0
github.com/iancoleman/orderedmap v0.3.0
github.com/moby/buildkit v0.34.1
github.com/moby/docker-image-spec v1.3.1
github.com/moby/moby/api v1.56.1
github.com/moby/moby/client v0.6.2
github.com/opencontainers/go-digest v1.0.0
Expand All @@ -17,6 +18,7 @@ require (
github.com/spin-stack/go-tools v0.0.0-20261025134518-0ee222410bc5
github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f
github.com/testcontainers/testcontainers-go v0.44.0
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.58.0
golang.org/x/net v0.61.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -127,7 +129,6 @@ require (
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.3.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
Expand Down Expand Up @@ -197,5 +198,6 @@ tool (
github.com/spin-stack/go-tools/cmd/testquality
github.com/spin-stack/go-tools/cmd/versions
github.com/spin-stack/go-tools/cmd/vulngate
go.uber.org/mock/mockgen
golang.org/x/tools/cmd/deadcode
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
Expand Down
1 change: 1 addition & 0 deletions internal/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ func prepareDockerfileBuild(cfg *config.DevContainer) (*dockerfileBuildPrep, err
prep.StageName = stageName
if modified != "" {
tmpDockerfile := dockerfilePath + ".devcontainer.build"
// mutate-exempt: only the docker build this process starts reads the file, as its owner, and Cleanup removes it
if err := os.WriteFile(tmpDockerfile, []byte(modified), 0644); err != nil {
return nil, fmt.Errorf("write build dockerfile: %w", err)
}
Expand Down
5 changes: 2 additions & 3 deletions internal/cli/build_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,10 @@ func registryHostFromImageRef(ref string) string {
if ref == "" {
return ""
}
i := strings.IndexByte(ref, '/')
if i < 0 {
first, _, found := strings.Cut(ref, "/")
if !found {
return ""
}
first := ref[:i]
if first == "localhost" || strings.ContainsAny(first, ".:") {
return first
}
Expand Down
50 changes: 50 additions & 0 deletions internal/cli/build_repo2_held_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cli

import (
"errors"
"os"
"path/filepath"
"strings"
"testing"

"github.com/devcontainers/cli/internal/config"
)

// The Dockerfile written to name the final stage sits next to the user's with
// a copy of its .dockerignore, and Cleanup removes both and leaves the user's
// files alone.
func TestPrepareDockerfileBuildCleanupRemovesTheWrittenFiles(t *testing.T) {
dir := t.TempDir()
dockerfile := filepath.Join(dir, "Dockerfile")
if err := os.WriteFile(dockerfile, []byte("FROM alpine\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(dockerfile+".dockerignore", []byte("secret\n"), 0o644); err != nil {
t.Fatal(err)
}
prep, err := prepareDockerfileBuild(&config.DevContainer{
ConfigFilePath: filepath.Join(dir, "devcontainer.json"),
Build: &config.Build{Dockerfile: "Dockerfile"},
})
if err != nil {
t.Fatal(err)
}
written, err := os.ReadFile(prep.Path)
if err != nil || !strings.Contains(string(written), "AS dev_container_auto_added_stage_label") {
t.Fatalf("written Dockerfile = %q, %v; want the final stage named", written, err)
}
if ignore, err := os.ReadFile(prep.Path + ".dockerignore"); err != nil || string(ignore) != "secret\n" {
t.Fatalf("written .dockerignore = %q, %v; want the user's copied", ignore, err)
}
prep.Cleanup()
for _, p := range []string{prep.Path, prep.Path + ".dockerignore"} {
if _, err := os.Stat(p); !errors.Is(err, os.ErrNotExist) {
t.Errorf("%s after Cleanup: %v, want it removed", p, err)
}
}
for _, p := range []string{dockerfile, dockerfile + ".dockerignore"} {
if _, err := os.Stat(p); err != nil {
t.Errorf("user's %s after Cleanup: %v", p, err)
}
}
}
1 change: 0 additions & 1 deletion internal/cli/collection_package_held_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,3 @@ func TestPackageTemplateOptionalPathsShape(t *testing.T) {
t.Errorf("collection lacks %s:\n%s", want, got)
}
}

2 changes: 1 addition & 1 deletion internal/cli/disallowed.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
func cacheFolder() string {
base := "devcontainercli"
if runtime.GOOS == "linux" {
if u, err := user.Current(); err == nil && u.Username != "" {
if u, err := user.Current(); err == nil {
base = "devcontainercli-" + u.Username
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/image_build_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ func (r *dockerfileResolver) replace(baseEnv map[string]string, str string, s *d
b.WriteString(str[last:m[0]])
last = m[1]
value := r.findValue(baseEnv, str[m[2]:m[3]], s, before)
varExp := m[4] // mutate-exempt: m[4] and m[5] are both -1 exactly when the :-word/:+word group is unmatched
varExp := m[4] // mutate-exempt: m[4] and m[5] are both -1 exactly when the :-word/:+word group is unmatched
if varExp != -1 { // ${var:-word} / ${var:+word}
option, word := str[m[6]], str[m[8]:m[9]] // the option is one character, - or +
isSet := value != ""
Expand Down
44 changes: 44 additions & 0 deletions internal/cli/ociauth_held_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cli

import (
"context"
"testing"

"github.com/devcontainers/cli/internal/log"
"github.com/devcontainers/cli/internal/oci"
)

// Storing the policy keeps the command context it is stored on: a cancelled
// command (Ctrl-C) must still cancel the registry traffic that reads the
// policy from the same context. A caller without a context gets one.
func TestWithOCIAuthPolicyKeepsTheContext(t *testing.T) {
policy, err := oci.NewAuthPolicy(true, nil, log.Null)
if err != nil {
t.Fatal(err)
}
parent, cancel := context.WithCancel(context.Background())
ctx := withOCIAuthPolicy(parent, policy)
cancel()
if ctx.Err() == nil {
t.Error("cancelling the command context did not reach the context holding the policy")
}
if got := ociAuthPolicy(ctx); got != policy {
t.Errorf("ociAuthPolicy = %p, want the stored policy %p", got, policy)
}
//nolint:staticcheck // SA1012: a nil context is the case under test
if got := ociAuthPolicy(withOCIAuthPolicy(nil, policy)); got != policy {
t.Errorf("ociAuthPolicy without a parent context = %p, want %p", got, policy)
}
}

// A context holding a nil policy reads as the default policy (hardening off,
// as the reference CLI defaults --oci-auth-hardening), never as nil.
func TestOCIAuthPolicyIgnoresAStoredNil(t *testing.T) {
policy := ociAuthPolicy(withOCIAuthPolicy(context.Background(), nil))
if policy == nil {
t.Fatal("ociAuthPolicy returned nil")
}
if policy.Hardening() {
t.Error("the default policy must not harden")
}
}
Loading
Loading