Skip to content

Commit e621348

Browse files
marcelolynchReemMelamed
authored andcommitted
ci(cache): consolidate cache fetching into one action, warmed from master (leanprover-community#40678)
Consolidates the build job's cache fetch into a single `get-cache` action. It used to be a 3-step sequence (a cold-cache probe, a landrun verify, then a parent-commit warmup plus the fetch); now one action runs a single HEAD-scoped `cache get`, optionally warmed from a free master snapshot. Two things come out of that: - HEAD-scoped fetch: dropping the parent-commit warmup (which checked out other SHAs and read their cache) means a run reads only its own scope plus the trusted master snapshot, matching the per-commit scope from leanprover-community#40035. - Warmed from master: master's `.ltar` set is otherwise re-read from the paid Azure cache on every cold runner. The master `push` build in `build.yml` now publishes it once as a free `cache-snapshot` artifact, pruned to that commit's set and reusing what is already on disk, so it adds no Azure read. The action seeds `~/.cache/mathlib` from the snapshot at the PR's merge-base (its unchanged files hash identically there), else the newest one at-or-before it, else the latest, before fetching, so the unchanged-from-master oleans come from GitHub rather than Azure. The snapshot download is trust-pinned to master push runs and fail-safe (any problem falls back to a plain Azure `cache get`), and fork-safe because `pull_request_target` runs the base branch's workflow and actions rather than the PR's. Each run logs a warm/cold count. `bors.yml` and `ci_dev.yml` gain read-only `actions: read`. Snapshot retention is 14 days, and `.ltar` are input-hash-keyed, so a stale base falls back gracefully.
1 parent 83df3b7 commit e621348

6 files changed

Lines changed: 138 additions & 94 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Get this commit's oleans, in two phases:
2+
# 1. Warm the cache from the `cache-snapshot` GitHub artifact (canonical repo only).
3+
# 2. Fetch this commit's oleans from the remote cache with the trusted master-built binary.
4+
# The fetch is HEAD-scoped (reads only this commit's own cache scope). The warm is fail-safe
5+
# (any failure → just the remote fetch) and its source is hardcoded, so nothing can redirect
6+
# the download off the trusted master pipeline.
7+
name: Get cache
8+
description: Get this commit's oleans into the local cache.
9+
inputs:
10+
working_directory:
11+
description: The lake project to fetch the cache for (e.g. the checked-out PR branch).
12+
required: true
13+
cache_bin:
14+
description: Path to the trusted `cache` binary, relative to `working_directory`.
15+
required: true
16+
runs:
17+
using: composite
18+
steps:
19+
# 1. Warm cache from the GitHub artifact. Resolve which snapshot to use: the one built
20+
# at this commit's merge-base with master (its unchanged files hash identically
21+
# there), else the newest still-retained one at-or-before it, else the latest (a
22+
# merge-base older than retention, or any failure, lands here). Canonical repo only.
23+
- name: Resolve cache snapshot
24+
id: resolve
25+
if: ${{ github.repository == 'leanprover-community/mathlib4' }}
26+
shell: bash
27+
env:
28+
GH_TOKEN: ${{ github.token }}
29+
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
30+
run: |
31+
set -uo pipefail
32+
runs="repos/leanprover-community/mathlib4/actions/workflows/build.yml/runs?branch=master&event=push&status=success"
33+
34+
# Each helper prints a matching successful master-push run-id, or empty.
35+
run_at() { gh api "${runs}&head_sha=$1" --jq '.workflow_runs[0].id // empty' 2>/dev/null || true; }
36+
latest_run() { gh api "${runs}&per_page=1" --jq '.workflow_runs[0].id // empty' 2>/dev/null || true; }
37+
# newest run created at-or-before date $1, but not older than cutoff $2
38+
newest_before() {
39+
gh api "${runs}&per_page=100" 2>/dev/null | jq -r --arg d "$1" --arg c "$2" \
40+
'[.workflow_runs[] | select(.created_at <= $d and ($c == "" or .created_at >= $c))][0].id // empty' \
41+
2>/dev/null || true
42+
}
43+
44+
# This PR's merge-base with master (+ its commit date), and the cutoff below which
45+
# snapshots have expired (retention ~14d).
46+
mb_info=$(gh api "repos/leanprover-community/mathlib4/compare/master...${HEAD_SHA}" \
47+
--jq '.merge_base_commit | "\(.sha) \(.commit.committer.date)"' 2>/dev/null || true)
48+
read -r mb mb_date <<< "${mb_info}"
49+
cutoff=$(date -u -d '13 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)
50+
51+
# Prefer the merge-base's snapshot (while still retained), then the newest one
52+
# before it, then the latest of all.
53+
run_id=""
54+
if [[ -n "${mb}" && ( -z "${cutoff}" || "${mb_date}" > "${cutoff}" ) ]]; then
55+
run_id=$(run_at "${mb}")
56+
[[ -z "${run_id}" ]] && run_id=$(newest_before "${mb_date}" "${cutoff}")
57+
fi
58+
[[ -z "${run_id}" ]] && run_id=$(latest_run)
59+
60+
echo "Resolved cache-snapshot run_id: '${run_id}' (merge-base: ${mb:-unknown})"
61+
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
62+
63+
- name: Warm cache from GitHub artifact
64+
if: ${{ steps.resolve.outputs.run_id != '' }}
65+
continue-on-error: true # fail-safe: fall back to the remote fetch (step 2)
66+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
67+
with:
68+
name: cache-snapshot
69+
path: /home/lean/.cache/mathlib
70+
repository: leanprover-community/mathlib4
71+
run-id: ${{ steps.resolve.outputs.run_id }}
72+
github-token: ${{ github.token }}
73+
74+
# 2. Fetch this commit's oleans from the remote cache with the trusted `cache` binary
75+
# (outside landrun). Runs on every repo; the warm above just gives the canonical
76+
# repo a local head start. HEAD-scoped: reads only this commit's own cache scope.
77+
- name: Fetch cache from remote
78+
shell: bash
79+
env:
80+
WORKDIR: ${{ inputs.working_directory }}
81+
CACHE_BIN: ${{ inputs.cache_bin }}
82+
CACHE_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
83+
run: |
84+
set -eo pipefail
85+
cd "${WORKDIR}"
86+
rm -rf .lake/build/lib/lean/Mathlib
87+
log="${RUNNER_TEMP:-/tmp}/cache-get.log"
88+
# --repo so fork PRs also read their own repo-namespaced cache (master is read flat,
89+
# so this still gets the master bulk); for in-repo runs it resolves to the same repo.
90+
"${CACHE_BIN}" --repo="${CACHE_REPO}" get 2>&1 | tee "${log}"
91+
# Warmth = how much the snapshot covered HEAD: files already cached locally
92+
# (just decompressed) vs downloaded from Azure. Parsed best-effort from the log.
93+
warm=$(grep -oE 'Decompressing [0-9]+ already-cached' "${log}" | grep -oE '[0-9]+' | head -1 || true)
94+
cold=$(grep -oE 'Attempting to download [0-9]+' "${log}" | grep -oE '[0-9]+' | head -1 || true)
95+
echo "Cache warmth: ${warm:-0} already-cached (warm) / ${cold:-0} downloaded from Azure (cold)"

.github/workflows/bors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ concurrency:
1616
permissions:
1717
contents: read
1818
id-token: write
19+
actions: read # download master artifacts (tools-bin, cache-snapshot)
1920
pull-requests: write # Only allow PR comments/labels
2021
# All other permissions are implicitly 'none'
2122

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,7 @@ jobs:
7070
&& 'cache-upload-forks'
7171
|| ''
7272
}}
73+
# the single producer: the master `push` build
74+
publish_cache: ${{ github.repository == 'leanprover-community/mathlib4' && github.event_name == 'push' && github.ref == 'refs/heads/master' }}
7375
runs_on: pr
7476
secrets: inherit

