Skip to content

Commit ad70095

Browse files
committed
Replace check-release.pl with check-release.py
1 parent ff691ae commit ad70095

File tree

7 files changed

+167
-5100
lines changed

7 files changed

+167
-5100
lines changed

.github/workflows/test-no-changes-file.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
- name: Check release artifacts
4141
shell: bash
4242
run: |
43-
./tests/check-release.pl \
43+
./tests/check-release.py \
4444
--artifact-id "${{ steps.release.outputs.artifact-id }}" \
4545
--executable-name test-project \
4646
--github-token "${{ github.token }}" \

.github/workflows/test-working-directory.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
- name: Check release artifacts
6161
shell: bash
6262
run: |
63-
./tests/check-release.pl \
63+
./tests/check-release.py \
6464
--artifact-id "${{ steps.release.outputs.artifact-id }}" \
6565
--executable-name sub-project \
6666
--github-token "${{ github.token }}" \

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
- name: Check release artifacts
5959
shell: bash
6060
run: |
61-
./tests/check-release.pl \
61+
./tests/check-release.py \
6262
--artifact-id "${{ steps.release.outputs.artifact-id }}" \
6363
--executable-name test-project \
6464
--github-token "${{ github.token }}" \

tests/check-release.pl

Lines changed: 0 additions & 102 deletions
This file was deleted.

tests/check-release.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python3
2+
3+
# Written by Claude.ai
4+
5+
import os
6+
import sys
7+
import glob
8+
import hashlib
9+
import subprocess
10+
import argparse
11+
import tempfile
12+
from pathlib import Path
13+
from typing import Tuple
14+
15+
16+
def main() -> None:
17+
"""Main function to validate GitHub artifacts."""
18+
args = parse_args()
19+
20+
# Create and change to a clean temporary directory
21+
with tempfile.TemporaryDirectory() as td:
22+
os.chdir(td)
23+
24+
download_artifact(args.artifact_id, args.repo, args.github_token)
25+
extract_artifact()
26+
27+
archive_file, checksum_file = find_archive_files(
28+
args.executable_name, args.target
29+
)
30+
verify_checksum(checksum_file, archive_file)
31+
extract_archive(archive_file)
32+
verify_archive_contents(args.executable_name, args.changes_file)
33+
34+
print("All validation checks passed successfully")
35+
36+
37+
def parse_args() -> argparse.Namespace:
38+
"""Parse and return command line arguments."""
39+
parser = argparse.ArgumentParser()
40+
parser.add_argument("--artifact-id", required=True)
41+
parser.add_argument("--executable-name", required=True)
42+
parser.add_argument("--github-token", required=True)
43+
parser.add_argument("--repo", required=True)
44+
parser.add_argument("--target", required=True)
45+
parser.add_argument("--no-changes-file", dest="changes_file", action="store_false")
46+
parser.set_defaults(changes_file=True)
47+
48+
return parser.parse_args()
49+
50+
51+
def download_artifact(artifact_id: str, repo: str, github_token: str) -> None:
52+
"""Download artifact from GitHub."""
53+
subprocess.run(
54+
[
55+
"curl",
56+
"-L",
57+
"-H",
58+
"Accept: application/vnd.github+json",
59+
"-H",
60+
f"Authorization: Bearer {github_token}",
61+
"-o",
62+
"artifact.zip",
63+
f"https://api.github.com/repos/{repo}/actions/artifacts/{artifact_id}/zip",
64+
],
65+
check=True,
66+
)
67+
68+
69+
def extract_artifact() -> None:
70+
"""Extract the downloaded artifact zip file."""
71+
subprocess.run(["unzip", "artifact.zip"], check=True)
72+
73+
74+
def find_archive_files(executable_name: str, target: str) -> Tuple[str, str]:
75+
"""Find and return the archive and checksum files."""
76+
is_windows = "windows" in target.lower()
77+
glob_pattern = (
78+
f"{executable_name}*.zip*" if is_windows else f"{executable_name}*.tar.gz*"
79+
)
80+
files = glob.glob(glob_pattern)
81+
82+
if len(files) != 2:
83+
sys.exit(f"Expected 2 files in artifact, found {len(files)}: {files}")
84+
85+
archive_file = next((f for f in files if "sha256" not in f), None)
86+
checksum_file = next((f for f in files if "sha256" in f), None)
87+
88+
if not archive_file:
89+
sys.exit("Archive file not found in artifact")
90+
if not checksum_file:
91+
sys.exit("Checksum file not found in artifact")
92+
93+
return archive_file, checksum_file
94+
95+
96+
def verify_checksum(checksum_file: str, archive_file: str) -> None:
97+
"""Verify the checksum of the archive file."""
98+
if not Path(checksum_file).is_file():
99+
return
100+
101+
checksum, filename = parse_checksum_file(checksum_file)
102+
103+
if filename != archive_file:
104+
sys.exit(
105+
f"Checksum filename '{filename}' doesn't match archive '{archive_file}'"
106+
)
107+
108+
calculated_checksum = calculate_file_sha256(filename)
109+
if checksum != calculated_checksum:
110+
sys.exit("Checksum verification failed")
111+
112+
113+
def parse_checksum_file(checksum_file: str) -> Tuple[str, str]:
114+
"""Parse the checksum file and return the checksum and filename."""
115+
with open(checksum_file) as f:
116+
checksum_contents = f.read().strip()
117+
118+
try:
119+
checksum, filename = checksum_contents.split(None, 1)
120+
filename = filename.strip("* ")
121+
return checksum, filename
122+
except ValueError:
123+
sys.exit(f"Invalid checksum file format: {checksum_contents}")
124+
125+
126+
def calculate_file_sha256(filename: str) -> str:
127+
"""Calculate SHA256 hash of a file."""
128+
sha256 = hashlib.sha256()
129+
with open(filename, "rb") as f:
130+
for chunk in iter(lambda: f.read(8192), b""):
131+
sha256.update(chunk)
132+
return sha256.hexdigest()
133+
134+
135+
def extract_archive(archive_file: str) -> None:
136+
"""Extract the archive file."""
137+
if not Path(archive_file).is_file():
138+
return
139+
140+
if archive_file.endswith(".zip"):
141+
subprocess.run(["unzip", archive_file], check=True)
142+
else:
143+
subprocess.run(["tar", "xzf", archive_file], check=True)
144+
145+
146+
def verify_archive_contents(executable_name: str, include_changes_file: bool) -> None:
147+
"""Verify the contents of the extracted archive."""
148+
expected_files = ["README.md"]
149+
if include_changes_file:
150+
expected_files.append("Changes.md")
151+
expected_files.append(executable_name)
152+
153+
for file in expected_files:
154+
if not Path(file).is_file():
155+
sys.exit(f"Expected file '{file}' not found in archive")
156+
157+
# Check executable permissions
158+
exec_path = Path(executable_name)
159+
if not os.access(exec_path, os.X_OK):
160+
sys.exit(f"'{executable_name}' is not executable")
161+
162+
163+
if __name__ == "__main__":
164+
main()

0 commit comments

Comments
 (0)