Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
79d70d9
refactor: apply go fix modernizers
aledbf Oct 25, 2026
15a0053
fix!: close divergences from the reference CLI
aledbf Oct 25, 2026
9ba6627
build: gate lint, vulnerabilities and releases on go-tools
aledbf Oct 25, 2026
b60b9c0
ci: run container lanes on spin-stack's docker8 fleet
aledbf Oct 25, 2026
cb6dad6
test(devtools): hold MeanMillis and ParseProfile to what callers see
aledbf Oct 25, 2026
1d2776f
refactor(cli): drop compose_up code no outcome depends on
aledbf Oct 25, 2026
db89cb3
test(cli): hold the compose up flow to what compose and the engine see
aledbf Oct 25, 2026
8d27c35
test(cli): hold stop and down to the results callers see
aledbf Oct 25, 2026
ecec33b
refactor(cli): split set-up so its decisions are testable without Docker
aledbf Oct 25, 2026
d604422
refactor(cli): extract up's pure decisions and test them
aledbf Oct 25, 2026
6408b12
test(cli): hold image build info resolution to what callers see
aledbf Oct 25, 2026
f996fdd
refactor(cli): split read-configuration's output assembly from its I/O
aledbf Oct 25, 2026
35ac8c0
refactor(cli): pull build's decisions out of its Docker orchestration
aledbf Oct 25, 2026
c65d391
refactor(cli): split run-user-commands checks from its container work
aledbf Oct 25, 2026
46c8841
refactor(cli): pull exec's decisions out of its docker wiring
aledbf Oct 25, 2026
30cedba
test(cli): drive the features test runner without docker
aledbf Oct 25, 2026
056398f
test(cli): hold collection publish, package and docs to their output
aledbf Oct 25, 2026
6e7581e
test(cli): check features info and outdated against a local registry
aledbf Oct 25, 2026
77ae41a
test(cli): hold setup, templates apply and the small helpers
aledbf Oct 25, 2026
c11b3d5
refactor(cli): drop feature-install branches no outcome depends on
aledbf Oct 25, 2026
50e7b9f
test(cli): hold feature fetching, staging and image extension
aledbf Oct 25, 2026
11cd4d0
test(cli): hold the mount metadata's type default and empty specs
aledbf Oct 25, 2026
d91df19
test: hold three edits the CI mutation sample found unrefused
aledbf Oct 25, 2026
51bf37e
test(parity): run the oracle with this process's node
aledbf Oct 25, 2026
3aea475
fix(config): keep deprecated properties moved into customizations
aledbf Oct 25, 2026
76ab54c
test(parity): retry a setup_cmd's transient build failure
aledbf Oct 25, 2026
529e232
test(docker): hold builder detection to a failed buildx
aledbf Oct 25, 2026
3af5a08
fix(cli): fail a Feature tarball download past its size cap
aledbf Oct 25, 2026
b1cb9e8
test(imagemeta): hold the metadata merge and dockerfile in unit tests
aledbf Oct 25, 2026
af74390
fix(features): treat a null option value as the reference does
aledbf Oct 25, 2026
6ecb92e
fix(features): cache the empty control manifest after a failed fetch
aledbf Oct 25, 2026
6cf6aa4
fix(oci): read DEVCONTAINERS_OCI_AUTH entries as the reference does
aledbf Oct 25, 2026
7fb23bc
test(oci): hold push, index variants and hub credentials without docker
aledbf Oct 25, 2026
66a20f0
test(features): hold lockfile dependsOn, mermaid nodes and secret masks
aledbf Oct 25, 2026
3634b52
fix(docker): name a compose image as TS does for a short version
aledbf Oct 25, 2026
c74373f
test(docker): hold builder detection, mount readonly and uid builds
aledbf Oct 25, 2026
4f4bbaf
test(lifecycle): hold hook skipping, failure lines and the env probe
aledbf Oct 25, 2026
c8e8435
fix(config): move a null devPort into customizations as TS does
aledbf Oct 25, 2026
24cd2d8
test(config): hold config loading and substitution without docker
aledbf Oct 25, 2026
f298d7c
fix(cli): print the legacy properties read-configuration did not migrate
aledbf Oct 25, 2026
fa1df6c
test(cli): hold the build and feature edits only docker tests refused
aledbf Oct 25, 2026
6c2a595
fix(config): reject a devcontainer.json that is not a JSON object
aledbf Oct 25, 2026
b88b62d
fix(cli): print a null devPort the loader left at the top level
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
2 changes: 2 additions & 0 deletions .github/deadcode-allow.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Functions no binary reaches that are correct for ever, one `<symbol> # why` a line (go tool deadgate -allow).
# An entry that matches nothing fails, so the list only shrinks.
2 changes: 2 additions & 0 deletions .github/deadcode-pending.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Functions no binary reaches whose deletion somebody owes, one `<symbol> # why` a line (go tool deadgate -pending).
# Printed on every green run; an entry that matches nothing fails.
3 changes: 3 additions & 0 deletions .github/testquality-allow.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Tests testquality reports that are kept on purpose, one <package>:<TestName> a line with why
# (go tool testquality -allow). An entry that matches nothing fails, so the list only shrinks.
github.com/devcontainers/cli/internal/exec:TestHelperProcess # the child process OSRunner tests exec; it asserts nothing by design, the parent tests do
77 changes: 67 additions & 10 deletions .github/workflows/go-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,50 @@ on:
permissions:
contents: read