.github/workflows/build_fork.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ on:
99
- 'staging*.tmp'
1010
- 'nolints'
1111
paths-ignore:
12-
# pull_request_target uses the workflow from the target branch:
13-
# PR changes under this directory won't affect this run, so
14-
# running it is just wasteful
15-
- '.github/workflows/**'
12+
# pull_request_target runs the workflow and its actions from the target branch, not
13+
# the PR, so PR changes under these dirs can't affect the run — triggering is wasteful.
14+
- '.github/**'
1615

1716
concurrency:
1817
# label each workflow run; only the latest with each label will run

.github/workflows/build_template.yml

Lines changed: 36 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ on:
3333
# federated credential is scoped to; it must match the writer `cache_application_id`.
3434
type: string
3535
required: true
36+
publish_cache:
37+
# On a successful build, publish this run's `.ltar` set as `cache-snapshot`.
38+
# Set by callers ONLY for the master-`push` build — the single producer.
39+
type: boolean
40+
required: false
41+
default: false
3642

3743
env:
3844
# Disable Lake's automatic fetching of cloud builds.
@@ -51,7 +57,6 @@ jobs:
5157
archive-outcome: ${{ steps.archive.outcome }}
5258
counterexamples-outcome: ${{ steps.counterexamples.outcome }}
5359
cache-staging-has-files: ${{ steps.cache_staging_check.outputs.has_files }}
54-
get-cache-outcome: ${{ steps.get.outcome }}
5560
lint-outcome: ${{ steps.lint.outcome }}
5661
mk_all-outcome: ${{ steps.mk_all.outcome }}
5762
noisy-outcome: ${{ steps.noisy.outcome }}
@@ -313,95 +318,12 @@ jobs:
313318
echo "✅ All inputRevs in lake-manifest.json are valid"
314319
fi
315320
316-
- name: get cache (1/3 - setup and initial fetch)
317-
id: get_cache_part1_setup
318-
shell: bash # only runs `cache get` from `tools-branch`, so doesn't need to be inside landrun
319-
run: |
320-
cd pr-branch
321-
echo "Removing old Mathlib build directories prior to cache fetch..."
322-
rm -rf .lake/build/lib/lean/Mathlib
323-
324-
# Fail quickly if the cache is completely cold, by checking for Mathlib.Init
325-
echo "Attempting to fetch olean for Mathlib/Init.lean from cache..."
326-
../tools-branch/.lake/build/bin/cache get Mathlib/Init.lean
327-
328-
- name: get cache (2/3 - test Mathlib.Init cache)
329-
id: get_cache_part2_test
330-
continue-on-error: true # Allow workflow to proceed to Part 3 to check outcome
331-
# This step uses the job's default shell, which is landrun-wrapped bash
332-
run: |
333-
cd pr-branch
334-
335-
echo "Attempting: lake build --no-build -v Mathlib.Init (this runs under landrun)"
336-
lake build --no-build -v Mathlib.Init
337-
338-
- name: get cache (3/3 - finalize cache operation)
339-
id: get
340-
shell: bash # only runs git and `cache get` from `tools-branch`, so doesn't need to be inside landrun
341-
env:
342-
BEFORE_SHA: ${{ github.event.before || '' }}
343-
BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
344-
CACHE_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
345-
run: |
346-
cd pr-branch
347-
if [[ "${{ steps.get_cache_part2_test.outcome }}" != "success" ]]; then
348-
echo "WARNING: 'lake build --no-build -v Mathlib.Init' failed."
349-
echo "No cache for 'Mathlib.Init' available or it could not be prepared."
350-
exit 0
351-
fi
352-
353-
ORIG_SHA="$(git rev-parse HEAD)"
354-
PREV_SHA=""
355-
PREV_SHA_SOURCE=""
356-
357-
# When a ref is newly created, github.event.before can be all-zero.
358-
if [[ "$BEFORE_SHA" =~ ^0{40}$ ]]; then
359-
BEFORE_SHA=""
360-
fi
361-
362-
if [[ -n "$BEFORE_SHA" ]]; then
363-
PREV_SHA="$BEFORE_SHA"
364-
PREV_SHA_SOURCE="github.event.before"
365-
elif [[ -n "$BASE_SHA" ]]; then
366-
PREV_SHA="$BASE_SHA"
367-
PREV_SHA_SOURCE="pull_request.base.sha"
368-
else
369-
PREV_SHA="$(git rev-parse --verify --quiet HEAD^ || true)"
370-
if [[ -n "$PREV_SHA" ]]; then
371-
PREV_SHA_SOURCE="HEAD^"
372-
fi
373-
fi
374-
375-
if [[ -n "$PREV_SHA" ]]; then
376-
# cf. https://leanprover.zulipchat.com/#narrow/channel/287929-mathlib4/topic/Mathlib.20has.20moved.20to.20the.20new.20module.20system/near/563452000
377-
echo "Warming up cache using previous commit: $PREV_SHA (source: $PREV_SHA_SOURCE)"
378-
if git cat-file -e "$PREV_SHA^{commit}" 2>/dev/null || git fetch --no-tags --depth=1 origin "$PREV_SHA"; then
379-
# Skip warmup if the previous commit uses a different toolchain
380-
PREV_TOOLCHAIN=$(git show "$PREV_SHA:lean-toolchain" 2>/dev/null || true)
381-
if [[ "$PREV_TOOLCHAIN" != "$(cat lean-toolchain)" ]]; then
382-
echo "Previous commit $PREV_SHA uses a different toolchain ($PREV_TOOLCHAIN); skipping warmup."
383-
else
384-
git checkout "$PREV_SHA"
385-
../tools-branch/.lake/build/bin/cache get
386-
# Run again with --repo, to ensure we actually get the oleans.
387-
../tools-branch/.lake/build/bin/cache --repo="$CACHE_REPO" get
388-
389-
echo "Switching back to branch head"
390-
git checkout "$ORIG_SHA"
391-
fi
392-
else
393-
echo "Could not fetch $PREV_SHA; skipping parent warmup cache fetch."
394-
fi
395-
else
396-
echo "No previous commit candidate found; skipping parent warmup cache fetch."
397-
fi
398-
399-
echo "Fetching all remaining cache..."
400-
401-
../tools-branch/.lake/build/bin/cache get
402-
403-
# Run again with --repo, to ensure we actually get the oleans.
404-
../tools-branch/.lake/build/bin/cache --repo="$CACHE_REPO" get
321+
# Get this commit's oleans into the local cache — see the action for the steps.
322+
- name: Get cache
323+
uses: ./workflow-actions/.github/actions/get-cache
324+
with:
325+
working_directory: pr-branch
326+
cache_bin: ../tools-branch/.lake/build/bin/cache
405327

