Skip to content

Commit 3eede93

Browse files
committed
Replace check-release.pl with check-release.py
1 parent fc369a5 commit 3eede93

File tree

7 files changed

+179
-5100
lines changed

7 files changed

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

0 commit comments

Comments
 (0)