|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 |
|
| 3 | +""" |
| 4 | +Release Checklist for Lean4 and Downstream Repositories |
| 5 | +
|
| 6 | +This script validates the status of a Lean4 release across all dependent repositories. |
| 7 | +It checks whether repositories are ready for release and identifies missing steps. |
| 8 | +
|
| 9 | +IMPORTANT: Keep this documentation up-to-date when modifying the script's behavior! |
| 10 | +
|
| 11 | +What this script does: |
| 12 | +1. Validates preliminary Lean4 release infrastructure: |
| 13 | + - Checks that the release branch (releases/vX.Y.0) exists |
| 14 | + - Verifies CMake version settings are correct |
| 15 | + - Confirms the release tag exists |
| 16 | + - Validates the release page exists on GitHub |
| 17 | + - Checks the release notes page on lean-lang.org |
| 18 | +
|
| 19 | +2. For each downstream repository (batteries, mathlib4, etc.): |
| 20 | + - Checks if dependencies are ready (e.g., mathlib4 depends on batteries) |
| 21 | + - Verifies the main branch is on the target toolchain (or newer) |
| 22 | + - Checks if a PR exists to bump the toolchain (if not yet updated) |
| 23 | + - Validates tags exist for the release version |
| 24 | + - Ensures tags are merged into stable branches (for non-RC releases) |
| 25 | + - Verifies bump branches exist and are configured correctly |
| 26 | + - Special handling for ProofWidgets4 release tags |
| 27 | +
|
| 28 | +3. Optionally automates missing steps (when not in --dry-run mode): |
| 29 | + - Creates missing release tags using push_repo_release_tag.py |
| 30 | + - Merges tags into stable branches using merge_remote.py |
| 31 | +
|
| 32 | +Usage: |
| 33 | + ./release_checklist.py v4.24.0 # Check release status |
| 34 | + ./release_checklist.py v4.24.0 --verbose # Show detailed debug info |
| 35 | + ./release_checklist.py v4.24.0 --dry-run # Check only, don't execute fixes |
| 36 | +
|
| 37 | +For automated release management with Claude Code: |
| 38 | + /release v4.24.0 # Run full release process with Claude |
| 39 | +
|
| 40 | +The script reads repository configurations from release_repos.yml and reports: |
| 41 | +- ✅ for completed requirements |
| 42 | +- ❌ for missing requirements (with instructions to fix) |
| 43 | +- 🟡 for repositories waiting on dependencies |
| 44 | +- ⮕ for automated actions being taken |
| 45 | +
|
| 46 | +This script is idempotent and safe to rerun multiple times. |
| 47 | +""" |
| 48 | + |
3 | 49 | import argparse |
4 | 50 | import yaml |
5 | 51 | import requests |
@@ -286,6 +332,68 @@ def check_bump_branch_toolchain(url, bump_branch, github_token): |
286 | 332 | print(f" ✅ Bump branch correctly uses toolchain: {content}") |
287 | 333 | return True |
288 | 334 |
|
| 335 | +def get_pr_ci_status(repo_url, pr_number, github_token): |
| 336 | + """Get the CI status for a pull request.""" |
| 337 | + api_base = repo_url.replace("https://github.com/", "https://api.github.com/repos/") |
| 338 | + headers = {'Authorization': f'token {github_token}'} if github_token else {} |
| 339 | + |
| 340 | + # Get PR details to find the head SHA |
| 341 | + pr_response = requests.get(f"{api_base}/pulls/{pr_number}", headers=headers) |
| 342 | + if pr_response.status_code != 200: |
| 343 | + return "unknown", "Could not fetch PR details" |
| 344 | + |
| 345 | + pr_data = pr_response.json() |
| 346 | + head_sha = pr_data['head']['sha'] |
| 347 | + |
| 348 | + # Get check runs for the commit |
| 349 | + check_runs_response = requests.get( |
| 350 | + f"{api_base}/commits/{head_sha}/check-runs", |
| 351 | + headers=headers |
| 352 | + ) |
| 353 | + |
| 354 | + if check_runs_response.status_code != 200: |
| 355 | + return "unknown", "Could not fetch check runs" |
| 356 | + |
| 357 | + check_runs_data = check_runs_response.json() |
| 358 | + check_runs = check_runs_data.get('check_runs', []) |
| 359 | + |
| 360 | + if not check_runs: |
| 361 | + # No check runs, check for status checks (legacy) |
| 362 | + status_response = requests.get( |
| 363 | + f"{api_base}/commits/{head_sha}/status", |
| 364 | + headers=headers |
| 365 | + ) |
| 366 | + if status_response.status_code == 200: |
| 367 | + status_data = status_response.json() |
| 368 | + state = status_data.get('state', 'unknown') |
| 369 | + if state == 'success': |
| 370 | + return "success", "All status checks passed" |
| 371 | + elif state == 'failure': |
| 372 | + return "failure", "Some status checks failed" |
| 373 | + elif state == 'pending': |
| 374 | + return "pending", "Status checks in progress" |
| 375 | + return "unknown", "No CI checks found" |
| 376 | + |
| 377 | + # Analyze check runs |
| 378 | + conclusions = [run['conclusion'] for run in check_runs if run.get('status') == 'completed'] |
| 379 | + in_progress = [run for run in check_runs if run.get('status') in ['queued', 'in_progress']] |
| 380 | + |
| 381 | + if in_progress: |
| 382 | + return "pending", f"{len(in_progress)} check(s) in progress" |
| 383 | + |
| 384 | + if not conclusions: |
| 385 | + return "pending", "Checks queued" |
| 386 | + |
| 387 | + if all(c == 'success' for c in conclusions): |
| 388 | + return "success", f"All {len(conclusions)} checks passed" |
| 389 | + |
| 390 | + failed = sum(1 for c in conclusions if c in ['failure', 'timed_out', 'action_required']) |
| 391 | + if failed > 0: |
| 392 | + return "failure", f"{failed} check(s) failed" |
| 393 | + |
| 394 | + # Some checks are cancelled, skipped, or neutral |
| 395 | + return "warning", f"Some checks did not complete normally" |
| 396 | + |
289 | 397 | def pr_exists_with_title(repo_url, title, github_token): |
290 | 398 | api_url = repo_url.replace("https://github.com/", "https://api.github.com/repos/") + "/pulls" |
291 | 399 | headers = {'Authorization': f'token {github_token}'} if github_token else {} |
@@ -471,6 +579,19 @@ def main(): |
471 | 579 | if pr_info: |
472 | 580 | pr_number, pr_url = pr_info |
473 | 581 | print(f" ✅ PR with title '{pr_title}' exists: #{pr_number} ({pr_url})") |
| 582 | + |
| 583 | + # Check CI status |
| 584 | + ci_status, ci_message = get_pr_ci_status(url, pr_number, github_token) |
| 585 | + if ci_status == "success": |
| 586 | + print(f" ✅ CI: {ci_message}") |
| 587 | + elif ci_status == "failure": |
| 588 | + print(f" ❌ CI: {ci_message}") |
| 589 | + elif ci_status == "pending": |
| 590 | + print(f" 🔄 CI: {ci_message}") |
| 591 | + elif ci_status == "warning": |
| 592 | + print(f" ⚠️ CI: {ci_message}") |
| 593 | + else: |
| 594 | + print(f" ❓ CI: {ci_message}") |
474 | 595 | else: |
475 | 596 | print(f" ❌ PR with title '{pr_title}' does not exist") |
476 | 597 | print(f" Run `script/release_steps.py {toolchain} {name}` to create it") |
|
0 commit comments