Skip to content

Commit 4c7705e

Browse files
authored
Update CI (#201)
2 parents 3835988 + 9741706 commit 4c7705e

1 file changed

Lines changed: 166 additions & 23 deletions

File tree

‎.github/workflows/blueprint.yml‎

Lines changed: 166 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ concurrency:
1616

1717
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
1818
permissions:
19-
contents: read # Read access to repository contents
19+
contents: write # Push the provenance ledger back to the branch (see the step that does)
2020
pages: write # Write access to GitHub Pages
2121
id-token: write # Write access to ID tokens
2222
issues: write # Write access to issues
2323
pull-requests: write # Write access to pull requests
24-
actions: write # Write access to GitHub Actions
24+
actions: read # Read the previous run's artifact, for the referee revision diff
2525

2626
env:
2727
REFEREE_REPO: LeanMachineLearning/exposition
@@ -32,18 +32,47 @@ env:
3232
# authentication and never expire, while a cross-repository artifact download needs a PAT with
3333
# `actions:read` (a workflow's own GITHUB_TOKEN cannot reach another repository's artifacts) and
3434
# is deleted after 90 days.
35+
#
36+
# Must resolve to a release that has `collect --hashes` and the `provenance` subcommand, both of
37+
# which this workflow now passes. v0.1.3 is the first that does; anything earlier fails with
38+
# "Unknown or incomplete option". Empty resolves to the most recent release.
3539
REFEREE_VERSION: ""
3640
# Kept as `exposition` rather than renamed to `referee` along with the tool: this path is the
3741
# published URL, and existing links to it would break.
3842
REFEREE_SITE_URL: https://leanmachinelearning.org/LML/exposition
3943

44+
# The library that is exposed on the site, and the one excluded from every phase that imports the
45+
# project. Named once here because `collect`, `extract`, `highlight` and the hash export all have
46+
# to agree — a mismatch between the hashed set and the collected set makes `provenance` hard-fail.
47+
REFEREE_ROOT: LeanMachineLearning
48+
REFEREE_EXCLUDE_LIB: LMLTutorial
49+
50+
# Generated data, deliberately *outside* the working tree. `provenance` records whether the tree
51+
# was clean when it folded, via `git status --porcelain`, which counts untracked files — so
52+
# leaving these where they land would permanently stamp the ledger as built from a dirty tree and
53+
# make the site disclaim its own blame.
54+
REFEREE_DATA: /tmp/referee-data/referee-data.json
55+
REFEREE_HASHES: /tmp/referee-data/hashes.jsonl
56+
57+
# The provenance ledger, committed to *this* repository rather than kept as an artifact: it is
58+
# append-only and its whole value is that it remembers years of history, which a 90-day artifact
59+
# cannot. Each run folds the current commit into it, so its resolution is this workflow's cadence
60+
# — per commit, since this runs on every push to main.
61+
REFEREE_PROVENANCE: provenance.json
62+
63+
# `semantic_hash` supplies the rename-invariant hashes the ledger and the revision diff are keyed
64+
# on. Leave the ref empty for the default branch; pin a tag or branch once the ledger matters,
65+
# since a change in how it hashes reads as every declaration changing meaning at once.
66+
SEMANTIC_HASH_REPO: https://github.com/mathlib-initiative/semantic_hash
67+
SEMANTIC_HASH_REF: ""
68+
4069
jobs:
4170
build_project:
4271
runs-on: ubuntu-latest
4372
name: Build project
4473
steps:
4574
- name: Cleanup to free disk space
46-
uses: jlumbroso/free-disk-space@main
75+
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.0
4776
with:
4877
tool-cache: false
4978
android: true # saves approximately 12 GB
@@ -54,8 +83,11 @@ jobs:
5483
swap-storage: false
5584

5685
- name: Checkout project
57-
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
86+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
5887
with:
88+
# Load-bearing for provenance, not just a convenience: the ledger's edit half is
89+
# `git blame`, and at the default `fetch-depth: 1` blame attributes the entire library to
90+
# the single fetched commit.
5991
fetch-depth: 0 # Fetch all history for all branches and tags
6092

6193
- name: Build and lint project
@@ -65,17 +97,13 @@ jobs:
6597
lint: true
6698
mk_all-check: true
6799

68-
- name: Build Verso Documentation
69-
run: |
70-
./scripts/build_docs.sh
71-
72100
- name: Download referee binary
73101
if: github.event_name == 'push'
74102
env:
75103
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76104
run: |
77105
set -euo pipefail
78-
mkdir -p /tmp/referee
106+
mkdir -p /tmp/referee "$(dirname "$REFEREE_DATA")"
79107
gh release download ${REFEREE_VERSION:+"$REFEREE_VERSION"} \
80108
-R "$REFEREE_REPO" \
81109
--pattern 'referee-linux-x86_64-*.tar.gz' \
@@ -97,9 +125,98 @@ jobs:
97125
exit 1
98126
fi
99127
128+
# Built here rather than taken as a Lake dependency, for the same reason referee is downloaded
129+
# rather than built: it loads this project's environment and refuses to run unless its own
130+
# sysroot matches, so it has to be compiled against *this* toolchain, not the one it pins.
131+
# Overwriting the pin is the whole patch. No dependencies beyond core, so this is ~1-2 min and
132+
# the toolchain is already on the runner from the build step.
133+
- name: Build semantic_hash
134+
if: github.event_name == 'push'
135+
run: |
136+
set -euo pipefail
137+
git clone --depth 1 ${SEMANTIC_HASH_REF:+--branch "$SEMANTIC_HASH_REF"} \
138+
"$SEMANTIC_HASH_REPO" /tmp/semantic_hash
139+
cp lean-toolchain /tmp/semantic_hash/lean-toolchain
140+
cd /tmp/semantic_hash
141+
lake build semantic_hash
142+
143+
# No `lake env`: it reads this project's LEAN_PATH itself from `--dir`. `--imports` takes the
144+
# same roots `collect` exposes — `LeanMachineLearning` alone, which is also how `LMLTutorial`
145+
# stays out of the hashed set without an exclusion flag of its own. It hashes the whole
146+
# upstream cone, which is what makes the hashes deep enough to notice a Mathlib bump changing
147+
# a statement underneath us, and `collect` keeps only the exposed declarations' entries.
148+
- name: Export semantic hashes
149+
if: github.event_name == 'push'
150+
run: |
151+
set -euo pipefail
152+
/tmp/semantic_hash/.lake/build/bin/semantic_hash export \
153+
--dir . --imports "$REFEREE_ROOT" --output "$REFEREE_HASHES"
154+
155+
# The phase split: everything needing a Lean environment produces data, and everything after
156+
# is a pure function of that data. Collection is its own step because `provenance` has to run
157+
# between it and the rendering.
158+
- name: Collect referee data
159+
if: github.event_name == 'push'
160+
run: |
161+
set -euo pipefail
162+
lake env "$REFEREE_BIN" collect \
163+
--root "$REFEREE_ROOT" \
164+
--exclude-lib "$REFEREE_EXCLUDE_LIB" \
165+
--hashes "$REFEREE_HASHES" \
166+
--data "$REFEREE_DATA"
167+
168+
# Needs a git working tree and no Lean environment at all, so it is a phase of its own. Runs
169+
# before `extract`/`highlight`/`build_docs.sh`/docgen write into the tree, so that the
170+
# cleanliness it records is the repository's and not this job's leftovers.
171+
#
172+
# Hard-fails rather than degrading, by design: it refuses to run on data collected without
173+
# `--hashes`, because the ledger is append-only and a text-keyed one would record the mass
174+
# false change of a toolchain upgrade permanently. A failure here means the hash export missed
175+
# declarations `collect` exposes, which is worth stopping for.
176+
#
177+
# `--ref` is left off, so each revision is named by `git describe --tags --always` — a tag
178+
# where there is one, else a short sha. Pass `--ref` explicitly to fold at release cadence
179+
# instead ("changed between v0.2 and v0.3").
180+
- name: Fold this revision into the provenance ledger
181+
if: github.event_name == 'push'
182+
run: |
183+
set -euo pipefail
184+
# Dumped so that the tool's "working tree has uncommitted changes" warning, if it fires,
185+
# comes with the reason attached rather than sending someone hunting for it.
186+
echo "working tree at fold time:"; git status --porcelain
187+
"$REFEREE_BIN" provenance \
188+
--data "$REFEREE_DATA" \
189+
--provenance "$REFEREE_PROVENANCE"
190+
191+
# The ledger is only worth keeping if it survives the run that wrote it. Pushed with
192+
# GITHUB_TOKEN, which by design does not trigger another workflow run, so this cannot loop
193+
# back into itself.
194+
#
195+
# A rejected push is a warning and not a failure: the fold is idempotent per commit and the
196+
# next run folds whatever it finds, so losing a race costs one revision of resolution rather
197+
# than correctness — and it must not take the deployment down with it.
198+
- name: Commit the provenance ledger
199+
if: github.event_name == 'push'
200+
run: |
201+
set -euo pipefail
202+
if git diff --quiet -- "$REFEREE_PROVENANCE" &&
203+
[ -z "$(git ls-files --others --exclude-standard -- "$REFEREE_PROVENANCE")" ]; then
204+
echo "ledger unchanged (already folded at this commit); nothing to commit"
205+
exit 0
206+
fi
207+
git config user.name "github-actions[bot]"
208+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
209+
git add "$REFEREE_PROVENANCE"
210+
git commit -m "chore(referee): fold ${GITHUB_SHA:0:7} into the provenance ledger"
211+
git push origin "HEAD:${GITHUB_REF_NAME}" ||
212+
echo "::warning::could not push the provenance ledger; the next run will fold this \
213+
revision together with the following one"
214+
100215
# The previous run's collected data, so the site can say what changed since it. Optional in
101216
# every direction: the first run has nothing to download, a failed lookup is swallowed, and
102-
# `build-site` simply omits the Changes page when no baseline reaches it.
217+
# `build-site` simply omits the Changes page when no baseline reaches it. Complementary to the
218+
# ledger rather than replaced by it — the ledger says *when* a declaration last changed, the
219+
# baseline shows the two statements side by side.
103220
- name: Fetch previous referee data for the revision diff
104221
if: github.event_name == 'push'
105222
continue-on-error: true
@@ -113,35 +230,54 @@ jobs:
113230
[ -n "$run_id" ] || { echo "no earlier successful run; skipping the baseline"; exit 0; }
114231
gh run download "$run_id" -R "$GITHUB_REPOSITORY" \
115232
-n referee-data -D /tmp/referee-baseline
116-
echo "REFEREE_BASELINE=/tmp/referee-baseline/referee-data.json" >> "$GITHUB_ENV"
233+
# Located rather than assumed: the artifact's internal layout follows whatever path was
234+
# uploaded, and a wrong guess here would silently cost the Changes page on every run.
235+
found=$(find /tmp/referee-baseline -name '*.json' | head -1)
236+
[ -n "$found" ] || { echo "artifact holds no JSON; skipping the baseline"; exit 0; }
237+
echo "REFEREE_BASELINE=$found" >> "$GITHUB_ENV"
117238
118239
- name: Build the referee site
119240
if: github.event_name == 'push'
120241
run: |
121242
set -euo pipefail
122-
# The phase split: everything needing a Lean environment produces data, and rendering is a
123-
# pure function of that data. `--exclude-lib` applies to the three phases that import the
124-
# project, and to none of `build-site`, which reads only the JSON. `highlight-extracted`
125-
# is deliberately skipped — it re-elaborates every extracted file and costs more than the
243+
# A baseline written by an older `collect` is rejected outright — the data file carries a
244+
# format version and `build-site` treats a mismatch as fatal, not as something to degrade
245+
# around. Dropped here instead: one run without a Changes page, and the next has one
246+
# again, which beats failing the deployment over a stale artifact.
247+
baseline="${REFEREE_BASELINE:-}"
248+
if [ -n "$baseline" ] &&
249+
[ "$(jq -r .version "$baseline")" != "$(jq -r .version "$REFEREE_DATA")" ]; then
250+
echo "::notice::baseline is collected-data version $(jq -r .version "$baseline") but \
251+
this build writes $(jq -r .version "$REFEREE_DATA"); skipping the revision diff"
252+
baseline=""
253+
fi
254+
255+
# `--exclude-lib` goes only to `extract`, the one phase here that imports the project
256+
# wholesale. `highlight` fans out to per-module workers driven by the data file, and
257+
# `build-site` reads only the JSON, so neither needs it. `highlight-extracted` is
258+
# deliberately skipped — it re-elaborates every extracted file and costs more than the
126259
# rest of this job combined; without it the standalone files are still written and linked,
127260
# just not rendered inline.
128-
lake env "$REFEREE_BIN" collect \
129-
--root LeanMachineLearning --exclude-lib LMLTutorial --data referee-data.json
130261
lake env "$REFEREE_BIN" extract \
131-
--data referee-data.json --exclude-lib LMLTutorial --output ./referee-site
262+
--data "$REFEREE_DATA" --exclude-lib "$REFEREE_EXCLUDE_LIB" --output ./referee-site
132263
lake env "$REFEREE_BIN" highlight \
133-
--data referee-data.json --output ./referee-site
264+
--data "$REFEREE_DATA" --output ./referee-site
134265
# `--trust` is an editorial claim, not a derived fact: it says whoever publishes this site
135266
# vouches for Mathlib and everything under it. Drop it and every upstream package counts
136267
# as unaudited; name more packages to vouch for more.
268+
#
269+
# `--provenance` is read-only here: `build-site` never folds. It adds the "meaning
270+
# unchanged since" line, the Browse column, the revision selector on the Changes page, and
271+
# pins every source link to the commit the ledger was folded at instead of to `main`.
137272
"$REFEREE_BIN" build-site \
138-
--data referee-data.json \
273+
--data "$REFEREE_DATA" \
139274
--output ./referee-site \
140275
--repo-url "https://github.com/$GITHUB_REPOSITORY" \
141276
--site-url "$REFEREE_SITE_URL" \
142277
--trust mathlib \
143-
${REFEREE_BASELINE:+--baseline "$REFEREE_BASELINE"} \
144-
${REFEREE_BASELINE:+--baseline-label "the previous build"}
278+
--provenance "$REFEREE_PROVENANCE" \
279+
${baseline:+--baseline "$baseline"} \
280+
${baseline:+--baseline-label "the previous build"}
145281
146282
# Kept as an artifact so the *next* run can diff against it. Also the thing to download when
147283
# you want to re-render the site locally without re-importing the project.
@@ -150,9 +286,16 @@ jobs:
150286
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
151287
with:
152288
name: referee-data
153-
path: referee-data.json
289+
path: ${{ env.REFEREE_DATA }}
154290
retention-days: 90
155291

292+
# Unconditional, unlike the referee steps: on a pull request this is the check that the
293+
# tutorial still builds. Placed after the provenance fold so that `home_page/` and
294+
# `LMLTutorial/_out/` do not exist yet when the ledger records whether the tree was clean.
295+
- name: Build Verso Documentation
296+
run: |
297+
./scripts/build_docs.sh
298+
156299
- name: Compile blueprint and documentation
157300
uses: leanprover-community/docgen-action@7b5b9a1822650bd45aaeb4182d94bceba2030f89 # 2026-04-14
158301
with:

0 commit comments

Comments
 (0)