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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user