406328
- name: update {Mathlib, Tactic, Counterexamples, Archive}.lean
407329
id: mk_all
@@ -522,6 +444,30 @@ jobs:
522444
name: cache-staging
523445
path: cache-staging/
524446

447+
# Prune to this commit's `.ltar` set so the published snapshot is exactly master's
448+
# current cache (the local dir also holds the previous snapshot it warmed from).
449+
- name: prune local cache to this commit's set
450+
if: ${{ inputs.publish_cache && steps.build.outcome == 'success' }}
451+
continue-on-error: true # best-effort; never fail the build
452+
shell: bash # runs the trusted tools-branch `cache` binary, so no landrun needed
453+
run: |
454+
cd pr-branch
455+
../tools-branch/.lake/build/bin/cache clean
456+
457+
# Publish this run's pruned `.ltar` set as `cache-snapshot` for other runs to warm
458+
# from. Already on disk (no Azure egress) and already zstd-compressed (skip
459+
# recompression); retention covers how far back a PR's merge-base can be matched.
460+
- name: upload cache snapshot warming artifact
461+
if: ${{ inputs.publish_cache && steps.build.outcome == 'success' }}
462+
continue-on-error: true # best-effort; never fail the build
463+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
464+
with:
465+
name: cache-snapshot
466+
path: /home/lean/.cache/mathlib/*.ltar
467+
compression-level: 0
468+
retention-days: 14
469+
if-no-files-found: warn
470+
525471
- name: Check if building Archive or Counterexamples failed
526472
if: steps.archive.outcome == 'failure' || steps.counterexamples.outcome == 'failure'
527473
run: |

.github/workflows/ci_dev.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ on:
2828
permissions:
2929
contents: read
3030
id-token: write
31+
actions: read # read-only: download master artifacts (tools-bin, cache-snapshot)
3132
# By default let's remove this permission (which is present in the other build pipelines)
3233
# from the CI experimentation runs to avoid unwitting side effects
3334
# pull-requests: write

0 commit comments

Comments
 (0)