diff --git a/.github/docker/Dockerfile.ci b/.github/docker/Dockerfile.ci index 23312c6f7..9941d7b1e 100644 --- a/.github/docker/Dockerfile.ci +++ b/.github/docker/Dockerfile.ci @@ -113,7 +113,8 @@ RUN bun --version && node --version && claude --version && jq --version && gh -- # if we move it out of the way and symlink back # Save node_modules + package.json snapshot for cache validation at runtime RUN mv /workspace/node_modules /opt/node_modules_cache \ - && cp /workspace/package.json /opt/node_modules_cache/.package.json + && cp /workspace/package.json /opt/node_modules_cache/.package.json \ + && cp /workspace/bun.lock /opt/node_modules_cache/.bun.lock # Claude CLI refuses --dangerously-skip-permissions as root. # Create a non-root user for eval runs (GH Actions overrides USER, so diff --git a/.github/workflows/ci-image.yml b/.github/workflows/ci-image.yml index e36092d4c..66e044297 100644 --- a/.github/workflows/ci-image.yml +++ b/.github/workflows/ci-image.yml @@ -25,6 +25,12 @@ jobs: # Copy lockfile + package.json into Docker build context - run: cp package.json bun.lock .github/docker/ + # Same content-hash tag expression as evals.yml / evals-periodic.yml. + # 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') }}" >> "$GITHUB_OUTPUT" + - uses: docker/login-action@v3 with: registry: ghcr.io @@ -36,6 +42,9 @@ jobs: 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 }} diff --git a/.github/workflows/evals-periodic.yml b/.github/workflows/evals-periodic.yml index 25fd76d01..318abf9a3 100644 --- a/.github/workflows/evals-periodic.yml +++ b/.github/workflows/evals-periodic.yml @@ -25,7 +25,9 @@ jobs: - uses: actions/checkout@v4 - id: meta - run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json', 'bun.lock') }}" >> "$GITHUB_OUTPUT" + # Keep in sync with evals.yml — key on Dockerfile + lockfile only + # (package.json's version field would bust the key on every ship). + run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'bun.lock') }}" >> "$GITHUB_OUTPUT" - uses: docker/login-action@v3 with: @@ -51,6 +53,9 @@ jobs: context: .github/docker file: .github/docker/Dockerfile.ci push: true + # Cron-triggered in the base repo only, so cache export is always safe here. + cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache + cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max tags: | ${{ steps.meta.outputs.tag }} ${{ env.IMAGE }}:latest @@ -107,7 +112,7 @@ jobs: # are on different overlay-fs layers, so cross-device hardlink fails. - name: Restore deps run: | - if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then + if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.bun.lock bun.lock >/dev/null 2>&1; then cp -r /opt/node_modules_cache node_modules else bun install diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml index 3b30271e6..6cd8d7277 100644 --- a/.github/workflows/evals.yml +++ b/.github/workflows/evals.yml @@ -25,7 +25,12 @@ jobs: - uses: actions/checkout@v4 - id: meta - run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json', 'bun.lock') }}" >> "$GITHUB_OUTPUT" + # Key on Dockerfile + lockfile only. package.json is deliberately NOT + # hashed: its version field changes on every ship (60/60 recent commits), + # which rebuilt the image each time for a dependency set that only + # bun.lock determines. A stale baked package.json is harmless — checkout + # overwrites /workspace and node_modules comes from the lockfile. + run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'bun.lock') }}" >> "$GITHUB_OUTPUT" - uses: docker/login-action@v3 with: @@ -55,6 +60,10 @@ jobs: context: .github/docker file: .github/docker/Dockerfile.ci push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + # Registry layer cache: reads are safe everywhere; the export is gated + # to same-repo runs because a fork PR's token can't write GHCR. + cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache + cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}:buildcache,mode=max', env.IMAGE) || '' }} tags: | ${{ steps.meta.outputs.tag }} ${{ env.IMAGE }}:latest @@ -142,7 +151,7 @@ jobs: # vastly cheaper than rerunning `bun install` (network + resolution). - name: Restore deps run: | - if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then + if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.bun.lock bun.lock >/dev/null 2>&1; then cp -r /opt/node_modules_cache node_modules else bun install