renderdiff: [viewer] add Run ID as a way to pull artifacts (#9272)

The viewer supports pulling artifacts based on PR number, and now
we support providing Run ID as an alternative to identify the
renderdiff run on Github CI.
This commit is contained in:
Powei Feng
2025-09-30 14:32:36 -07:00
committed by GitHub
parent 73ba8fc753
commit 10e63bf2cf
2 changed files with 95 additions and 76 deletions

View File

@@ -138,6 +138,17 @@ python3 test/renderdiff/src/viewer.py --pr_number=[PR #] --github_token=[github
where `[PR #]` is the numeric ID of your pull request, and the `[github token]` is an acess
token that you (as a github user) needs to generate ([reference][github_token_ref]).
To see the results of a specific run, you would do the following
```
python3 test/renderdiff/src/viewer.py --run_number=[RUN #] --github_token=[github token]
```
where `[RUN #]` is the numeric ID of the run. You can find the run number in the URL of the
GitHub Actions page. For example, in the URL
`https://github.com/google/filament/actions/runs/18023632663/job/51286323708?pr=9264`,
the run number is `18023632663`.
[github_token_ref]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
[Mesa]: https://docs.mesa3d.org
[SwiftShader]: https://github.com/google/swiftshader

View File

@@ -28,27 +28,67 @@ from flask import Flask, request, make_response, send_from_directory
DIR = pathlib.Path(__file__).parent.absolute()
HTML_DIR = os.path.join(DIR, "viewer_html")
def _download_and_extract_artifacts(run_id, headers, output_dir):
OWNER_REPO = 'google/filament'
artifacts_url = f"https://api.github.com/repos/{OWNER_REPO}/actions/runs/{run_id}/artifacts"
downloaded_any_artifact = False
try:
response = requests.get(artifacts_url, headers=headers)
response.raise_for_status()
artifacts_data = response.json()
artifacts = artifacts_data.get("artifacts", [])
if not artifacts:
print(f" No artifacts found for workflow run ID {run_id}.")
return downloaded_any_artifact
for artifact in artifacts:
artifact_id = artifact["id"]
artifact_name = artifact["name"]
archive_download_url = artifact["archive_download_url"]
print(f" Found artifact: '{artifact_name}' (ID: {artifact_id})")
# Perform the download request
print(f" Downloading '{artifact_name}'...")
# Use a copy of headers and specific Accept for ZIP download
download_headers = headers.copy()
download_headers["Accept"] = "application/vnd.github.v3+zip"
download_response = requests.get(archive_download_url, headers=download_headers, stream=True)
download_response.raise_for_status() # Check for errors in download
# --- Step 5: Extract the contents ---
# Use BytesIO to handle the zip file content in memory without saving to a temporary file
with io.BytesIO(download_response.content) as zip_buffer:
try:
with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
# Create a unique subdirectory for each artifact to avoid file name conflicts
extract_path = os.path.join(output_dir, f"{artifact_name}_{artifact_id}")
os.makedirs(extract_path, exist_ok=True)
zip_ref.extractall(extract_path)
print(f" Successfully extracted '{artifact_name}' to '{extract_path}/'")
downloaded_any_artifact = True
except zipfile.BadZipFile:
print(f" Error: Downloaded file for '{artifact_name}' is not a valid zip file. Skipping extraction.")
except Exception as e:
print(f" An error occurred during extraction of '{artifact_name}': {e}")
return downloaded_any_artifact
except requests.exceptions.HTTPError as e:
print(f" An HTTP error occurred while fetching artifacts for run {run_id}: {e}")
if e.response.status_code == 403:
print(" This often means you need a GitHub Personal Access Token with 'repo' scope (even for public repos for artifact downloads).")
except requests.exceptions.RequestException as e:
print(f" A network error occurred while fetching artifacts for run {run_id}: {e}")
def _download_github_artifacts_by_run(run_id, github_token, output_dir= ".") -> None:
headers = {"Accept": "application/vnd.github.v3+json"}
if github_token:
headers["Authorization"] = f"token {github_token}"
os.makedirs(output_dir, exist_ok=True)
return _download_and_extract_artifacts(run_id, headers, output_dir)
# Generated by gemini
def _download_github_artifacts(pr_number, github_token, output_dir= ".") -> None:
"""
Downloads artifacts associated with a specific GitHub Pull Request.
This function performs the following steps:
1. Fetches the details of the Pull Request to get its head commit SHA.
2. Searches for GitHub Actions workflow runs triggered by that specific commit.
3. Iterates through successful workflow runs to find and list all associated artifacts.
4. Downloads each artifact (which comes as a ZIP file).
5. Extracts the contents of each downloaded ZIP file into a unique subdirectory
within the specified output directory.
Args:
owner (str): The GitHub repository owner (e.g., "octocat").
repo (str): The GitHub repository name (e.g., "Spoon-Knife").
pr_number (int): The Pull Request number.
output_dir (str): The local directory where downloaded artifacts will be saved.
Defaults to the current directory.
"""
# Prepare HTTP headers for GitHub API requests
headers = {"Accept": "application/vnd.github.v3+json"}
if github_token:
@@ -119,60 +159,9 @@ def _download_github_artifacts(pr_number, github_token, output_dir= ".") -> None
run_id = run["id"]
run_name = run["name"]
print(f"\nProcessing workflow run '{run_name}' (ID: {run_id})...")
artifacts_url = f"https://api.github.com/repos/{OWNER_REPO}/actions/runs/{run_id}/artifacts"
try:
response = requests.get(artifacts_url, headers=headers)
response.raise_for_status()
artifacts_data = response.json()
artifacts = artifacts_data.get("artifacts", [])
if not artifacts:
print(f" No artifacts found for workflow run ID {run_id}.")
continue # Move to the next workflow run
for artifact in artifacts:
artifact_id = artifact["id"]
artifact_name = artifact["name"]
archive_download_url = artifact["archive_download_url"]
print(f" Found artifact: '{artifact_name}' (ID: {artifact_id})")
# Perform the download request
print(f" Downloading '{artifact_name}'...")
# Use a copy of headers and specific Accept for ZIP download
download_headers = headers.copy()
download_headers["Accept"] = "application/vnd.github.v3+zip"
download_response = requests.get(archive_download_url, headers=download_headers, stream=True)
download_response.raise_for_status() # Check for errors in download
# --- Step 5: Extract the contents ---
# Use BytesIO to handle the zip file content in memory without saving to a temporary file
with io.BytesIO(download_response.content) as zip_buffer:
try:
with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
# Create a unique subdirectory for each artifact to avoid file name conflicts
extract_path = os.path.join(output_dir, f"{artifact_name}_{artifact_id}")
os.makedirs(extract_path, exist_ok=True)
zip_ref.extractall(extract_path)
print(f" Successfully extracted '{artifact_name}' to '{extract_path}/'")
downloaded_any_artifact = True
except zipfile.BadZipFile:
print(f" Error: Downloaded file for '{artifact_name}' is not a valid zip file. Skipping extraction.")
except Exception as e:
print(f" An error occurred during extraction of '{artifact_name}': {e}")
# Once we find the lastest run with artifacts, we just quit
if len(artifacts) > 0:
downloaded_any_artifact = _download_and_extract_artifacts(run_id, headers, output_dir)
if downloaded_any_artifact:
break
except requests.exceptions.HTTPError as e:
print(f" An HTTP error occurred while fetching artifacts for run {run_id}: {e}")
if e.response.status_code == 403:
print(" This often means you need a GitHub Personal Access Token with 'repo' scope (even for public repos for artifact downloads).")
continue # Continue processing the next workflow run
except requests.exceptions.RequestException as e:
print(f" A network error occurred while fetching artifacts for run {run_id}: {e}")
continue # Continue processing the next workflow run
if not downloaded_any_artifact:
print("\nNo artifacts were downloaded for the specified PR.")
@@ -225,15 +214,20 @@ if __name__ == '__main__':
parser = ArgParseImpl()
parser.add_argument('--diff', type=str, help='Diff result directory')
parser.add_argument('--pr_number', type=str, help='Pull request artifacts to examine')
parser.add_argument('--run_number', type=str, help='Run number to examine')
parser.add_argument('--github_token', type=str, help='Necessary for pull PR artifacts')
args, _ = parser.parse_known_args(sys.argv[1:])
if not args.diff and not args.pr_number:
print('Need to specify either a diff result directory or a Pull Request number')
if not args.diff and not args.pr_number and not args.run_number:
print('Need to specify either a diff result directory, a Pull Request number, or a run number')
exit(1)
if args.diff and args.pr_number:
print('Cannot specify both a diff result directory and a Pull Request number')
if args.diff and (args.pr_number or args.run_number):
print('Cannot specify both a diff result directory and a Pull Request/run number')
exit(1)
if args.pr_number and args.run_number:
print('Cannot specify both a PR number and a run number')
exit(1)
fdir = args.diff
@@ -251,6 +245,20 @@ if __name__ == '__main__':
directory_name = list(os.listdir(output_dir))[0]
fdir = os.path.join(os.path.join(output_dir, directory_name), 'diffs/presubmit')
if args.run_number:
if not args.github_token:
print('Must provide --github_token to be able to download artifacts')
exit(1)
output_dir = f'/tmp/filament-run{args.run_number}-rdiff-result'
res = _download_github_artifacts_by_run(args.run_number, args.github_token, output_dir)
if not res:
print('Failed to retrieve run artifacts')
exit(1)
# TODO: Clean up the following so that we're not so specific on the paths diffs/presubmit
directory_name = list(os.listdir(output_dir))[0]
fdir = os.path.join(os.path.join(output_dir, directory_name), 'diffs/presubmit')
with open(os.path.join(fdir, 'compare_results.json'), 'r') as f:
config = json.loads(f.read())
config['diff_dir'] = os.path.abspath(fdir)