Skip to content

Commit ae89b7e

Browse files
authored
feat: further release automation (#9092)
This PR further updates release automation. The per-repository update scripts `script/release_steps.py` now actually performs the tests, rather than outputting a script for the release manager to run line by line. It's been tested on `v4.21.0` (i.e. the easy case of a stable release), and we'll debug its behaviour on `v4.22.0-rc1` tonight.
1 parent ede8a7e commit ae89b7e

File tree

4 files changed

+486
-76
lines changed

4 files changed

+486
-76
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ fwOut.txt
3131
wdErr.txt
3232
wdIn.txt
3333
wdOut.txt
34+
downstream_releases/

script/push_repo_release_tag.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
import subprocess
44
import requests
55

6+
def check_gh_auth():
7+
"""Check if GitHub CLI is properly authenticated."""
8+
try:
9+
result = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True)
10+
if result.returncode != 0:
11+
return False, result.stderr
12+
return True, None
13+
except FileNotFoundError:
14+
return False, "GitHub CLI (gh) is not installed. Please install it first."
15+
except Exception as e:
16+
return False, f"Error checking authentication: {e}"
17+
18+
def handle_gh_error(error_output):
19+
"""Handle GitHub CLI errors and provide helpful messages."""
20+
if "Not Found (HTTP 404)" in error_output:
21+
return "Repository not found or access denied. Please check:\n" \
22+
"1. The repository name is correct\n" \
23+
"2. You have access to the repository\n" \
24+
"3. Your GitHub CLI authentication is valid"
25+
elif "Bad credentials" in error_output or "invalid" in error_output.lower():
26+
return "Authentication failed. Please run 'gh auth login' to re-authenticate."
27+
elif "rate limit" in error_output.lower():
28+
return "GitHub API rate limit exceeded. Please try again later."
29+
else:
30+
return f"GitHub API error: {error_output}"
31+
632
def main():
733
if len(sys.argv) != 4:
834
print("Usage: ./push_repo_release_tag.py <repo> <branch> <version_tag>")
@@ -14,6 +40,13 @@ def main():
1440
print(f"Error: Branch '{branch}' is not 'master' or 'main'.")
1541
sys.exit(1)
1642

43+
# Check GitHub CLI authentication first
44+
auth_ok, auth_error = check_gh_auth()
45+
if not auth_ok:
46+
print(f"Authentication error: {auth_error}")
47+
print("\nTo fix this, run: gh auth login")
48+
sys.exit(1)
49+
1750
# Get the `lean-toolchain` file content
1851
lean_toolchain_url = f"https://raw.githubusercontent.com/{repo}/{branch}/lean-toolchain"
1952
try:
@@ -43,12 +76,23 @@ def main():
4376
for tag in existing_tags:
4477
print(tag.replace("refs/tags/", ""))
4578
sys.exit(1)
79+
elif list_tags_output.returncode != 0:
80+
# Handle API errors when listing tags
81+
error_msg = handle_gh_error(list_tags_output.stderr)
82+
print(f"Error checking existing tags: {error_msg}")
83+
sys.exit(1)
4684

4785
# Get the SHA of the branch
4886
get_sha_cmd = [
4987
"gh", "api", f"repos/{repo}/git/ref/heads/{branch}", "--jq", ".object.sha"
5088
]
51-
sha_result = subprocess.run(get_sha_cmd, capture_output=True, text=True, check=True)
89+
sha_result = subprocess.run(get_sha_cmd, capture_output=True, text=True)
90+
91+
if sha_result.returncode != 0:
92+
error_msg = handle_gh_error(sha_result.stderr)
93+
print(f"Error getting branch SHA: {error_msg}")
94+
sys.exit(1)
95+
5296
sha = sha_result.stdout.strip()
5397

5498
# Create the tag
@@ -58,11 +102,20 @@ def main():
58102
"-F", f"ref=refs/tags/{version_tag}",
59103
"-F", f"sha={sha}"
60104
]
61-
subprocess.run(create_tag_cmd, capture_output=True, text=True, check=True)
105+
create_result = subprocess.run(create_tag_cmd, capture_output=True, text=True)
106+
107+
if create_result.returncode != 0:
108+
error_msg = handle_gh_error(create_result.stderr)
109+
print(f"Error creating tag: {error_msg}")
110+
sys.exit(1)
62111

