name: Build CI Image on: # Weekly self-heal (Monday 4am UTC) — deliberately 2h BEFORE # evals-periodic's 6am cron so the weekly eval run finds the image instead # of racing a half-pushed tag. With the claude CLI pinned in Dockerfile.ci # (v1.77+), this cron no longer pulls CLI updates: when the content-hash # tag already exists it's a ~30s no-op, and it only rebuilds if the tag # was somehow lost. CLI bumps happen by editing the Dockerfile pin in a PR # that runs the PTY gate against the new TUI. schedule: - cron: '0 4 * * 1' # Rebuild on Dockerfile or lockfile changes. package.json is deliberately # NOT a trigger: the tag hash below excludes it (its version field bumps on # every ship), so a package.json-triggered run rebuilt and re-pushed the # IDENTICAL tag on every merge to main (~2m26s each for zero content change). push: branches: [main] paths: - '.github/docker/Dockerfile.ci' - 'bun.lock' - 'patches/**' # Manual trigger workflow_dispatch: # Two rapid main pushes must not race pushing the same :latest/:buildcache # tags; newest wins. concurrency: group: ci-image-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubicloud-standard-8 timeout-minutes: 30 permissions: contents: read packages: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # Copy lockfile + package.json into Docker build context - run: cp package.json bun.lock .github/docker/ && cp -R patches .github/docker/patches # Same content-hash tag expression as evals.yml / evals-periodic.yml # (byte-identity pinned by test/ci-image-tag-binding.test.ts). This is # the tag the eval matrix looks up first — without pushing it here, the # weekly/main prebuild never warms the cache that matters. - id: meta run: echo "tag=ghcr.io/${{ github.repository }}/ci:${{ hashFiles('.github/docker/Dockerfile.ci', 'bun.lock', 'patches/**') }}" >> "$GITHUB_OUTPUT" - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Skip the ~2.5min build when the content-hash tag already exists # (mirrors evals.yml's check). The weekly cron still refreshes :latest # via a full run when the tag is genuinely new. - name: Check if image exists id: check run: | if docker manifest inspect ${{ steps.meta.outputs.tag }} > /dev/null 2>&1; then echo "exists=true" >> "$GITHUB_OUTPUT" else echo "exists=false" >> "$GITHUB_OUTPUT" fi # Registry cache export needs a docker-container builder — the default # `docker` driver hard-errors on cache-to. - if: steps.check.outputs.exists == 'false' uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - if: steps.check.outputs.exists == 'false' uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: .github/docker file: .github/docker/Dockerfile.ci push: true cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/ci:buildcache cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/ci:buildcache,mode=max tags: | ${{ steps.meta.outputs.tag }} ghcr.io/${{ github.repository }}/ci:latest ghcr.io/${{ github.repository }}/ci:${{ github.sha }}