feat: support revised nightly releases (nightly-YYYY-MM-DD-revK) (#12461)

This PR adds support for manually re-releasing nightlies when a build
issue or critical fix requires it. When a `workflow_dispatch` triggers
the nightly release job and a `nightly-YYYY-MM-DD` tag already exists,
the CI now creates `nightly-YYYY-MM-DD-rev1` (then `-rev2`, etc.)
instead of silently skipping.

### Lake `ToolchainVer`

- Extend `ToolchainVer.nightly` with an optional `rev : Option Nat`
field
- Parse `-revK` suffixes from nightly tags in `ofString`
- Ordering: `nightly-YYYY-MM-DD` < `nightly-YYYY-MM-DD-rev1` < `-rev2` <
`nightly-YYYY-MM-DD+1`
- Round-trip: `toString (ofString s) == s` for both variants

### CI workflow

- "Set Nightly" step probes existing tags on `workflow_dispatch` to find
next available `-revK`
- Scheduled nightlies retain existing behavior (skip if commit already
tagged)
- Changelog grep updated from `nightly-[-0-9]*` to `nightly-[^ ,)]*` to
match `-revK` suffixes

### `lean-bisect`

- Updated `NIGHTLY_PATTERN` regex, sort key, error messages, and help
text

### Companion PRs

- https://github.com/leanprover-community/mathlib4/pull/35220: update
`nightly_bump_and_merge.yml` tag grep and `nightly_detect_failure.yml`
warning message
-
https://github.com/leanprover-community/leanprover-community.github.io/pull/787:
update `tags_and_branches.md` documentation

🤖 Prepared with Claude Code

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kim Morrison
2026-02-13 11:41:04 +11:00
committed by GitHub
parent 56bd8cd0c2
commit d7e57b66d5
5 changed files with 71 additions and 22 deletions

View File

@@ -60,10 +60,25 @@ jobs:
if [[ -n '${{ secrets.PUSH_NIGHTLY_TOKEN }}' ]]; then
git remote add nightly https://foo:'${{ secrets.PUSH_NIGHTLY_TOKEN }}'@github.com/${{ github.repository_owner }}/lean4-nightly.git
git fetch nightly --tags
LEAN_VERSION_STRING="nightly-$(date -u +%F)"
# do nothing if commit already has a different tag
if [[ "$(git name-rev --name-only --tags --no-undefined HEAD 2> /dev/null || echo "$LEAN_VERSION_STRING")" == "$LEAN_VERSION_STRING" ]]; then
BASE_NIGHTLY="nightly-$(date -u +%F)"
if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then
# Manual re-release: find next available revision if base nightly exists
if git rev-parse "refs/tags/$BASE_NIGHTLY" >/dev/null 2>&1; then
REV=1
while git rev-parse "refs/tags/${BASE_NIGHTLY}-rev${REV}" >/dev/null 2>&1; do
REV=$((REV + 1))
done
LEAN_VERSION_STRING="${BASE_NIGHTLY}-rev${REV}"
else
LEAN_VERSION_STRING="$BASE_NIGHTLY"
fi
echo "nightly=$LEAN_VERSION_STRING" >> "$GITHUB_OUTPUT"
else
# Scheduled: do nothing if commit already has a different tag
LEAN_VERSION_STRING="$BASE_NIGHTLY"
if [[ "$(git name-rev --name-only --tags --no-undefined HEAD 2> /dev/null || echo "$LEAN_VERSION_STRING")" == "$LEAN_VERSION_STRING" ]]; then
echo "nightly=$LEAN_VERSION_STRING" >> "$GITHUB_OUTPUT"
fi
fi
fi
@@ -475,7 +490,7 @@ jobs:
git tag "${{ needs.configure.outputs.nightly }}"
git push nightly "${{ needs.configure.outputs.nightly }}"
git push -f origin refs/tags/${{ needs.configure.outputs.nightly }}:refs/heads/nightly
last_tag="$(git log HEAD^ --simplify-by-decoration --pretty="format:%d" | grep -o "nightly-[-0-9]*" | head -n 1)"
last_tag="$(git log HEAD^ --simplify-by-decoration --pretty="format:%d" | grep -o "nightly-[^ ,)]*" | head -n 1)"
echo -e "*Changes since ${last_tag}:*\n\n" > diff.md
git show "$last_tag":RELEASES.md > old.md
#./script/diff_changelogs.py old.md doc/changes.md >> diff.md

