Skip to content

Commit e70a4b2

Browse files
committed
review
1 parent 832e95b commit e70a4b2

5 files changed

Lines changed: 92 additions & 12 deletions

File tree

.github/workflows/check_action_tags.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,20 @@ on:
2424
branches:
2525
- main
2626
paths:
27-
- ".github/workflows/dummy.yml"
27+
- ".github/workflows/check_action_tags.yml"
28+
- ".github/workflows/update.yml"
29+
- ".github/actions/for-dependabot-triggered-reviews/action.yml"
30+
- "actions.yml"
31+
- "approved_patterns.yml"
32+
- "gateway/**"
2833
pull_request:
2934
paths:
30-
- ".github/workflows/update_actions.yml"
31-
- ".github/workflows/dummy.yml"
32-
- gateway/*
35+
- ".github/workflows/check_action_tags.yml"
36+
- ".github/workflows/update.yml"
37+
- ".github/actions/for-dependabot-triggered-reviews/action.yml"
38+
- "actions.yml"
39+
- "approved_patterns.yml"
40+
- "gateway/**"
3341

3442
permissions:
3543
contents: read

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,9 @@ Two workflows in `.github/workflows/` run `verify-action-build` on PRs that touc
273273

274274
- **`verify` job in `verify_dependabot_action.yml`** — triggers on Dependabot PRs that modify `.github/actions/for-dependabot-triggered-reviews/action.yml`. Extracts the action reference from the PR, rebuilds the compiled JavaScript in Docker, and compares it against the published version.
275275
- **`verify` job in `verify_manual_action.yml`** — triggers on human-authored PRs that modify `actions.yml` or `approved_patterns.yml` (i.e. manual allow-list additions / version bumps). Dependabot-authored PRs are skipped, since they are already covered by the workflow above.
276+
- **`check_action_tags` job in `check_action_tags.yml`** — triggers when `actions.yml`, `approved_patterns.yml`, the generated Dependabot composite action, the update workflow, or gateway verification code changes. It verifies that configured action SHAs exist and, when a `tag` is recorded, that the SHA is reachable from that Git tag or branch.
276277

277-
Both workflows use a regular `pull_request` trigger with read-only permissions and no PR comments — pass/fail is surfaced through the status check. Neither workflow auto-approves or merges; a human reviewer must still approve.
278+
These workflows use regular `pull_request` triggers with read-only permissions and no PR comments — pass/fail is surfaced through the status check. They do not auto-approve or merge; a human reviewer must still approve.
278279

279280
The script exits with code **1** (failure) when something is unexpectedly broken — for example, the action cannot be compiled, the rebuilt JavaScript is invalid, or required tools are missing. In all other cases it exits with code **0** and produces reviewable diffs: a large diff does not by itself cause an error (e.g. major version bumps will naturally have big diffs). It is always up to a human reviewer to inspect the output, assess the changes, and decide whether the update is safe to approve.
280281

gateway/action_tags.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def _gh_api_get(url_abspath: str) -> ApiResponse:
8585
# Unauthorized GH API requests are quite rate-limited.
8686
# Tip: add an extra space before 'export' to prevent adding the line to the shell history.
8787
# export GH_TOKEN=$(gh auth token)
88-
gh_token = os.environ['GH_TOKEN']
88+
gh_token = os.environ.get('GH_TOKEN')
8989
if gh_token:
9090
headers['Authorization'] = f"Bearer {gh_token}"
9191
req_url = f"https://api.github.com{url_abspath}"
@@ -120,7 +120,7 @@ def _gh_compare(owner_repo: str, tag: str, requested_sha: str) -> ApiResponse:
120120
requested_sha = urllib.parse.quote(requested_sha, safe="")
121121
return _gh_api_get(f"/repos/{owner_repo}/compare/{requested_sha}...{tag}")
122122

123-
def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = True, today: date = date.today()) -> ActionTagsCheckResult:
123+
def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = True, today: date | None = None) -> ActionTagsCheckResult:
124124
"""
125125
Validates the contents of the actions file against GitHub.
126126
@@ -152,9 +152,12 @@ def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = Tru
152152
log_to_console: Whether to log messages immediately to the console (default: True)
153153
today: The current date (default: today)
154154
"""
155+
if today is None:
156+
today = date.today()
157+
155158
if on_gha():
156159
print(f"::group::Verify GitHub Actions")
157-
gh_token = os.environ['GH_TOKEN']
160+
gh_token = os.environ.get('GH_TOKEN')
158161
if not gh_token or len(gh_token) == 0:
159162
raise Exception("GH_TOKEN environment variable is not set or empty")
160163

@@ -280,7 +283,6 @@ def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = Tru
280283
result.warning(f"GitHub action {name} references an invalid Git SHA but 'ignore_invalid_git_sha' is set: will ignore invalid Git SHA '{ref}'", " ..")
281284
else:
282285
result.failure(f"GitHub action {name} references an invalid Git SHA '{ref}'", " ..")
283-
raise Exception("foo")
284286

285287
for req_tag, req_shas in requested_shas_by_tag.items():
286288
result.log(f" .. checking tag '{req_tag}'")
@@ -377,7 +379,7 @@ def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = Tru
377379
f.write('```\n')
378380
for msg in result.warnings:
379381
f.write(f"{msg}\n\n")
380-
f.write('```\n')
382+
f.write('```\n')
381383
f.write(f"## Log\n")
382384
f.write('```\n')
383385
for msg in result.logs:

gateway/run_action_tags.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
# Helper script to run the action tag verification.
2222
#
23-
# You must have a GH_TOKEN environment variable set to a GitHub PAT w/ read accessto public repos.
23+
# You must have a GH_TOKEN environment variable set to a GitHub PAT w/ read access to public repos.
2424
#
2525
# From a local environment use:
2626
# uvx --with ruyaml python gateway/run_action_tags.py
@@ -36,7 +36,7 @@
3636

3737
def run_main():
3838
if not 'GH_TOKEN' in os.environ:
39-
raise Exception("GH_TOKEN environment variable should be must.")
39+
raise Exception("GH_TOKEN environment variable must be set.")
4040

4141
cwd = Path(os.getcwd())
4242
dummy_workflow = cwd / ".github/actions/for-dependabot-triggered-reviews/action.yml"
@@ -46,6 +46,8 @@ def run_main():
4646
if not dummy_workflow.exists() or not actions_yaml.exists() or not approved_patterns_yaml.exists():
4747
raise Exception(f"Missing required files: {dummy_workflow.absolute()}, {actions_yaml.absolute()}, {approved_patterns_yaml.absolute()}")
4848

49+
# Keep the verification inputs in sync with the generated allowlist artifacts
50+
# before checking tags, matching the update workflow's behavior.
4951
update_actions(dummy_workflow, actions_yaml)
5052
update_patterns(approved_patterns_yaml, actions_yaml)
5153

gateway/test_action_tags.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ def test_sha_non_existent():
6363
]
6464
assert result.warnings == []
6565

66+
def test_invalid_sha_records_failure_without_crashing():
67+
# noinspection PyTypeChecker
68+
result = verify_actions({
69+
"dtolnay/rust-toolchain": {
70+
"stable": {
71+
},
72+
},
73+
})
74+
75+
assert result.failures == [
76+
"GitHub action dtolnay/rust-toolchain references an invalid Git SHA 'stable'"
77+
]
78+
assert result.warnings == []
79+
6680
@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.")
6781
def test_tag_sha_vs_commit_sha():
6882
# noinspection PyTypeChecker
@@ -195,6 +209,59 @@ def test_branch_compare_api_failure():
195209
]
196210
assert result.warnings == []
197211

212+
def test_branch_compare_api_failure_can_be_ignored():
213+
with (
214+
mock.patch("action_tags._gh_matching_tags", return_value=_api_response(200, "[]")),
215+
mock.patch("action_tags._gh_get_branch", return_value=_api_response(200, "{}")),
216+
mock.patch(
217+
"action_tags._gh_compare",
218+
return_value=_api_response(500, "compare failed", "Internal Server Error"),
219+
),
220+
):
221+
# noinspection PyTypeChecker
222+
result = verify_actions({
223+
"dtolnay/rust-toolchain": {
224+
DTOLNAY_RUST_TOOLCHAIN_SHA: {
225+
"tag": "stable",
226+
"ignore_gh_api_errors": True,
227+
},
228+
},
229+
})
230+
231+
assert result.failures == []
232+
assert result.warnings == [
233+
"ignore_gh_api_errors is set to true: will ignore GH API errors for action "
234+
f"dtolnay/rust-toolchain ref '{DTOLNAY_RUST_TOOLCHAIN_SHA}'",
235+
"Failed to find Git SHA "
236+
f"'{DTOLNAY_RUST_TOOLCHAIN_SHA}' on Git branch 'stable' in GitHub repo "
237+
"'https://github.com/dtolnay/rust-toolchain': HTTP/500: Internal Server Error, "
238+
"API URL: https://api.github.test\ncompare failed",
239+
]
240+
241+
def test_branch_lookup_api_failure_can_be_ignored():
242+
with (
243+
mock.patch("action_tags._gh_matching_tags", return_value=_api_response(200, "[]")),
244+
mock.patch("action_tags._gh_get_branch", return_value=_api_response(500, "branch failed", "Internal Server Error")),
245+
):
246+
# noinspection PyTypeChecker
247+
result = verify_actions({
248+
"dtolnay/rust-toolchain": {
249+
DTOLNAY_RUST_TOOLCHAIN_SHA: {
250+
"tag": "stable",
251+
"ignore_gh_api_errors": True,
252+
},
253+
},
254+
})
255+
256+
assert result.failures == []
257+
assert result.warnings == [
258+
"ignore_gh_api_errors is set to true: will ignore GH API errors for action "
259+
f"dtolnay/rust-toolchain ref '{DTOLNAY_RUST_TOOLCHAIN_SHA}'",
260+
"Failed to check Git branch 'stable' against GitHub repo "
261+
"'https://github.com/dtolnay/rust-toolchain': HTTP/500: Internal Server Error, "
262+
"API URL: https://api.github.test\nbranch failed",
263+
]
264+
198265
def test_missing_branch_falls_back_to_missing_tag_failure():
199266
with (
200267
mock.patch("action_tags._gh_matching_tags", return_value=_api_response(200, "[]")),

0 commit comments

Comments
 (0)