# A newer push to a branch makes the run before it moot, so that run is cancelled; never on main,
# whose every commit needs its answer. 3 of 11 push runs (2026-10-23..25) ran for a commit a newer
# push had replaced.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}

# The jobs that build and run containers land on spin-stack's docker8 fleet (spin-stack/ci: an
# ephemeral 8-vCPU EC2 instance per job, no nested virtualization). On GitHub's 4-vCPU runners a
# runtime parity shard took 10.7 min at p50 and 14.4 at p90, against 5.0 on Blacksmith's. A pull
# request from a fork runs on GitHub's runners instead: its code never reaches spin-stack's AWS
# account. Only a branch of this repository, which takes write access to push, runs on the fleet.

jobs:
# What a pull request changed, broken one small edit at a time (go-tools mutate): an
# edit no test refuses fails, so new code arrives with tests that hold it.
mutate:
if: github.event_name == 'pull_request'
runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'runs-on/fleet=docker8/env=production' || 'ubuntu-latest' }}
# 4 min on GitHub's runners for a diff's 200 mutations; a large diff's builds are many.
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- name: What this change broke, and whether a test noticed
env:
BASE: origin/${{ github.base_ref }}
run: go tool task mutate

lint-and-test:
runs-on: ubuntu-latest
# p90 3.2 min on GitHub's runners.
timeout-minutes: 15
permissions:
contents: read
outputs:
# Where the jobs that build and run containers land (see the top of this file).
fleet: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'runs-on/fleet=docker8/env=production' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
Expand Down Expand Up @@ -51,7 +90,9 @@ jobs:
if-no-files-found: error