View File

@@ -36,7 +36,7 @@ sys.path.insert(0, str(Path(__file__).parent))
import build_artifact
# Constants
NIGHTLY_PATTERN = re.compile(r'^nightly-(\d{4})-(\d{2})-(\d{2})$')
NIGHTLY_PATTERN = re.compile(r'^nightly-(\d{4})-(\d{2})-(\d{2})(-rev(\d+))?$')
VERSION_PATTERN = re.compile(r'^v4\.(\d+)\.(\d+)(-rc\d+)?$')
# Accept short SHAs (7+ chars) - we'll resolve to full SHA later
SHA_PATTERN = re.compile(r'^[0-9a-f]{7,40}$')
@@ -158,7 +158,7 @@ def parse_identifier(s: str) -> Tuple[str, str]:
return ('sha', full_sha)
error(f"Invalid identifier format: '{s}'\n"
f"Expected one of:\n"
f" - nightly-YYYY-MM-DD (e.g., nightly-2024-06-15)\n"
f" - nightly-YYYY-MM-DD or nightly-YYYY-MM-DD-revK (e.g., nightly-2024-06-15, nightly-2024-06-15-rev1)\n"
f" - v4.X.Y or v4.X.Y-rcK (e.g., v4.8.0, v4.9.0-rc1)\n"
f" - commit SHA (short or full)")
@@ -244,8 +244,13 @@ def fetch_nightly_tags() -> List[str]:
if len(data) < 100:
break
# Sort by date (nightly-YYYY-MM-DD format sorts lexicographically)
tags.sort()
# Sort by date and revision (nightly-YYYY-MM-DD-revK needs numeric comparison on rev)
def nightly_sort_key(tag):
m = NIGHTLY_PATTERN.match(tag)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(5) or 0))
return (0, 0, 0, 0)
tags.sort(key=nightly_sort_key)
return tags
def get_commit_for_nightly(nightly: str) -> str:
@@ -1024,6 +1029,7 @@ Range Syntax:
Identifier Formats:
nightly-YYYY-MM-DD Nightly build date (e.g., nightly-2024-06-15)
nightly-YYYY-MM-DD-revK Revised nightly (e.g., nightly-2024-06-15-rev1)
Uses pre-built toolchains from leanprover/lean4-nightly.
Fast: downloads via elan (~30s each).
@@ -1151,9 +1157,9 @@ Examples:
# Validate --nightly-only
if args.nightly_only:
if from_val is not None and from_type != 'nightly':
error("--nightly-only requires FROM to be a nightly identifier (nightly-YYYY-MM-DD)")
error("--nightly-only requires FROM to be a nightly identifier (nightly-YYYY-MM-DD or nightly-YYYY-MM-DD-revK)")
if to_type != 'nightly':
error("--nightly-only requires TO to be a nightly identifier (nightly-YYYY-MM-DD)")
error("--nightly-only requires TO to be a nightly identifier (nightly-YYYY-MM-DD or nightly-YYYY-MM-DD-revK)")
if from_val:
info(f"From: {from_val} ({from_type})")

View File

