Skip to content
66 changes: 50 additions & 16 deletions .github/actions/setup-bazel/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ inputs:
description: Whether master jobs may save Bazelisk and newly fetched dependencies.
default: "true"
repository-cache-key:
description: Exact dependency snapshot to restore without prefix fallback.
description: Exact dependency snapshot to restore without a workload fallback.
default: ""
verify-launcher-cache:
description: Restore only the complete launcher cache for an offline verification.
default: "false"
windows-redirect-temp:
description: Put Windows test temporary files on the runner workspace drive.
default: "false"
Expand All @@ -52,12 +55,15 @@ outputs:
repository-cache-key:
description: Fresh save key; empty when this job cannot publish dependencies.
value: ${{ github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && inputs.cache-save == 'true' && steps.repository-cache.outputs.cache-primary-key || '' }}
repository-cache-restored-key:
description: Snapshot restored before the build.
value: ${{ steps.repository-cache.outputs.cache-matched-key }}
repository-cache-entries:
description: Restored entry count, used to avoid uploading an unchanged cache.
value: ${{ steps.repository-cache-state.outputs.entries }}
launcher-cache-key:
description: Immutable platform and manifest key for a complete launcher archive.
value: ${{ steps.launcher-cache.outputs.cache-primary-key }}
launcher-cache-hit:
description: Whether the exact complete launcher archive was restored.
value: ${{ steps.launcher-cache.outputs.cache-hit }}

runs:
using: composite
Expand Down Expand Up @@ -148,6 +154,10 @@ runs:
shell: bash
env:
CACHE_NAME: ${{ inputs.cache-key }}
WORKFLOW_NAME: ${{ github.workflow }}
JOB_NAME: ${{ github.job }}
MANIFEST: ${{ hashFiles('MODULE.bazel', 'Cargo.lock', 'pnpm-lock.yaml', '.bazelversion') }}
LAUNCHER_MANIFEST: ${{ hashFiles('MODULE.bazel', 'Cargo.lock', 'pnpm-lock.yaml', '.bazelversion', '.bazelrc', 'scripts/bazel/downloader.cfg', '.github/workflows/dependency-cache.yml') }}
EXTRA_RESTORE_KEYS: ${{ inputs.extra-restore-keys }}
run: |
node <<'JS'
Expand All @@ -162,17 +172,30 @@ runs:
const repositoryPath = windowsRoot
? `${windowsRoot}b-repo`
: `${os.homedir().replace(/\\/g, '/')}/.cache/bazel-repo`;
const repositoryPrefix = `setup-bazel-1-${process.platform}-${os.arch()}-repository-`;
const platform = `setup-bazel-1-${process.platform}-${os.arch()}`;
const { WORKFLOW_NAME, JOB_NAME, CACHE_NAME, MANIFEST } = process.env;
const identity = [WORKFLOW_NAME, JOB_NAME, CACHE_NAME].map(encodeURIComponent).join(':');
const workload = `${platform}-deps:${identity}:`;
const launcher = `${platform}-launcher-helpers-${process.env.LAUNCHER_MANIFEST}`;
const names = [process.env.CACHE_NAME, ...(process.env.EXTRA_RESTORE_KEYS || '')
.split(',')
.map((name) => name.trim())
.filter(Boolean)];
const prefixes = names.map((name) =>
`setup-bazel-1-${process.platform}-${os.arch()}-disk-${name}-`);
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`repository-path=${repositoryPath}\nrepository-prefix=${repositoryPrefix}\npath=${path}\noutput-base=${windowsRoot ? `${windowsRoot}b` : ''}\nprefix=${prefixes[0]}\nrestore-keys<<EOF\n${prefixes.join('\n')}\nEOF\n`,
);
const outputs = [
`repository-path=${repositoryPath}`,
`workload=${workload}`,
`workload-manifest=${workload}${MANIFEST}-`,
`launcher=${launcher}`,
`path=${path}`,
`output-base=${windowsRoot ? `${windowsRoot}b` : ''}`,
`prefix=${prefixes[0]}`,
'restore-keys<<EOF',
...prefixes,
'EOF',
];
fs.appendFileSync(process.env.GITHUB_OUTPUT, outputs.join('\n') + '\n');
JS

- name: Preserve the runner network environment for Tailscale
Expand Down Expand Up @@ -273,18 +296,29 @@ runs:
# under its repository name twice; keep the output base short.
output-base: ${{ steps.disk-cache-config.outputs.output-base }}

