Skip to content

Commit ffa90b2

Browse files
DanielRyanSmithDanielRyanSmithjcscottiii
authored
Check Chromium revision pinning scripts into repo (#4220)
* Check Chromium revision pinning scripts into repo * Update scripts/README.md Co-authored-by: James C Scott III <jcscottiii@users.noreply.github.com> * Changes suggested by @jcscottiii * make similar changes to update_chromium_revision * Pin other GH API calls --------- Co-authored-by: DanielRyanSmith <danielrsmith@google.com> Co-authored-by: James C Scott III <jcscottiii@users.noreply.github.com>
1 parent 2e298eb commit ffa90b2

4 files changed

Lines changed: 295 additions & 3 deletions

File tree

scripts/README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
1+
## Updating the pinned Chromium revision in WPT
2+
These scripts exists as Cloud Functions in GCP and will need to be redeployed
3+
after subsequent changes to the file.
4+
5+
_check_chromium_revision.py_
6+
7+
The purpose of this script is to find a new Chromium revision that is available
8+
for all major platforms (Win/Mac/Linux) and trigger the WPT CI check suite to
9+
run against this new revision.
10+
11+
12+
_update_chromium_revision.py_
13+
14+
The purpose of this script is to check the WPT CI check suite to see if all
15+
tests passed for the new revision, and to update the pinned revision if so.
16+
17+
The current PR used for running the check suites is at https://github.com/web-platform-tests/wpt/pull/50375
18+
119
## Build Test History
20+
_process_test_history.py_
21+
222
This script exists as a cloud function in GCP and will need to be redeployed
323
after subsequent changes to the file. The `BUCKET_NAME`, `PROJECT_NAME`,
424
and `RUNS_API_URL` constants will need to be changed based on which environment
@@ -30,7 +50,7 @@ command.
3050
will take a considerable amount of time (hours).
3151

3252
```sh
33-
python scripts/build_test_history.py -v --delete-history-entities
53+
python scripts/process_test_history.py -v --delete-history-entities
3454
```
3555

3656
Additionally, the `Date` property of the
@@ -40,7 +60,7 @@ in the CLI in ISO format.
4060

4161
```sh
4262
# Set history processing start date to the beginning of 2023
43-
python scripts/build_test_history.py --set-history-start-date=2023-01-01T00:00:00.000Z
63+
python scripts/process_test_history.py --set-history-start-date=2023-01-01T00:00:00.000Z
4464
```
4565

4666
Once all entities have been deleted, new JSON files will need to be generated
@@ -53,5 +73,5 @@ is stopped early, entities will again need to be deleted and this command
5373
will need to be re-invoked.
5474

5575
```sh
56-
python scripts/build_test_history.py --generate-new-statuses-json
76+
python scripts/process_test_history.py --generate-new-statuses-json
5777
```

scripts/check_chromium_revision.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright 2025 The WPT Dashboard Project. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
"""
6+
This script requires the following environment variables to be set:
7+
8+
GIT_CHECK_PR_STATUS_TOKEN: A GitHub personal access token with permissions to
9+
update pull request statuses.
10+
REPO_OWNER: The owner of the GitHub repository (e.g., "owner_name").
11+
REPO_NAME: The name of the GitHub repository (e.g., "repo_name").
12+
PR_NUMBER: The number of the pull request.
13+
14+
Please ensure these variables are configured before running the script.
15+
"""
16+
17+
import os
18+
import requests
19+
from time import time
20+
from google.cloud import storage
21+
22+
DEFAULT_TIMEOUT = 600.0
23+
BUCKET_NAME = 'wpt-versions'
24+
NEW_REVISION_FILE = 'pinned_chromium_revision_NEW'
25+
OLD_REVISION_FILE = 'pinned_chromium_revision'
26+
PLATFORM_INFO = [
27+
("Win_x64", "chrome-win.zip"),
28+
("Win", "chrome-win.zip"),
29+
("Linux_x64", "chrome-linux.zip"),
30+
("Mac", "chrome-mac.zip")
31+
]
32+
SNAPSHOTS_PATH = "https://storage.googleapis.com/chromium-browser-snapshots/"
33+
34+
35+
def trigger_ci_tests() -> str | None:
36+
# Reopen the PR to run the CI tests.
37+
s = requests.Session()
38+
s.headers.update({
39+
"Authorization": f"token {get_token()}",
40+
# Specified API version. See https://docs.github.com/en/rest/about-the-rest-api/api-versions
41+
"X-GitHub-Api-Version": "2022-11-28",
42+
})
43+
repo_owner = os.environ["REPO_OWNER"]
44+
repo_name = os.environ["REPO_NAME"]
45+
pr_number = os.environ["PR_NUMBER"]
46+
url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}"
47+
48+
response = s.patch(url, data='{"state": "closed"}')
49+
if response.status_code != 200:
50+
return f'Failed to close PR {pr_number}'
51+
52+
response = s.patch(url, data='{"state": "open"}')
53+
if response.status_code != 200:
54+
return f'Failed to open PR {pr_number}'
55+
56+
57+
def get_token() -> str | None:
58+
"""Get token to check on the CI runs."""
59+
return os.environ["GIT_CHECK_PR_STATUS_TOKEN"]
60+
61+
62+
def get_start_revision() -> int:
63+
"""Get the latest revision for Linux as a starting point to check for a
64+
valid revision for all platforms."""
65+
try:
66+
url = f"{SNAPSHOTS_PATH}Linux_x64/LAST_CHANGE"
67+
start_revision = int(requests.get(url).text.strip())
68+
except requests.RequestException as e:
69+
raise requests.RequestException(f"Failed LAST_CHANGE lookup: {e}")
70+
71+
return start_revision
72+
73+
74+
def check_new_chromium_revision() -> str:
75+
"""Find a new Chromium revision that is available for all major platforms (Win/Mac/Linux)"""
76+
timeout = DEFAULT_TIMEOUT
77+
start = time()
78+
79+
# Load existing pinned revision.
80+
storage_client = storage.Client()
81+
bucket = storage_client.bucket(BUCKET_NAME)
82+
# Read new revision number.
83+
blob = bucket.blob(OLD_REVISION_FILE)
84+
existing_revision = int(blob.download_as_string())
85+
86+
start_revision = get_start_revision()
87+
88+
if start_revision == existing_revision:
89+
print("No new revision.")
90+
return "No new revision."
91+
92+
# Step backwards through revision numbers until we find one
93+
# that is available for all platforms.
94+
candidate_revision = start_revision
95+
new_revision = -1
96+
timed_out = False
97+
while new_revision == -1 and candidate_revision > existing_revision:
98+
available_for_all = True
99+
# For each platform, check if Chromium is available for download from snapshots.
100+
for platform, filename in PLATFORM_INFO:
101+
try:
102+
url = (f"{SNAPSHOTS_PATH}{platform}/"
103+
f"{candidate_revision}/{filename}")
104+
# Check the headers of each possible download URL.
105+
r = requests.head(url)
106+
# If the file is not available for download, decrement the revision and try again.
107+
if r.status_code != 200:
108+
candidate_revision -= 1
109+
available_for_all = False
110+
break
111+
except requests.RequestException:
112+
print(f"Failed to fetch headers for revision {candidate_revision}. Skipping it.")
113+
candidate_revision -= 1
114+
available_for_all = False
115+
break
116+
117+
if available_for_all:
118+
new_revision = candidate_revision
119+
if time() - start > timeout:
120+
timed_out = True
121+
break
122+
123+
end = time()
124+
if timed_out:
125+
raise Exception(f"Reached timeout {timeout}s while checking revision {candidate_revision}")
126+
127+
if new_revision <= existing_revision:
128+
message = ("No new mutually available revision found after "
129+
f"{'{:.2f}'.format(end - start)} seconds. Keeping revision {existing_revision}.")
130+
print(message)
131+
return message
132+
133+
134+
# Replace old revision number with new number.
135+
blob = bucket.blob(NEW_REVISION_FILE)
136+
blob.upload_from_string(str(new_revision))
137+
pr_error_msg = trigger_ci_tests()
138+
message = (f"Found mutually available revision at {new_revision}.\n"
139+
f"This process started at {start_revision} and checked "
140+
f"{start_revision - new_revision} revisions.\n"
141+
f"The whole process took {'{:.2f}'.format(end - start)} seconds.\n")
142+
if pr_error_msg:
143+
raise Exception(f"PR interaction error: {pr_error_msg}")
144+
print(message)
145+
return message
146+
147+
148+
def main(args, _) -> None:
149+
return check_new_chromium_revision()
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright 2025 The WPT Dashboard Project. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
"""
6+
This script requires the following environment variables to be set:
7+
8+
GIT_CHECK_PR_STATUS_TOKEN: A GitHub personal access token with permissions to
9+
update pull request statuses.
10+
REPO_OWNER: The owner of the GitHub repository (e.g., "owner_name").
11+
REPO_NAME: The name of the GitHub repository (e.g., "repo_name").
12+
PR_NUMBER: The number of the pull request.
13+
14+
Please ensure these variables are configured before running the script.
15+
"""
16+
17+
import os
18+
import requests
19+
from datetime import date
20+
from google.cloud import storage
21+
22+
23+
BUCKET_NAME = 'wpt-versions'
24+
NEW_REVISION_FILE = 'pinned_chromium_revision_NEW'
25+
OLD_REVISION_FILE = 'pinned_chromium_revision'
26+
27+
28+
def all_passing_checks(repo_owner: str, repo_name: str, pr_number: str) -> bool:
29+
"""Check if all CI tests passed."""
30+
s = requests.Session()
31+
sha = get_sha(repo_owner, repo_name, pr_number)
32+
s.headers.update(get_github_api_headers())
33+
url = f'https://api.github.com/repos/{repo_owner}/{repo_name}/commits/{sha}/check-suites'
34+
response = s.get(url)
35+
if response.status_code != 200:
36+
print(f'Received response status {response.status_code} from {url}')
37+
check_info = response.json()
38+
for check in check_info['check_suites']:
39+
if check['conclusion'] != 'success':
40+
return False
41+
return True
42+
43+
44+
def update_pr_body(
45+
new_revision: str,
46+
tests_passed: bool,
47+
repo_owner: str,
48+
repo_name: str,
49+
pr_number: str,
50+
) -> bool:
51+
outcome = 'Passed' if tests_passed else 'Failed'
52+
body = (
53+
'This pull request is used for automated runs of the WPT check suites '
54+
'against a new available Chromium revision. If all tests pass, the new '
55+
'revision will be pinned for use.\\n\\nLast revision checked: '
56+
f'{new_revision.decode('utf-8')}\\nCheck run date: {date.today()}'
57+
f'\\nOutcome: **{outcome}**'
58+
)
59+
60+
body = '{"body":"' + body + '"}'
61+
s = requests.Session()
62+
s.headers.update(get_github_api_headers())
63+
url = f'https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}'
64+
response = s.patch(url, data=body)
65+
return response.status_code == 200
66+
67+
68+
def get_new_revision() -> str:
69+
storage_client = storage.Client()
70+
bucket = storage_client.bucket(BUCKET_NAME)
71+
blob = bucket.blob(NEW_REVISION_FILE)
72+
return blob.download_as_string()
73+
74+
75+
def update_chromium_revision(new_revision) -> None:
76+
storage_client = storage.Client()
77+
bucket = storage_client.bucket(BUCKET_NAME)
78+
79+
# Replace old revision number with new number.
80+
blob = bucket.blob(OLD_REVISION_FILE)
81+
blob.upload_from_string(new_revision)
82+
83+
84+
def get_github_api_headers():
85+
return {
86+
'Authorization': f'token {get_token()}',
87+
# Specified API version. See https://docs.github.com/en/rest/about-the-rest-api/api-versions
88+
'X-GitHub-Api-Version': '2022-11-28',
89+
}
90+
91+
92+
def get_token() -> str:
93+
"""Get token to check on the CI runs."""
94+
return os.environ['GIT_CHECK_PR_STATUS_TOKEN']
95+
96+
97+
def get_sha(repo_owner: str, repo_name: str, pr_number: str) -> str:
98+
"""Get head sha from PR."""
99+
s = requests.Session()
100+
s.headers.update(get_github_api_headers())
101+
url = f'https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}'
102+
response = s.get(url)
103+
pr_info = response.json()
104+
return pr_info['head']['sha']
105+
106+
107+
def main(args, _):
108+
repo_owner = os.environ['REPO_OWNER']
109+
repo_name = os.environ['REPO_NAME']
110+
pr_number = os.environ['PR_NUMBER']
111+
112+
tests_passed = all_passing_checks(repo_owner, repo_name, pr_number)
113+
new_revision = get_new_revision()
114+
115+
if tests_passed:
116+
update_chromium_revision(new_revision)
117+
if not update_pr_body(new_revision, tests_passed, repo_owner, repo_name, pr_number):
118+
print('Failed to update PR body description.')
119+
if tests_passed:
120+
print(f'Revision updated to {new_revision}.')
121+
return f'Revision updated to {new_revision}.'
122+
print(f'Some checks failed for PR {pr_number}. Revision not updated.')
123+
return f'Some checks failed for PR {pr_number}. Revision not updated.'

0 commit comments

Comments
 (0)