@@ -235,13 +235,14 @@ open ToolchainVer in
/-- A Lean toolchain version. -/
inductive ToolchainVer
| release (ver : LeanVer)
| nightly (date : Date)
| nightly (date : Date) (rev : Option Nat := none)
| pr (n : Nat)
| other (v : String)
with
@[computed_field] toString : ToolchainVer String
| .release ver => s!"{defaultOrigin}:v{ver}"
| .nightly date => s!"{defaultOrigin}:nightly-{date}"
| .nightly date rev => s!"{defaultOrigin}:nightly-{date}" ++
match rev with | none => "" | some r => s!"-rev{r}"
| .pr n => s!"{prOrigin}:pr-release-{n}"
| .other v => v
deriving Repr, DecidableEq
@@ -264,13 +265,20 @@ public def ofString (ver : String) : ToolchainVer := Id.run do
if let .ok ver := StdVer.parse (tag.drop 1).copy then
if noOrigin|| origin == defaultOrigin then
return .release ver
else if let some date := tag.dropPrefix? "nightly-" then
if let some date := Date.ofString? date.toString then
if noOrigin then
return .nightly date
else if let some suffix := origin.dropPrefix? defaultOrigin then
if suffix.isEmpty || suffix == "-nightly" then
return .nightly date
else if let some rest := tag.dropPrefix? "nightly-" then
let rest := rest.toString
-- Dates are exactly 10 characters (YYYY-MM-DD); anything after must be a -revK suffix
if let some date := Date.ofString? (rest.take 10).toString then
let rev? : Option Nat := do
let suffix (rest.drop 10).dropPrefix? "-rev"
suffix.toString.toNat?
-- Accept if no suffix (plain nightly) or valid -revK suffix
if rest.length 10 || rev?.isSome then
if noOrigin then
return .nightly date rev?
else if let some suffix := origin.dropPrefix? defaultOrigin then
if suffix.isEmpty || suffix == "-nightly" then
return .nightly date rev?
else if let some n := tag.dropPrefix? "pr-release-" then
if let some n := n.toNat? then
if noOrigin || origin == prOrigin then
@@ -301,7 +309,8 @@ public instance : FromJson ToolchainVer := ⟨(ToolchainVer.ofString <$> fromJso
public def blt (a b : ToolchainVer) : Bool :=
match a, b with
| .release v1, .release v2 => v1 < v2
| .nightly d1, .nightly d2 => d1 < d2
| .nightly d1 r1, .nightly d2 r2 =>
d1 < d2 || (d1 == d2 && (r1.getD 0) < (r2.getD 0))
| _, _ => false
public instance : LT ToolchainVer := (·.blt ·)
@@ -311,7 +320,8 @@ public instance decLt (a b : ToolchainVer) : Decidable (a < b) :=
public def ble (a b : ToolchainVer) : Bool :=
match a, b with
| .release v1, .release v2 => v1 v2
| .nightly d1, .nightly d2 => d1 d2
| .nightly d1 r1, .nightly d2 r2 =>
d1 < d2 || (d1 == d2 && (r1.getD 0) (r2.getD 0))
| .pr n1, .pr n2 => n1 = n2
| .other v1, .other v2 => v1 = v2
| _, _ => false

View File

@@ -12,21 +12,33 @@ def test := do
IO.println ""
checkParse "leanprover/lean4:v4.13.0-rc1"
checkParse "leanprover/lean4:nightly-2024-09-15"
checkParse "leanprover/lean4:nightly-2024-09-15-rev1"
checkParse "nightly-2024-09-15-rev2"
checkParse "leanprover/lean4-pr-releases:pr-release-101"
checkParse "leanprover/lean:v4.1.0"
checkParse "4.12.0"
checkParse "nightly-2024-09-15-foo" -- malformed suffix → .other
checkLt "4.6.0-rc1" "v4.6.0"
checkLt "4.12.0" "leanprover/lean4:v4.13.0-rc1"
checkLt "nightly-2024-09-08" "nightly-2024-10-09"
checkLt "nightly-2024-09-08" "nightly-2024-09-08-rev1"
checkLt "nightly-2024-09-08-rev1" "nightly-2024-09-08-rev2"
checkLt "nightly-2024-09-08-rev2" "nightly-2024-09-09"
checkLt "nightly-2024-09-08" "4.0.0"
/--
info:
Lake.ToolchainVer.release { toSemVerCore := { major := 4, minor := 13, patch := 0 }, specialDescr := "rc1" }
Lake.ToolchainVer.nightly { year := 2024, month := 9, day := 15 }
Lake.ToolchainVer.nightly { year := 2024, month := 9, day := 15 } none
Lake.ToolchainVer.nightly { year := 2024, month := 9, day := 15 } (some 1)
Lake.ToolchainVer.nightly { year := 2024, month := 9, day := 15 } (some 2)
Lake.ToolchainVer.pr 101
Lake.ToolchainVer.other "leanprover/lean:v4.1.0"
Lake.ToolchainVer.release { toSemVerCore := { major := 4, minor := 12, patch := 0 }, specialDescr := "" }
Lake.ToolchainVer.other "nightly-2024-09-15-foo"
true
true
true
true
true
true

View File

@@ -42,6 +42,12 @@ echo nightly-2024-10-01 > a/lean-toolchain
echo nightly-2024-01-10 > b/lean-toolchain
test_update leanprover/lean4:nightly-2024-10-01
# Test nightly revision comparison
./clean.sh
echo nightly-2024-10-01-rev1 > a/lean-toolchain
echo nightly-2024-01-10 > b/lean-toolchain
test_update leanprover/lean4:nightly-2024-10-01-rev1
# Test up-to-date root
./clean.sh
echo v4.4.0 > a/lean-toolchain