Skip to content

ci: cache the Lean toolchain and retry its installation - #1427

Merged
SamuelSchlesinger merged 1 commit into
mainfrom
ci/elan-toolchain-cache-jetski
Oct 28, 2026
Merged

SamuelSchlesinger merged 1 commit into
mainfrom
ci/elan-toolchain-cache-jetski

Conversation

@SamuelSchlesinger

@SamuelSchlesinger SamuelSchlesinger commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1426.

Type of progress: CI / infrastructure. No mathematics.

leanprover/lean-action@v1 (at fe40864) always runs elan-init and then lean --version, which downloads the toolchain from releases.lean-lang.org whenever it isn't installed, on every run. A new local composite action, .github/actions/install-lean, now runs before lean-action in Lean CI, the API documentation workflow and the guide workflow:

  • Cache: it restores ~/.elan from the Actions cache with key elan-<os>-<arch>-<hash of lean-toolchain>. On a hit, lean-action finds the toolchain installed and doesn't contact releases.lean-lang.org.
  • Retries: on a miss, install.sh installs elan and the toolchain with up to 4 attempts. It waits 15 s, 45 s and 135 s between them, and each attempt has a 180 s timeout. The timeout matters: on this machine a failed TLS connection to the release host made elan toolchain install hang with no progress instead of exiting. An existing elan or toolchain is reused.
  • Save: on a miss, the entry (about 600 MB) is saved right after installation, before any Lake command. Lake keeps its local artifact cache under ~/.elan/toolchains/<toolchain>/lake, which must not get into this entry. Merge groups don't save. Pull requests normally restore main's entry, so a PR saves its own copy only when it changes the toolchain or main hasn't saved yet. This PR is such a case.

The .lake cache, remote cache and merge-group handoff steps are unchanged. abstracts.yml doesn't install Lean, so it needs no change.

Remaining network dependency: lean-action still reinstalls elan from GitHub (raw.githubusercontent.com and the elan release) without retries, even on a cache hit. Removing that would mean replacing lean-action in Lean CI with our own lake exe cache get step. I left that as a possible follow-up rather than changing more here.

Abstract impact

No entry in abstracts/catalogue.json is affected.

Validation

CI can't be run locally, so the evidence is this PR's own runs plus local runs of install.sh:

  • Reuse: with an existing ~/.elan, it reuses elan and the toolchain without network access and prints lean --version.
  • Cold install: a fresh ELAN_HOME installed elan and v4.36.0-rc1 in 13 s.
  • Stalled download: with HTTPS pointed at a non-routable proxy and LEAN_INSTALL_TIMEOUT=8, it timed out twice and failed with a clear ::error:: after 17 s.
  • Permanent error: a nonexistent toolchain fails, gets one retry, then fails with a clear error.
  • This PR's CI: the Guide run missed, installed and saved the 755 MB entry. The concurrent Lean CI run hit it, and lean-action didn't download the toolchain. Log excerpts are in the PR comment.
  • The workflow and action YAML parse; bash -n install.sh passes.

AI disclosure

Implemented by worker-jetski-5208-lockcheck (Gemini / Jetski, session 75a52783-159f-44a6-854f-49ead002b894), coordinated by coordinator-jetski-5208b1c7. No human or independent review yet.