e2e:
runs-on: ubuntu-latest
runs-on: ${{ needs.lint-and-test.outputs.fleet }}
# p90 1.6 min on GitHub's runners.
timeout-minutes: 15
needs: lint-and-test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand All @@ -75,7 +116,10 @@ jobs:
# Hermetic parity gate: read-configuration golden comparison + the contract
# lane of the parity matrix (flag validation / output contract, no Docker).
parity:
# The contract and network lanes start no container: GitHub's runner is enough.
runs-on: ubuntu-latest
# p90 2.0 min on GitHub's runners.
timeout-minutes: 10
needs: lint-and-test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -124,6 +168,7 @@ jobs:
daily-changes:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.check.outputs.run }}
steps:
Expand All @@ -139,7 +184,7 @@ jobs:
# run with the plain binary: a fast gate (the full matrix is ~45 min).
# daily — the FULL matrix with the instrumented binary, so one run gates parity
# AND measures coverage; daily-changes keeps it off on quiet days.
# 4 shards across independent runners (each its own docker daemon) so the matrix
# 2 shards across independent runners (each its own docker daemon) so the matrix
# never contends for one runner's CPU/disk/daemon. PARITY_SHARD_TOTAL MUST equal the
# length of matrix.shard — parity_sharding_workflow_test.go enforces it so a mismatch
# can't silently drop cases.
Expand All @@ -153,18 +198,25 @@ jobs:
&& needs.lint-and-test.result == 'success'
&& (github.event_name == 'push' || github.event_name == 'pull_request'
|| needs.daily-changes.outputs.run == 'true')
runs-on: ubuntu-latest
runs-on: ${{ needs.lint-and-test.outputs.fleet }}
# A shard took 14.4 min at p90 as one of four on GitHub's runners; one of two on them (a
# fork's pull request) takes about twice that.
timeout-minutes: 45
strategy:
fail-fast: false
# On a push or a pull request the first failing shard stops the other: what it would have
# found is in the next push's run. The daily run keeps going, since it also measures
# coverage.
fail-fast: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
matrix:
shard: [0, 1, 2, 3]
shard: [0, 1]
env:
PARITY_RUNTIME_TIMEOUT: "10m"
# Each shard owns ~a quarter of the selected cases; -parallel 2 per 2-core runner
# is the proven-safe load (per-case id-label / COMPOSE_PROJECT_NAME isolation).
PARITY_PARALLEL: "2"
# Each shard owns half of the selected cases. -parallel 2 was the proven load on a
# 4-vCPU runner (per-case id-label / COMPOSE_PROJECT_NAME isolation); docker8's 8 vCPU
# hold twice that.
PARITY_PARALLEL: ${{ needs.lint-and-test.outputs.fleet == 'ubuntu-latest' && '2' || '4' }}
PARITY_SHARD_INDEX: ${{ matrix.shard }}
PARITY_SHARD_TOTAL: "4"
PARITY_SHARD_TOTAL: "2"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down Expand Up @@ -220,7 +272,10 @@ jobs:
parity-publish-full:
needs: [lint-and-test, daily-changes]
if: needs.daily-changes.outputs.run == 'true'
runs-on: ubuntu-latest
# Daily and dispatched only: this repository's own code.
runs-on: runs-on/fleet=docker8/env=production
# p90 3.3 min on GitHub's runners.
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand All @@ -247,6 +302,7 @@ jobs:
parity-runtime-gate:
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [lint-and-test, e2e, parity, parity-runtime, parity-publish-full]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -277,6 +333,7 @@ jobs:

cross-compile:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: lint-and-test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ jobs:

# Runtime lane (real Docker containers). ubuntu-latest ships Docker.
gate-runtime:
runs-on: ubuntu-latest
# A tag push or a dispatch: this repository's own code, so spin-stack's docker8 fleet (see
# go-cli.yml), where the whole runtime matrix is one job.
runs-on: runs-on/fleet=docker8/env=production
# e2e, the whole runtime matrix and publish parity in one job; the matrix alone is about 45
# min on a 4-vCPU runner.
timeout-minutes: 90
needs: prepare
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -178,6 +183,9 @@ jobs:

- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

# The released image's base, by digest from versions.yaml.
- run: go tool task release:env

- uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7
with:
distribution: goreleaser
Expand Down
2 changes: 0 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ linters:
- fmt.Fprintln
- (*github.com/spf13/pflag.FlagSet).MarkHidden
- (*github.com/spf13/cobra.Command).MarkFlagRequired
- github.com/devcontainers/cli/internal/pfs.Remove
- github.com/devcontainers/cli/internal/pfs.MkdirAll
# Best-effort metadata/config parsing: on failure the target stays zero
# (an accepted fallback throughout); behavioral divergences are caught by
# the parity matrix, not by these unchecked returns.
Expand Down
2 changes: 2 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ dockers_v2:
build_args:
VERSION: "{{ .Version }}"
REVISION: "{{ .FullCommit }}"
# The base image by digest, from versions.yaml (task release:env).
DISTROLESS_IMAGE: "{{ .Env.DISTROLESS_IMAGE }}"
labels:
org.opencontainers.image.title: devcontainer-cli
org.opencontainers.image.source: https://github.com/spin-stack/devcontainer-cli
Expand Down
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
# context. For a local single-arch build:
# mkdir -p linux/amd64
# CGO_ENABLED=0 go build -o linux/amd64/devcontainer ./cmd/devcontainer
# docker buildx build --platform linux/amd64 --load -t devcontainer-cli .
FROM gcr.io/distroless/static:nonroot
# docker buildx build --platform linux/amd64 --load $(go tool versions args distroless) -t devcontainer-cli .
#
# The base is pinned by digest in versions.yaml (distroless) and handed in: GoReleaser
# reads it from DISTROLESS_IMAGE, which `task release:env` exports.
ARG DISTROLESS_IMAGE
FROM ${DISTROLESS_IMAGE}

# Version/revision are injected at build time. GoReleaser passes them via
# build_args; a local build can pass --build-arg VERSION=... .
Expand Down
Loading
Loading