63112
print(f"Successfully created and pushed tag '{version_tag}' to {repo}.")
64113
except subprocess.CalledProcessError as e:
65-
print(f"Error while creating/pushing tag: {e.stderr.strip() if e.stderr else e}")
114+
error_msg = handle_gh_error(e.stderr.strip() if e.stderr else str(e))
115+
print(f"Error while creating/pushing tag: {error_msg}")
116+
sys.exit(1)
117+
except Exception as e:
118+
print(f"Unexpected error: {e}")
66119
sys.exit(1)
67120

68121
if __name__ == "__main__":

script/release_checklist.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,61 @@ def pr_exists_with_title(repo_url, title, github_token):
262262
return pr['number'], pr['html_url']
263263
return None
264264

265+
def check_proofwidgets4_release(repo_url, target_toolchain, github_token):
266+
"""Check if ProofWidgets4 has a release tag that uses the target toolchain."""
267+
api_base = repo_url.replace("https://github.com/", "https://api.github.com/repos/")
268+
headers = {'Authorization': f'token {github_token}'} if github_token else {}
269+
270+
# Get all tags matching v0.0.* pattern
271+
response = requests.get(f"{api_base}/git/matching-refs/tags/v0.0.", headers=headers)
272+
if response.status_code != 200:
273+
print(f" ❌ Could not fetch ProofWidgets4 tags")
274+
return False
275+
276+
tags = response.json()
277+
if not tags:
278+
print(f" ❌ No v0.0.* tags found for ProofWidgets4")
279+
return False
280+
281+
# Extract tag names and sort by version number (descending)
282+
tag_names = []
283+
for tag in tags:
284+
ref = tag['ref']
285+
if ref.startswith('refs/tags/v0.0.'):
286+
tag_name = ref.replace('refs/tags/', '')
287+
try:
288+
# Extract the number after v0.0.
289+
version_num = int(tag_name.split('.')[-1])
290+
tag_names.append((version_num, tag_name))
291+
except (ValueError, IndexError):
292+
continue
293+
294+
if not tag_names:
295+
print(f" ❌ No valid v0.0.* tags found for ProofWidgets4")
296+
return False
297+
298+
# Sort by version number (descending) and take the most recent 10
299+
tag_names.sort(reverse=True)
300+
recent_tags = tag_names[:10]
301+
302+
# Check each recent tag to see if it uses the target toolchain
303+
for version_num, tag_name in recent_tags:
304+
toolchain_content = get_branch_content(repo_url, tag_name, "lean-toolchain", github_token)
305+
if toolchain_content is None:
306+
continue
307+
308+
if is_version_gte(toolchain_content.strip(), target_toolchain):
309+
print(f" ✅ Found release {tag_name} using compatible toolchain (>= {target_toolchain})")
310+
return True
311+
312+
# If we get here, no recent release uses the target toolchain
313+
# Find the highest version number to suggest the next one
314+
highest_version = max(version_num for version_num, _ in recent_tags)
315+
next_version = highest_version + 1
316+
print(f" ❌ No recent ProofWidgets4 release uses toolchain >= {target_toolchain}")
317+
print(f" You will need to create and push a tag v0.0.{next_version}")
318+
return False
319+
265320
def main():
266321
parser = argparse.ArgumentParser(description="Check release status of Lean4 repositories")
267322
parser.add_argument("toolchain", help="The toolchain version to check (e.g., v4.6.0)")
@@ -386,6 +441,12 @@ def main():
386441
continue
387442
print(f" ✅ On compatible toolchain (>= {toolchain})")
388443

444+
# Special handling for ProofWidgets4
445+
if name == "ProofWidgets4":
446+
if not check_proofwidgets4_release(url, toolchain, github_token):
447+
repo_status[name] = False
448+
continue
449+
389450
if check_tag:
390451
tag_exists_initially = tag_exists(url, toolchain, github_token)
391452
if not tag_exists_initially:

0 commit comments

Comments
 (0)