name: Devcontainer Smoke # Smoke-tests .devcontainer/ whenever it changes. Two things happen here. # First, unit tests cover host->container config transforms and actual Husky # pre-commit enforcement in temporary repositories. Second, the image is built # through the standard @devcontainers/cli path. That CLI reads build.args from # devcontainer.json, so the version pin there stays the single source of truth. on: push: branches: [main] paths: - '.devcontainer/**' - '.github/workflows/ci-devcontainer.yml' - 'gitnexus-web/package-lock.json' pull_request: paths: - '.devcontainer/**' - '.github/workflows/ci-devcontainer.yml' - 'gitnexus-web/package-lock.json' permissions: contents: read # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Grouped per branch or tag. Cancel a PR run when a newer one replaces it. # Never cancel a push-to-main run. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: config-transforms: name: Config-transform unit tests runs-on: ubuntu-latest timeout-minutes: 5 steps: # persist-credentials: false — this job only reads (tests and syntax # checks) and never pushes. The setting keeps GITHUB_TOKEN out of # .git/config, which zizmor flags as the "artipacked" issue. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 # Hook tests install real Husky in temporary repositories. - name: Install hook test tooling run: | npm ci --no-audit --no-fund node .devcontainer/verify-git-hooks.cjs - name: Unit-test devcontainer setup and hooks run: node --test .devcontainer/*.test.cjs - name: Syntax-check the lifecycle shell scripts run: | bash -n .devcontainer/install-deps.sh bash -n .devcontainer/post-create.sh bash -n .devcontainer/seed-codex.sh bash -n .devcontainer/tune-9p.sh build: name: Build devcontainer image (${{ matrix.profile }}) runs-on: ubuntu-latest timeout-minutes: 45 strategy: fail-fast: false matrix: profile: [default, ui] steps: # persist-credentials: false — this is a read-only build smoke that # never pushes. The setting keeps GITHUB_TOKEN out of .git/config, # which zizmor flags as the "artipacked" issue. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 - name: Generate UI profile if: matrix.profile == 'ui' run: node .devcontainer/profile.cjs ui # Builds the image the same way a developer's "Reopen in Container" does. # @devcontainers/cli reads devcontainer.json (jsonc format), resolves # build.args (including BUN_VERSION), and runs the Dockerfile. This smoke # catches Dockerfile regressions, including native CLI installation. # The lifecycle hooks (post-create.sh) do not # run here. They need the host config mounts, and CI has none. # # ARCH COVERAGE: this runs on an x64 runner with no --platform or QEMU, so # it builds only amd64. Native CLI installers also support arm64, but that # path is not built here. Add QEMU and a linux/arm64 matrix leg if needed. # # The @devcontainers/cli version is pinned on purpose. A bare # `npx --yes @devcontainers/cli` would resolve @latest at run time. A # breaking or malicious publish could then change CI behavior, or change # how devcontainer.json is read, with no diff to show for it. Bump this pin # deliberately, alongside the Dockerfile and devcontainer.json pins. # # @devcontainers/cli wraps the Dockerfile with `# syntax=docker/dockerfile:1`, # which BuildKit resolves from Docker Hub. Hub blips surface as # `DeadlineExceeded` / `i/o timeout` on the syntax frontend (see run # 26797815133). Build retry (2 attempts, 45s backoff) matches # `.github/actions/docker-build-push-retry` (docker/build-push-action#1422). # Pre-pull of docker/dockerfile:1 is extra hardening; best-effort so the # build retry still runs if Hub is flaky only during pull. - name: Pre-pull BuildKit Dockerfile frontend (retry) continue-on-error: true run: | set -euo pipefail img="docker/dockerfile:1" for attempt in 1 2 3; do if docker pull "$img"; then exit 0 fi echo "::warning::docker pull ${img} attempt ${attempt} failed" if [ "$attempt" -lt 3 ]; then sleep $((attempt * 15)) fi done echo "::warning::failed to pre-pull ${img} after 3 attempts; continuing — build step may still succeed" exit 1 - name: Build devcontainer via @devcontainers/cli env: DEVCONTAINER_CONFIG: ${{ matrix.profile == 'ui' && '.devcontainer/generated/devcontainer.json' || '.devcontainer/devcontainer.json' }} run: | set -euo pipefail for attempt in 1 2; do if npx --yes @devcontainers/cli@0.87.0 build --workspace-folder . --config "$DEVCONTAINER_CONFIG" --frozen-lockfile; then if [ "$attempt" -eq 2 ]; then echo "::notice::devcontainer build retry succeeded (attempt 2); investigate if this recurs across runs." fi exit 0 fi if [ "$attempt" -eq 2 ]; then echo "::error::devcontainer build failed after 2 attempts" exit 1 fi echo "::warning::devcontainer build attempt ${attempt} failed; retrying in 45s…" sleep 45 done