# Restore by manifest, then platform. Publishing uses a fresh key only
# when the caller fetched new blobs, so a partial snapshot can grow.
- name: Restore dependency cache
# A partial dependency snapshot can seed only its own workload.
- name: Restore workload dependency cache
id: repository-cache
if: inputs.repository-cache-key == '' && inputs.verify-launcher-cache != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.disk-cache-config.outputs.repository-path }}
key: ${{ inputs.repository-cache-key || format('{0}{1}-run-{2}-{3}-{4}-{5}', steps.disk-cache-config.outputs.repository-prefix, hashFiles('MODULE.bazel', 'Cargo.lock', 'pnpm-lock.yaml', '.bazelversion'), github.run_id, github.run_attempt, github.job, inputs.cache-key) }}
fail-on-cache-miss: ${{ inputs.repository-cache-key != '' }}
key: ${{ steps.disk-cache-config.outputs.workload-manifest }}run-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ inputs.repository-cache-key == '' && format('{0}{1}-', steps.disk-cache-config.outputs.repository-prefix, hashFiles('MODULE.bazel', 'Cargo.lock', 'pnpm-lock.yaml', '.bazelversion')) || '' }}
${{ inputs.repository-cache-key == '' && steps.disk-cache-config.outputs.repository-prefix || '' }}
${{ steps.disk-cache-config.outputs.workload-manifest }}
${{ steps.disk-cache-config.outputs.workload }}

# The hosted Ubuntu warmer publishes all launcher targets as one immutable
# snapshot. Extract it into the checksum-addressed cache after the workload
# snapshot, before Bazel runs, so a partial workload cannot shadow it.
- name: Restore exact dependency snapshot
id: launcher-cache
if: runner.os == 'Linux' || inputs.repository-cache-key != ''
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.disk-cache-config.outputs.repository-path }}
key: ${{ inputs.repository-cache-key || steps.disk-cache-config.outputs.launcher }}
fail-on-cache-miss: ${{ inputs.repository-cache-key != '' || inputs.verify-launcher-cache == 'true' }}

- name: Measure restored dependency cache
id: repository-cache-state
Expand Down
11 changes: 2 additions & 9 deletions .github/actions/setup-bazel/save/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,12 @@ inputs:
restored-entries:
description: Entry count returned by setup-bazel before the build.
required: true
restored-key:
description: Previously restored snapshot, returned when no new files were fetched.
default: ""
outputs:
key:
description: Published or already complete dependency snapshot.
value: ${{ steps.changed.outputs.changed == 'true' && inputs.key || inputs.restored-key }}
runs:
using: composite
steps:
- name: Check for newly fetched dependencies
id: changed
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && inputs.key != ''
if: always() && !cancelled() && github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && inputs.key != '' && inputs.restored-entries != ''
shell: bash
env:
CACHE_PATH: ${{ inputs.path }}
Expand All @@ -37,7 +30,7 @@ runs:
fs.appendFileSync(process.env.GITHUB_OUTPUT, `changed=${changed}\n`);
JS
- uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: steps.changed.outputs.changed == 'true'
if: always() && !cancelled() && steps.changed.outputs.changed == 'true'
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
23 changes: 17 additions & 6 deletions .github/workflows/dependency-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
helpers:
name: Cache launcher helpers
outputs:
cache-key: ${{ steps.saved.outputs.key }}
cache-key: ${{ steps.bazel.outputs.launcher-cache-key }}
runs-on: ubuntu-latest
timeout-minutes: 15
env:
Expand All @@ -42,9 +42,11 @@ jobs:
with:
cache-key: dependency-helpers
cache-save: ${{ inputs.offline != true }}
verify-launcher-cache: ${{ inputs.offline == true }}

- &resolve-helpers
name: Resolve launcher helpers for every supported platform
id: fetch
run: |
set -euo pipefail
targets=()
Expand All @@ -59,20 +61,28 @@ jobs:
fi
bazel build "${options[@]}" "${targets[@]}"

- name: Save newly fetched Bazel dependencies
id: saved
if: always() && !cancelled() && steps.bazel.outputs.repository-cache-key != ''
# Only a successful build of every target establishes a complete archive.
# A warm workload cache can already contain every blob on the
# first run; the immutable helper key must still be published then.
- name: Publish complete launcher helpers
if: success() && github.ref == 'refs/heads/master' && inputs.offline != true && steps.bazel.outputs.launcher-cache-hit != 'true' && steps.bazel.outputs.launcher-cache-key != ''
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.bazel.outputs.repository-cache-path }}
key: ${{ steps.bazel.outputs.launcher-cache-key }}

- name: Preserve partially fetched launcher dependencies
if: failure() && !cancelled() && steps.fetch.outcome == 'failure' && steps.bazel.outputs.repository-cache-key != ''
uses: ./.github/actions/setup-bazel/save
with:
path: ${{ steps.bazel.outputs.repository-cache-path }}
key: ${{ steps.bazel.outputs.repository-cache-key }}
restored-entries: ${{ steps.bazel.outputs.repository-cache-entries }}
restored-key: ${{ steps.bazel.outputs.repository-cache-restored-key }}

verify:
name: Verify restored dependency cache
needs: helpers
if: inputs.offline != true
if: inputs.offline != true && github.ref == 'refs/heads/master'
permissions:
contents: read
actions: write
Expand All @@ -87,6 +97,7 @@ jobs:
cache-key: dependency-helpers-verification
repository-cache-key: ${{ needs.helpers.outputs.cache-key }}
cache-save: "false"
verify-launcher-cache: "true"
- *resolve-helpers

- name: Retire incomplete legacy Linux cache snapshots
Expand Down
Loading