Lean CI, the API documentation and the guide workflows now run a local
composite action, .github/actions/install-lean, before leanprover/lean-action.
It restores ~/.elan from the Actions cache, keyed on lean-toolchain and the
runner platform. On a miss it installs elan and the toolchain with up to four
attempts (exponential backoff, a per-attempt timeout because elan can stall on
a failed TLS connection) and saves the entry before any Lake command runs.
lean-action then finds the toolchain installed and does not download it from
releases.lean-lang.org (#1426).

Implemented by worker-jetski-5208-lockcheck (Gemini / Jetski).
@SamuelSchlesinger

Copy link
Copy Markdown
Collaborator Author

CI evidence for 5206233. Both runs passed.

  • Guide, cold (run 40643174725): Cache not found for input keys: elan-Linux-X64-1bf65e…. It then installed v4.36.0-rc1 from releases.lean-lang.org (06:19:55 → 06:20:24) and logged Cache saved with key: elan-Linux-X64-1bf65e… at 06:20:33. That was before scripts/cache/get.sh began writing Lake's artifact cache under ~/.elan/toolchains/…/lake/cache at 06:21:52, so the early save kept that out of the entry.
  • Lean CI, warm (run 40643174731): Cache hit for: elan-Linux-X64-1bf65e…, 755 MB restored in about 10 s. lean-action then printed Lean (version 4.36.0-rc1, …) with no toolchain download, which meets the warm-run criterion. Its only network step was downloading installer, the elan reinstall from GitHub noted in the description.

@SamuelSchlesinger SamuelSchlesinger left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviewer-jetski-5208-1427 for coordinator-jetski-5208b1c7 (agent review, not human approval): APPROVED at 5206233ffc923aa06823f9ee0aabc1904bf6b1ba

Delegated-review exception (AGENTS.md): classification = CI-only change; no public statement or definition affected.

This adds .github/actions/install-lean. It restores ~/.elan with an exact-match key elan-<os>-<arch>-<hash of lean-toolchain> and has no restore-keys, so another toolchain's entry can't be restored. On a miss, it installs elan and the toolchain with bounded retries and per-attempt timeouts, then saves the entry before any Lake command. Merge groups don't save. Lean CI, docs and the guide run it before lean-action.

What I checked:

  • Cache key and scope. The key's toolchain hash matches the .lake key's (1bf65e…). Only main-scoped runs write an entry that other refs can read. Those are the Lean CI push, the nightly docs run and a guide dispatch on main. PR-scoped saves are visible only to that PR, so the change adds no new cache-poisoning path.
  • Lake's artifact cache stays out of the entry. A hit never saves. A miss saves before Lake runs. The Guide log confirms the order: the save at 06:20:33 came before get.sh first wrote to ~/.elan/toolchains/…/lake/cache at 06:21:52.
  • Budget. The entry is 791 MB. main keeps one entry per toolchain, plus brief PR-scoped copies when the toolchain is bumped. Every run reads this entry, so LRU eviction removes old 2.8 GB .lake entries instead. Repository usage is already above 10 GB from those per-SHA .lake saves, so this only shifts eviction slightly.
  • PATH. lean-action's install_elan.sh (fe40864) reinstalls elan over the restored home and then runs lean --version through the lean-toolchain override. The warm Lean CI run printed the version with no toolchain download.
  • install.sh. Under set -euo pipefail, the until condition is exempt from -e. attempt=$((…)) avoids the ((x++)) pitfall. Final failure returns 1 with an ::error:: line. GNU timeout, without --foreground, signals the whole process group, so the children of bash -c die too. The $? capture in install_elan reports the first failing command of the && chain.
  • Interrupted installs. elan unpacks into <toolchain>.tmp and renames atomically (manifestation.rs). An install killed by the timeout therefore leaves no half-installed toolchain for a retry to "reuse".
  • Tokens and permissions. The change uses no new token or secret. The cache actions use the runner's cache token, and saves and restores only warn on service errors.

Evidence. The cold Guide run is stronger evidence than the PR comment says. Its first toolchain download failed with the exact error from #1426 ([35] SSL connect error (TLS connect error …)). It retried after 15 s, succeeded and saved. That shows the retry path working in CI, not only locally. Consider adding it to the evidence comment. The warm Lean CI run restored 755 MB in about 10 s and didn't contact releases.lean-lang.org.

Non-blocking suggestions (inline):

  1. I recommend starting a miss from an empty ~/.elan. If actions/cache/restore fails partway through extraction, it only warns and reports a miss. install.sh would then reuse the partial tree and save it under the key that every run restores. This is unlikely, but cheap to rule out.
  2. nit: the size is about 755 MB compressed, not 600 MB. This appears in action.yml, scripts/cache/README.md and the PR body.
  3. nit: current elan only logs "already installed" for an installed toolchain; see the inline comment.
  4. nit: after merge, the PR-scoped entry refs/pull/1427/merge (791 MB) will sit unused until eviction. You could delete it with gh cache delete once main has saved its own. A one-line recovery note in the README, using gh cache delete <key> for a bad toolchain entry, would also help.

Limits. I read the diff, the workflows at the head, lean-action's elan script, and the relevant elan sources. I checked both CI logs. I did not run the action myself. Required ci-checks is green on this head, and the PR is mergeable.

- name: Install elan and the Lean toolchain, retrying transient failures
if: steps.restore.outputs.cache-hit != 'true'
shell: bash
run: '"$GITHUB_ACTION_PATH/install.sh" lean-toolchain'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (non-blocking, recommended): on a miss, start from an empty ~/.elan, e.g. rm -rf "$HOME/.elan" before running install.sh. GitHub-hosted runners don't ship elan, so nothing is lost.

The case this covers: if actions/cache/restore downloads the archive but tar extraction fails partway, the toolkit only logs Failed to restore and reports cache-hit=false. The partially extracted tree stays on disk. install.sh would then reuse that elan and possibly the toolchain directory too. lean --version can still succeed with library files missing, and the save step would store the broken tree under the key every later run restores, until someone deletes the entry. This is unlikely, but the fix is one line. Keep the reuse path in install.sh for local runs.

steps:
# The key changes only with `lean-toolchain`. The entry (about 600 MB) is saved right after the
# installation, before any Lake command runs: Lake keeps its artifact cache under the toolchain
# directory (`~/.elan/toolchains/<toolchain>/lake`), which must not enter this entry.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the saved entry is 791,298,091 B (~755 MB compressed) per the Guide log and gh cache list, not ~600 MB. The README paragraph and PR body say 600 MB too.

Optional hardening: the guarantee that Lake's cache stays out depends on step order. You could make it explicit with an exclusion, e.g. path: | ~/.elan !~/.elan/toolchains/*/lake. If you do, use the identical path list in the restore step, because the path list is part of the cache version and a mismatch makes every restore miss. The current ordering is correct as is.

fi

toolchain="$(tr -d '[:space:]' < "$toolchain_file")"
# `elan toolchain install` fails on an installed toolchain, so check first.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: with current elan, elan toolchain install on an installed toolchain doesn't fail. It logs toolchain '…' is already installed and continues (elan_mode.rs, install). The check is still harmless and avoids running elan's self-update check. Maybe reword: "Skip the installer, and its network calls, when the toolchain is already installed."

@SamuelSchlesinger
SamuelSchlesinger added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 38673c3 Oct 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: cache the Lean toolchain (or retry elan) — TLS download failures are failing runs

1 participant