From 6c049deab86e475147732cfb0d9ff46343076700 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 11:54:56 +0000 Subject: [PATCH 1/3] ci: merge devcontainer-check into build-checks The sandbox dev containers were built by their own workflow, which rebuilt all of them whenever any Dockerfile or smoke test changed. Move that job into build-checks.yml so it is driven by the same change detection as the image builds: a change under node/ now builds only the node sandbox. changed-images.sh no longer ignores .devcontainer/sandbox-, and maps each sandbox directory to its image the same way it already maps the Feature configurations. A setup step asserts the sandbox directories and the image list still match, since the matrix is derived from the image list and would otherwise skip a sandbox that belongs to no image. The sandbox jobs keep building through the Dev Container CLI rather than reusing the images the other jobs load locally. Reusing them requires the same build args, and the only channel for those is ${localEnv:...} in the sandbox configurations, which cannot carry the pinned Debian digest: the CLI splits the default value on every colon, so the digest would be truncated. Wrapping the values would also break the devcontainer.json patterns in renovate.jsonc. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01US45nXX1mwPpp6SKM9jMea --- .github/workflows/build-checks.yml | 56 +++++++++++++++++++ .github/workflows/devcontainer-check.yml | 69 ------------------------ AGENTS.md | 3 +- scripts/changed-images.sh | 13 +++-- 4 files changed, 63 insertions(+), 78 deletions(-) delete mode 100644 .github/workflows/devcontainer-check.yml diff --git a/.github/workflows/build-checks.yml b/.github/workflows/build-checks.yml index c4955ef..65efdd9 100644 --- a/.github/workflows/build-checks.yml +++ b/.github/workflows/build-checks.yml @@ -11,6 +11,7 @@ on: - '*/rustup-init-*.sha256' - '*/uv-*.sha256' - .devcontainer/feature-** + - .devcontainer/sandbox-** - scripts/build-config.sh - scripts/changed-images.sh - .trivyignore.yaml @@ -25,6 +26,7 @@ on: - '*/rustup-init-*.sha256' - '*/uv-*.sha256' - .devcontainer/feature-** + - .devcontainer/sandbox-** - scripts/build-config.sh - scripts/changed-images.sh @@ -44,6 +46,7 @@ jobs: outputs: base_matrix: ${{ steps.build.outputs.base_matrix }} matrix: ${{ steps.build.outputs.matrix }} + sandbox_matrix: ${{ steps.build.outputs.sandbox_matrix }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -81,6 +84,19 @@ jobs: echo "$REPORT" | jq -r '.[] | "[" + (if .selected then "RUN " else "SKIP" end) + "] " + .image + ": " + .reason, (.files[] | " " + .)' + - name: Verify sandbox coverage + # The sandbox matrix below is derived from the image list rather than + # from the sandbox directories, so a sandbox belonging to no image + # would never be built. Fail instead of skipping it silently. + run: | + IMAGES=$(bash scripts/build-config.sh images | jq -r '.[]' | sort) + SANDBOXES=$(find .devcontainer -maxdepth 1 -type d -name 'sandbox-*' -printf '%f\n' \ + | sed 's/^sandbox-//' | sort) + if [ "$IMAGES" != "$SANDBOXES" ]; then + echo "ERROR: sandbox dev containers do not match the image list" >&2 + diff <(echo "$IMAGES") <(echo "$SANDBOXES") >&2 || true + exit 1 + fi - name: Build matrix id: build # Expand each entry across both architectures so images are @@ -101,6 +117,10 @@ jobs: echo "base_matrix=${BASE_MATRIX}" >> "$GITHUB_OUTPUT" echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + # One sandbox dev container per image, so this matrix is not expanded + # across variants or architectures: the configuration pins its own + # build args and does not vary with either. + echo "sandbox_matrix=${SELECTED}" >> "$GITHUB_OUTPUT" check-base: needs: setup @@ -251,3 +271,39 @@ jobs: ignore-unfixed: true trivyignores: .trivyignore.yaml exit-code: '1' + + # Builds the sandbox dev container of each changed image through the Dev + # Container CLI. The jobs above build the same Dockerfiles directly, so what + # this adds is that the sandbox configurations themselves still resolve: the + # build args they pin, and the dev container the CLI produces from them. + check-sandbox: + needs: setup + if: needs.setup.outputs.sandbox_matrix != '[]' + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + image: ${{ fromJson(needs.setup.outputs.sandbox_matrix) }} + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Build and run smoke tests + uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 + env: + MISE_GITHUB_TOKEN: ${{ matrix.image == 'mise' && github.token || '' }} + with: + configFile: .devcontainer/sandbox-${{ matrix.image }}/devcontainer.json + push: never + env: | + MISE_GITHUB_TOKEN + runCmd: | + set -e + bash debian/smoke-test.sh + if [ "${{ matrix.image }}" != "debian" ]; then + bash ${{ matrix.image }}/smoke-test.sh + fi diff --git a/.github/workflows/devcontainer-check.yml b/.github/workflows/devcontainer-check.yml deleted file mode 100644 index f45ce38..0000000 --- a/.github/workflows/devcontainer-check.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Devcontainer Checks - -on: - pull_request: - paths: - - .github/workflows/devcontainer-check.yml - - .devcontainer/sandbox-** - - '*/Dockerfile' - - '*/smoke-test.sh' - push: - branches: - - main - paths: - - .devcontainer/sandbox-** - - '*/Dockerfile' - - '*/smoke-test.sh' - -permissions: {} - -jobs: - setup: - permissions: - contents: read - runs-on: ubuntu-slim - timeout-minutes: 5 - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - name: Build sandbox matrix - id: matrix - run: | - MATRIX=$(find .devcontainer -maxdepth 1 -type d -name 'sandbox-*' -printf '%f\n' \ - | sed 's/^sandbox-//' | sort | jq -R -s -c 'split("\n")[:-1]') - echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" - - test: - needs: setup - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - image: ${{ fromJson(needs.setup.outputs.matrix) }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - name: Build and run smoke tests - uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 - env: - MISE_GITHUB_TOKEN: ${{ matrix.image == 'mise' && github.token || '' }} - with: - configFile: .devcontainer/sandbox-${{ matrix.image }}/devcontainer.json - push: never - env: | - MISE_GITHUB_TOKEN - runCmd: | - set -e - bash debian/smoke-test.sh - if [ "${{ matrix.image }}" != "debian" ]; then - bash ${{ matrix.image }}/smoke-test.sh - fi diff --git a/AGENTS.md b/AGENTS.md index b01f33d..8c172f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,8 +15,7 @@ scripts/ verify-image.sh # confirms image verification steps succeed for a published image; run by attest-check.yml .github/workflows/ release.yml # builds and pushes images to GHCR - build-checks.yml # builds each changed image, smoke-tests it, and runs the Dev Container Feature tests on the base - devcontainer-check.yml # builds each sandbox dev container and runs its smoke tests + build-checks.yml # for each changed image: builds and smoke-tests it, builds its sandbox dev container, and runs the Dev Container Feature tests on the base .devcontainer/ default/ # dev container for working in this repo sandbox-/ # one per image; for manually testing each published image. Kept free of Features so it mirrors the published image diff --git a/scripts/changed-images.sh b/scripts/changed-images.sh index 47c8715..6ae9c10 100755 --- a/scripts/changed-images.sh +++ b/scripts/changed-images.sh @@ -20,6 +20,8 @@ # - Files under / affect that image. # - Files under .devcontainer/feature-/ affect that image, since that # configuration layers the Dev Container Features onto it. +# - Files under .devcontainer/sandbox-/ affect that image, since that +# configuration builds it as a dev container. # - The files listed in CROSS_IMAGE_PATHS affect every image even though they # live in one image's directory. Note that debian/Dockerfile is NOT one of # them: build-checks.yml builds each derived image FROM the *published* @@ -40,12 +42,8 @@ CROSS_IMAGE_PATHS='["debian/smoke-test.sh"]' # Paths that cannot reach any build the caller performs, matched as a regular # expression against each changed path. Without this they would fall through to # the repository-wide catch-all below and rebuild every image on both -# architectures: -# - Markdown is documentation only; no Dockerfile copies one in. -# - The sandbox dev containers are built and smoke-tested by -# devcontainer-check.yml, which selects them on its own; build-checks.yml -# never reads them. -IGNORED_PATTERN='\.md$|^\.devcontainer/sandbox-' +# architectures. Markdown is documentation only; no Dockerfile copies one in. +IGNORED_PATTERN='\.md$' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -63,7 +61,8 @@ jq -n -c --argjson images "$IMAGES" --argjson cross_image "$CROSS_IMAGE_PATHS" \ # Directory of the image a path belongs to, or null when it belongs to none. def owner($path): ($images | map(select(. as $dir | $path | startswith($dir + "/"))) | first) - // ($images | map(select(. as $dir | $path | startswith(".devcontainer/feature-" + $dir + "/"))) | first); + // ($images | map(select(. as $dir | $path | startswith(".devcontainer/feature-" + $dir + "/"))) | first) + // ($images | map(select(. as $dir | $path | startswith(".devcontainer/sandbox-" + $dir + "/"))) | first); ($changed | split("\n") | map(select(length > 0 and (test($ignored) | not)))) as $files | ($files | map(select(IN($cross_image[])))) as $cross_image_changed From 791c63fa820d9a7d1d711e74d79e69705d0405e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 12:10:30 +0000 Subject: [PATCH 2/3] ci: build the sandbox dev containers from the image build jobs Replaces the standalone check-sandbox job, which rebuilt each image a second time from scratch on its own runner, with a step in the jobs that already build those images. The sandbox configurations now read their build args from the environment through ${localEnv:...}. check-base and check-image export the arguments they just built with, so the Dev Container CLI resolves the same Dockerfile, context and arguments and reuses the layers the build step produced. The smoke tests are no longer repeated there: the image is the one the previous step already tested, so the step asserts only what the configuration adds on top of it, that the CLI applies remoteUser. No default is given for any argument, matching feature-debian: a missing value fails the build instead of silently selecting a different version. This removes the pinned copy of every build arg from the sandbox configurations, leaving build.yaml as their only source, and with it the Renovate rules that kept the two in sync. The setup guard now also checks that each sandbox declares exactly the build args of its image, since a mismatch would leave an argument empty rather than fail outright. AGENTS.md documents how to export the arguments when opening a sandbox by hand. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01US45nXX1mwPpp6SKM9jMea --- .devcontainer/sandbox-bun/devcontainer.json | 10 +- .../sandbox-debian/devcontainer.json | 7 +- .devcontainer/sandbox-deno/devcontainer.json | 10 +- .../sandbox-golang/devcontainer.json | 13 +- .devcontainer/sandbox-mise/devcontainer.json | 10 +- .devcontainer/sandbox-node/devcontainer.json | 13 +- .../sandbox-rustup/devcontainer.json | 10 +- .../sandbox-terraform/devcontainer.json | 13 +- .devcontainer/sandbox-uv/devcontainer.json | 10 +- .devcontainer/sandbox-zig/devcontainer.json | 16 +-- .github/workflows/build-checks.yml | 116 ++++++++++-------- AGENTS.md | 13 +- renovate.jsonc | 13 +- 13 files changed, 146 insertions(+), 108 deletions(-) diff --git a/.devcontainer/sandbox-bun/devcontainer.json b/.devcontainer/sandbox-bun/devcontainer.json index 8d87baa..0a01b61 100644 --- a/.devcontainer/sandbox-bun/devcontainer.json +++ b/.devcontainer/sandbox-bun/devcontainer.json @@ -1,12 +1,14 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-bun", "build": { "dockerfile": "../../bun/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=github-releases depName=oven-sh/bun versioning=semver extractVersion=^bun-v(?.+) - "BUN_VERSION": "1.3.14" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "BUN_VERSION": "${localEnv:BUN_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-debian/devcontainer.json b/.devcontainer/sandbox-debian/devcontainer.json index c34afef..57d73df 100644 --- a/.devcontainer/sandbox-debian/devcontainer.json +++ b/.devcontainer/sandbox-debian/devcontainer.json @@ -1,10 +1,13 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-debian", "build": { "dockerfile": "../../debian/Dockerfile", "args": { - // renovate: datasource=docker depName=debian versioning=debian - "DEBIAN_TAG": "trixie-20260713@sha256:fac46bff2e02f51425b6e33b0e1169f55dfb053d83511ca28aa50c09fd5ed7a4" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-deno/devcontainer.json b/.devcontainer/sandbox-deno/devcontainer.json index b4aae5e..61fd61b 100644 --- a/.devcontainer/sandbox-deno/devcontainer.json +++ b/.devcontainer/sandbox-deno/devcontainer.json @@ -1,12 +1,14 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-deno", "build": { "dockerfile": "../../deno/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=github-releases depName=denoland/deno versioning=semver extractVersion=^v(?.+) - "DENO_VERSION": "2.9.4" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "DENO_VERSION": "${localEnv:DENO_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-golang/devcontainer.json b/.devcontainer/sandbox-golang/devcontainer.json index b080d87..30414d1 100644 --- a/.devcontainer/sandbox-golang/devcontainer.json +++ b/.devcontainer/sandbox-golang/devcontainer.json @@ -1,14 +1,15 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-golang", "build": { "dockerfile": "../../golang/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=golang-version depName=golang - "GOLANG_VERSION": "1.26.5", - // renovate: datasource=go depName=golang.org/x/tools/gopls - "GOPLS_VERSION": "v0.23.0" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "GOLANG_VERSION": "${localEnv:GOLANG_VERSION}", + "GOPLS_VERSION": "${localEnv:GOPLS_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-mise/devcontainer.json b/.devcontainer/sandbox-mise/devcontainer.json index b55d0f6..809d3a9 100644 --- a/.devcontainer/sandbox-mise/devcontainer.json +++ b/.devcontainer/sandbox-mise/devcontainer.json @@ -1,12 +1,14 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-mise", "build": { "dockerfile": "../../mise/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=github-releases depName=jdx/mise versioning=semver - "MISE_VERSION": "2026.7.13" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "MISE_VERSION": "${localEnv:MISE_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-node/devcontainer.json b/.devcontainer/sandbox-node/devcontainer.json index fb8f430..59b47ef 100644 --- a/.devcontainer/sandbox-node/devcontainer.json +++ b/.devcontainer/sandbox-node/devcontainer.json @@ -1,14 +1,15 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-node", "build": { "dockerfile": "../../node/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=node-version depName=node versioning=node - "NODE_VERSION": "26.5.0", - // renovate: datasource=npm depName=corepack versioning=semver - "COREPACK_VERSION": "0.35.0" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "NODE_VERSION": "${localEnv:NODE_VERSION}", + "COREPACK_VERSION": "${localEnv:COREPACK_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-rustup/devcontainer.json b/.devcontainer/sandbox-rustup/devcontainer.json index b5b3082..44773e3 100644 --- a/.devcontainer/sandbox-rustup/devcontainer.json +++ b/.devcontainer/sandbox-rustup/devcontainer.json @@ -1,12 +1,14 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-rustup", "build": { "dockerfile": "../../rustup/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=github-releases depName=rust-lang/rustup versioning=semver - "RUSTUP_VERSION": "1.29.0" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "RUSTUP_VERSION": "${localEnv:RUSTUP_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-terraform/devcontainer.json b/.devcontainer/sandbox-terraform/devcontainer.json index 5f5a800..f85965e 100644 --- a/.devcontainer/sandbox-terraform/devcontainer.json +++ b/.devcontainer/sandbox-terraform/devcontainer.json @@ -1,14 +1,15 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-terraform", "build": { "dockerfile": "../../terraform/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=github-releases depName=hashicorp/terraform versioning=semver extractVersion=^v(?.+) - "TERRAFORM_VERSION": "1.15.8", - // renovate: datasource=github-releases depName=hashicorp/terraform-ls versioning=semver extractVersion=^v(?.+) - "TERRAFORM_LS_VERSION": "0.39.0" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "TERRAFORM_VERSION": "${localEnv:TERRAFORM_VERSION}", + "TERRAFORM_LS_VERSION": "${localEnv:TERRAFORM_LS_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-uv/devcontainer.json b/.devcontainer/sandbox-uv/devcontainer.json index 5e8d7e2..26ad56e 100644 --- a/.devcontainer/sandbox-uv/devcontainer.json +++ b/.devcontainer/sandbox-uv/devcontainer.json @@ -1,12 +1,14 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-uv", "build": { "dockerfile": "../../uv/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=github-releases depName=astral-sh/uv versioning=semver - "UV_VERSION": "0.11.32" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "UV_VERSION": "${localEnv:UV_VERSION}" } }, "runArgs": [ diff --git a/.devcontainer/sandbox-zig/devcontainer.json b/.devcontainer/sandbox-zig/devcontainer.json index 17333d0..9e74342 100644 --- a/.devcontainer/sandbox-zig/devcontainer.json +++ b/.devcontainer/sandbox-zig/devcontainer.json @@ -1,16 +1,16 @@ { + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. "name": "sandbox-zig", "build": { "dockerfile": "../../zig/Dockerfile", "args": { - // renovate: datasource=docker registryUrl=https://ghcr.io depName=bare-devcontainer/debian versioning=debian - "DEBIAN_TAG": "trixie-20260725@sha256:709adc59c1bd061e416d33119aad002fd361b4058a845543d9ddc4fac762dd48", - // renovate: datasource=gitea-tags registryUrl=https://codeberg.org depName=ziglang/zig versioning=semver - "ZIG_VERSION": "0.16.0", - // renovate: datasource=github-tags depName=zigtools/zls versioning=semver - "ZLS_VERSION": "0.16.0", - // renovate: datasource=gitea-commits registryUrl=https://codeberg.org depName=ziglang/shell-completions - "SHELL_COMPLETIONS_COMMIT": "c2983a75dcbcaf3a1df74ab563a9bd3c8e7f448e" + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "ZIG_VERSION": "${localEnv:ZIG_VERSION}", + "ZLS_VERSION": "${localEnv:ZLS_VERSION}", + "SHELL_COMPLETIONS_COMMIT": "${localEnv:SHELL_COMPLETIONS_COMMIT}" } }, "runArgs": [ diff --git a/.github/workflows/build-checks.yml b/.github/workflows/build-checks.yml index 65efdd9..cf87e7c 100644 --- a/.github/workflows/build-checks.yml +++ b/.github/workflows/build-checks.yml @@ -46,7 +46,6 @@ jobs: outputs: base_matrix: ${{ steps.build.outputs.base_matrix }} matrix: ${{ steps.build.outputs.matrix }} - sandbox_matrix: ${{ steps.build.outputs.sandbox_matrix }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -84,19 +83,43 @@ jobs: echo "$REPORT" | jq -r '.[] | "[" + (if .selected then "RUN " else "SKIP" end) + "] " + .image + ": " + .reason, (.files[] | " " + .)' - - name: Verify sandbox coverage - # The sandbox matrix below is derived from the image list rather than - # from the sandbox directories, so a sandbox belonging to no image - # would never be built. Fail instead of skipping it silently. + - name: Verify sandbox configurations + # The build jobs export the build args of the variant they build so the + # sandbox configuration picks them up through ${localEnv:...}. That only + # holds while every image has a sandbox and every sandbox declares + # exactly the args of its image; otherwise an argument silently resolves + # to the empty string. Neither is tied to a changed image, so both are + # checked for the whole repository here. run: | - IMAGES=$(bash scripts/build-config.sh images | jq -r '.[]' | sort) + IMAGES=$(bash scripts/build-config.sh images | jq -r '.[]') SANDBOXES=$(find .devcontainer -maxdepth 1 -type d -name 'sandbox-*' -printf '%f\n' \ | sed 's/^sandbox-//' | sort) - if [ "$IMAGES" != "$SANDBOXES" ]; then + if [ "$(echo "$IMAGES" | sort)" != "$SANDBOXES" ]; then echo "ERROR: sandbox dev containers do not match the image list" >&2 - diff <(echo "$IMAGES") <(echo "$SANDBOXES") >&2 || true + diff <(echo "$IMAGES" | sort) <(echo "$SANDBOXES") >&2 || true exit 1 fi + + STATUS=0 + for IMAGE in $IMAGES; do + CONFIG=".devcontainer/sandbox-${IMAGE}/devcontainer.json" + DECLARED=$(grep -o '\${localEnv:[A-Z_]\+}' "$CONFIG" \ + | sed 's/^\${localEnv:\(.*\)}$/\1/' | sort -u) + for VARIANT in $(bash scripts/build-config.sh variants "$IMAGE" | jq -r '.[]'); do + # DEBIAN_TAG is a build arg of the debian image and the base + # selector of every other one, so only the latter add it here. + EXPECTED=$({ + bash scripts/build-config.sh build-args "$IMAGE" "$VARIANT" | cut -d= -f1 + [ "$IMAGE" = "debian" ] || echo "DEBIAN_TAG" + } | sort -u) + if [ "$DECLARED" != "$EXPECTED" ]; then + echo "ERROR: ${CONFIG} does not declare the build args of ${IMAGE}/${VARIANT}" >&2 + diff <(echo "$EXPECTED") <(echo "$DECLARED") >&2 || true + STATUS=1 + fi + done + done + exit "$STATUS" - name: Build matrix id: build # Expand each entry across both architectures so images are @@ -117,10 +140,6 @@ jobs: echo "base_matrix=${BASE_MATRIX}" >> "$GITHUB_OUTPUT" echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" - # One sandbox dev container per image, so this matrix is not expanded - # across variants or architectures: the configuration pins its own - # build args and does not vary with either. - echo "sandbox_matrix=${SELECTED}" >> "$GITHUB_OUTPUT" check-base: needs: setup @@ -174,6 +193,22 @@ jobs: --mount "type=bind,source=${{ github.workspace }}/debian/smoke-test.sh,target=/smoke-test.sh,readonly" \ "debian:${{ matrix.variant }}" \ bash /smoke-test.sh + - name: Verify sandbox dev container + # Built with the same args as the build step above, so the Dev Container + # CLI reuses those layers rather than rebuilding the image. The image + # contents are already covered by the smoke test, so this asserts only + # what the configuration adds: the CLI resolves it and applies remoteUser. + if: matrix.arch == 'amd64' + uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 + env: + DEBIAN_TAG: ${{ steps.read.outputs.debian_tag }} + with: + configFile: .devcontainer/sandbox-debian/devcontainer.json + push: never + runCmd: | + set -e + [ "$(id -un)" = "dev" ] \ + || { echo "ERROR: expected user dev, got $(id -un)" >&2; exit 1; } - name: Run Trivy vulnerability scanner # Vulnerability findings are identical across architectures; scan once. if: matrix.arch == 'amd64' @@ -228,6 +263,13 @@ jobs: echo "DEBIAN_TAG=${DEBIAN_VARIANT}" echo "${DELIM}" } >> "$GITHUB_OUTPUT" + # The sandbox configuration reads the same values through + # ${localEnv:...}. Exported rather than set per step because the keys + # differ per image. + { + bash scripts/build-config.sh build-args "${{ matrix.image }}" "${{ matrix.variant }}" + echo "DEBIAN_TAG=${DEBIAN_VARIANT}" + } >> "$GITHUB_ENV" - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build language image @@ -259,6 +301,20 @@ jobs: --mount "type=bind,source=${{ github.workspace }}/${{ matrix.image }}/smoke-test.sh,target=/smoke-test.sh,readonly" \ "${{ matrix.image }}:${{ matrix.variant }}" \ bash -c "bash /smoke-test-debian.sh && bash /smoke-test.sh" + - name: Verify sandbox dev container + # Built with the same args as the build step above, so the Dev Container + # CLI reuses those layers rather than rebuilding the image. The image + # contents are already covered by the smoke test, so this asserts only + # what the configuration adds: the CLI resolves it and applies remoteUser. + if: matrix.arch == 'amd64' + uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 + with: + configFile: .devcontainer/sandbox-${{ matrix.image }}/devcontainer.json + push: never + runCmd: | + set -e + [ "$(id -un)" = "dev" ] \ + || { echo "ERROR: expected user dev, got $(id -un)" >&2; exit 1; } - name: Run Trivy vulnerability scanner # Vulnerability findings are identical across architectures; scan once. if: matrix.arch == 'amd64' @@ -271,39 +327,3 @@ jobs: ignore-unfixed: true trivyignores: .trivyignore.yaml exit-code: '1' - - # Builds the sandbox dev container of each changed image through the Dev - # Container CLI. The jobs above build the same Dockerfiles directly, so what - # this adds is that the sandbox configurations themselves still resolve: the - # build args they pin, and the dev container the CLI produces from them. - check-sandbox: - needs: setup - if: needs.setup.outputs.sandbox_matrix != '[]' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - image: ${{ fromJson(needs.setup.outputs.sandbox_matrix) }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - name: Build and run smoke tests - uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 - env: - MISE_GITHUB_TOKEN: ${{ matrix.image == 'mise' && github.token || '' }} - with: - configFile: .devcontainer/sandbox-${{ matrix.image }}/devcontainer.json - push: never - env: | - MISE_GITHUB_TOKEN - runCmd: | - set -e - bash debian/smoke-test.sh - if [ "${{ matrix.image }}" != "debian" ]; then - bash ${{ matrix.image }}/smoke-test.sh - fi diff --git a/AGENTS.md b/AGENTS.md index 8c172f0..629a1c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ scripts/ build-checks.yml # for each changed image: builds and smoke-tests it, builds its sandbox dev container, and runs the Dev Container Feature tests on the base .devcontainer/ default/ # dev container for working in this repo - sandbox-/ # one per image; for manually testing each published image. Kept free of Features so it mirrors the published image + sandbox-/ # one per image; for manually testing each image. Kept free of Features so it mirrors the published image feature-/ # an image with every verified Dev Container Feature layered on, plus the test.sh covering them renovate.jsonc # Renovate config ``` @@ -33,6 +33,17 @@ renovate.jsonc # Renovate config - Verify against `debian` alone, since every image extends it. Add the Feature to the single `.devcontainer/feature-debian` configuration rather than introducing another one. - Leave Feature options at their upstream defaults, so the check reflects what a consumer gets. Record the reason in a comment whenever a default has to be overridden. - Assert only what the Feature and the image are jointly responsible for. The container's runtime flags are Docker's behaviour, not this repository's, so leave them unasserted; the image's own `smoke-test.sh` runs first and covers what the image ships, so never repeat it in `test.sh`. +- The sandbox dev containers take their build args from the environment with no defaults, so `build-checks.yml` can build them with the arguments it just built the image with and reuse those layers. Export the arguments of the variant you want before opening one by hand: + + ```sh + set -a + . <(scripts/build-config.sh build-args node 26-trixie) + DEBIAN_TAG=$(scripts/build-config.sh get-field node 26-trixie debian_variant) + set +a + code . + ``` + + `debian` is the exception: its `build-args` already carry `DEBIAN_TAG`, so the second line is unnecessary. - Use English for all documentation and comments. - Comments should follow either of the following types: - Documentation comments: describe the purpose/signature of a file, function, or block of code. diff --git a/renovate.jsonc b/renovate.jsonc index b5c0212..10ccd79 100644 --- a/renovate.jsonc +++ b/renovate.jsonc @@ -102,13 +102,6 @@ ], "minimumReleaseAge": null }, - { - "description": "Sandbox dev containers pin the same versions as the image build.yaml files (updated without delay above) and must stay in sync with the committed trust material those updates refresh, so apply their updates without delay as well.", - "matchFileNames": [ - ".devcontainer/sandbox-*/devcontainer.json" - ], - "minimumReleaseAge": null - }, { "description": "Keep Debian image tags in the - format and pinned to the same release line (e.g., bookworm, trixie). Renovate's built-in debian versioning rewrites dated codename tags to bare numeric release tags (e.g. trixie-20260713 -> 13) in getNewValue, so override it (including the per-line versioning=debian annotations) with a regex versioning that treats the codename as the compatibility layer.", "matchDatasources": [ @@ -132,8 +125,7 @@ }, { "matchFileNames": [ - "golang/build.yaml", - ".devcontainer/sandbox-golang/devcontainer.json" + "golang/build.yaml" ], "matchPackageNames": [ "golang" @@ -146,8 +138,7 @@ }, { "matchFileNames": [ - "zig/build.yaml", - ".devcontainer/sandbox-zig/devcontainer.json" + "zig/build.yaml" ], "matchPackageNames": [ "ziglang/zig", From 06e81e57a11a731d074af42f3192d01526257f95 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 12:13:46 +0000 Subject: [PATCH 3/3] ci: verify the sandbox dev containers on both architectures The step reuses the layers the build step in the same job produced, so it costs about the same on either runner. Dropping the amd64 restriction also matches the Dev Container Feature tests next to it, which have always run on both. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01US45nXX1mwPpp6SKM9jMea --- .github/workflows/build-checks.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-checks.yml b/.github/workflows/build-checks.yml index cf87e7c..71aaf46 100644 --- a/.github/workflows/build-checks.yml +++ b/.github/workflows/build-checks.yml @@ -198,7 +198,6 @@ jobs: # CLI reuses those layers rather than rebuilding the image. The image # contents are already covered by the smoke test, so this asserts only # what the configuration adds: the CLI resolves it and applies remoteUser. - if: matrix.arch == 'amd64' uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 env: DEBIAN_TAG: ${{ steps.read.outputs.debian_tag }} @@ -306,7 +305,6 @@ jobs: # CLI reuses those layers rather than rebuilding the image. The image # contents are already covered by the smoke test, so this asserts only # what the configuration adds: the CLI resolves it and applies remoteUser. - if: matrix.arch == 'amd64' uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 with: configFile: .devcontainer/sandbox-${{ matrix.image }}/devcontainer.json