Repository navigation
fix(ci): run task sync without private action downloads #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
avivka
wants to merge
2
commits into
main
Choose a base branch
from
aviv/bugfix/public-task-id-check
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| #!/usr/bin/env python3 | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Public-fork task validation without downloading private executable code.""" | ||
|
|
||
| import base64 | ||
| import json | ||
| import os | ||
| import re | ||
| import sys | ||
| import urllib.error | ||
| import urllib.parse | ||
| import urllib.request | ||
| from pathlib import Path | ||
|
|
||
| BOT_AUTHORS = { | ||
| "release-please[bot]", | ||
| "release-please-app-globster[bot]", | ||
| "dependabot[bot]", | ||
| "github-actions[bot]", | ||
| } | ||
| WORKSPACE = "/repos/globster-monday/workspace" | ||
|
|
||
|
|
||
| def parse_task_id(author, body): | ||
| if author in BOT_AUTHORS: | ||
| return "none" | ||
| fields = re.findall(r"^[ \t]*Task-ID:([^\r\n]*)", body or "", re.MULTILINE) | ||
| if len(fields) != 1: | ||
| raise ValueError("PR body must have exactly one Task-ID field") | ||
| task_id = fields[0].strip() | ||
| if task_id != "none" and not re.fullmatch(r"[0-9]{2}\.[0-9]{2}\.[0-9]{2}", task_id): | ||
| raise ValueError("Task-ID must be NN.NN.NN or none") | ||
| return task_id | ||
|
|
||
|
|
||
| def verify_task(task_id, request): | ||
| if not re.fullmatch(r"[0-9]{2}\.[0-9]{2}\.[0-9]{2}", task_id): | ||
| raise ValueError("Invalid task identity") | ||
| tree = request(f"{WORKSPACE}/git/trees/main?recursive=1") | ||
| if tree.get("truncated"): | ||
| raise ValueError( | ||
| "Workspace tree is incomplete; task identity cannot be verified" | ||
| ) | ||
| pattern = ( | ||
| rf"work/epics/[^/]+/stories/[^/]+/tasks/{re.escape(task_id)}-[^/]+/README\.md" | ||
| ) | ||
| matches = [ | ||
| item["path"] | ||
| for item in tree["tree"] | ||
| if re.fullmatch(pattern, item.get("path", "")) | ||
| ] | ||
| if len(matches) != 1: | ||
| raise ValueError("Task-ID must resolve to exactly one Workspace task") | ||
| path = urllib.parse.quote(matches[0], safe="/") | ||
| content = request(f"{WORKSPACE}/contents/{path}?ref=main") | ||
| if content.get("encoding") != "base64": | ||
| raise ValueError("Workspace task content is unavailable") | ||
| readme = base64.b64decode(content["content"]).decode("utf-8") | ||
| frontmatter = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|$)", readme, re.DOTALL) | ||
| if not frontmatter: | ||
| raise ValueError("Workspace task metadata is unavailable") | ||
|
|
||
| def field(name): | ||
| values = re.findall(rf"^{name}:[ \t]*([^\r\n]*)", frontmatter[1], re.MULTILINE) | ||
| if len(values) != 1: | ||
| raise ValueError("Workspace task metadata is ambiguous") | ||
| return values[0].split("#", 1)[0].strip().strip("'\"") | ||
|
|
||
| if field("type") != "task" or field("id") != task_id: | ||
| raise ValueError("Workspace task identity does not match") | ||
| if field("status") not in {"ready", "in_progress"}: | ||
| raise ValueError("Workspace task is not actionable") | ||
| if not re.fullmatch(r"[0-9]+", field("monday_item_id")): | ||
| raise ValueError("Workspace task has not been mirrored") | ||
|
|
||
|
|
||
| def merged_task_event(event): | ||
| pr = event["pull_request"] | ||
| if not pr.get("merged") or pr["base"]["ref"] != "main": | ||
| raise ValueError("Only a PR merged into main may complete a task") | ||
| task_id = parse_task_id(pr["user"]["login"], pr.get("body")) | ||
| if task_id == "none": | ||
| return None | ||
| repository = pr["base"]["repo"]["full_name"] | ||
| merge_sha = pr["merge_commit_sha"] | ||
| if not re.fullmatch(r"[0-9a-f]{40}", merge_sha): | ||
| raise ValueError("Missing authoritative merge commit") | ||
| return { | ||
| "event_type": "task-merged", | ||
| "client_payload": { | ||
| "task_id": task_id, | ||
| "commit_url": f"https://github.com/{repository}/commit/{merge_sha}", | ||
| "repository": repository, | ||
| "pr_number": str(pr["number"]), | ||
| "pr_url": pr["html_url"], | ||
| "pr_title": pr["title"], | ||
| "pr_author_id": str(pr["user"]["id"]), | ||
| "pr_author_login": pr["user"]["login"], | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def github_request(path, data=None): | ||
| request = urllib.request.Request( | ||
| f"https://api.github.com{path}", | ||
| data=json.dumps(data).encode() if data is not None else None, | ||
| headers={ | ||
| "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}", | ||
| "Accept": "application/vnd.github+json", | ||
| "X-GitHub-Api-Version": "2022-11-28", | ||
| "Content-Type": "application/json", | ||
| }, | ||
| ) | ||
| with urllib.request.urlopen(request, timeout=30) as response: | ||
| return None if response.status == 204 else json.load(response) | ||
|
|
||
|
|
||
| def main(): | ||
| if sys.argv[1:] == ["parse"]: | ||
| event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text()) | ||
| pr = event["pull_request"] | ||
| task_id = parse_task_id(pr["user"]["login"], pr.get("body")) | ||
| with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output: | ||
| output.write(f"task_id={task_id}\n") | ||
| elif sys.argv[1:] == ["verify"]: | ||
| verify_task(os.environ["TASK_ID"], github_request) | ||
| print("Workspace task identity, actionable status and mirror verified") | ||
| elif sys.argv[1:] == ["notify"]: | ||
| event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text()) | ||
| payload = merged_task_event(event) | ||
| if payload is not None: | ||
| github_request(f"{WORKSPACE}/dispatches", payload) | ||
| print("Merged task dispatched to Workspace for authoritative verification") | ||
| else: | ||
| raise ValueError("Expected parse, verify or notify") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| try: | ||
| main() | ||
| except (ValueError, KeyError, TypeError, urllib.error.URLError): | ||
| print( | ||
| "::error::Task metadata validation failed; check the PR and task-sync app access" | ||
| ) | ||
| sys.exit(1) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import base64 | ||
| import unittest | ||
|
|
||
| from check_task_id import merged_task_event, parse_task_id, verify_task | ||
|
|
||
|
|
||
| class TaskIdTests(unittest.TestCase): | ||
| def test_exact_field_and_release_exceptions(self): | ||
| self.assertEqual( | ||
| parse_task_id("owner", "Summary\r\nTask-ID: 34.01.03\r\n"), "34.01.03" | ||
| ) | ||
| self.assertEqual(parse_task_id("owner", "Task-ID: none"), "none") | ||
| self.assertEqual(parse_task_id("dependabot[bot]", None), "none") | ||
| for body in [None, "Task-ID: 34.1.3", "Task-ID: 34.01.03\nTask-ID: none"]: | ||
| with self.subTest(body=body), self.assertRaises(ValueError): | ||
| parse_task_id("owner", body) | ||
|
|
||
| def test_only_one_actionable_mirrored_matching_task_is_accepted(self): | ||
| path = "work/epics/34-capacity/stories/34.01-reserve/tasks/34.01.03-binding/README.md" | ||
| original = ( | ||
| "---\ntype: task\nid: '34.01.03'\nstatus: ready\n" | ||
| "monday_item_id: 123\n---\n# Task\n" | ||
| ) | ||
| for change in [ | ||
| None, | ||
| ("status: ready", "status: in_progress"), | ||
| ("status: ready", "status: blocked"), | ||
| ("status: ready", "status: ready\nstatus: in_progress"), | ||
| ("monday_item_id: 123", "monday_item_id: null"), | ||
| ("id: '34.01.03'", "id: '34.01.04'"), | ||
| ("type: task", "type: story"), | ||
| ]: | ||
| readme = original if change is None else original.replace(*change) | ||
|
|
||
| def request(endpoint, readme=readme): | ||
| if "/git/trees/" in endpoint: | ||
| return {"tree": [{"path": path}]} | ||
| return { | ||
| "encoding": "base64", | ||
| "content": base64.b64encode(readme.encode()).decode(), | ||
| } | ||
|
|
||
| with self.subTest(change=change): | ||
| if change is None or change[1] == "status: in_progress": | ||
| verify_task("34.01.03", request) | ||
| else: | ||
| with self.assertRaises(ValueError): | ||
| verify_task("34.01.03", request) | ||
|
|
||
| for tree in [ | ||
| {"tree": []}, | ||
| {"tree": [{"path": path}, {"path": path}]}, | ||
| {"tree": [{"path": path}], "truncated": True}, | ||
| ]: | ||
| with self.subTest(tree=tree), self.assertRaises(ValueError): | ||
| verify_task("34.01.03", lambda _, tree=tree: tree) | ||
|
|
||
| def test_completion_requires_a_main_merge_and_preserves_receiver_contract(self): | ||
| pr = { | ||
| "merged": True, | ||
| "base": {"ref": "main", "repo": {"full_name": "globster-monday/OpenShell"}}, | ||
| "body": "Task-ID: 34.01.03", | ||
| "merge_commit_sha": "a" * 40, | ||
| "number": 32, | ||
| "html_url": "https://github.com/globster-monday/OpenShell/pull/32", | ||
| "title": 'Title with "quotes"\nand newlines', | ||
| "user": {"id": 123, "login": "owner"}, | ||
| } | ||
| payload = merged_task_event({"pull_request": pr}) | ||
| self.assertEqual(payload["event_type"], "task-merged") | ||
| self.assertEqual( | ||
| payload["client_payload"], | ||
| { | ||
| "task_id": "34.01.03", | ||
| "commit_url": "https://github.com/globster-monday/OpenShell/commit/" | ||
| + "a" * 40, | ||
| "repository": "globster-monday/OpenShell", | ||
| "pr_number": "32", | ||
| "pr_url": pr["html_url"], | ||
| "pr_title": pr["title"], | ||
| "pr_author_id": "123", | ||
| "pr_author_login": "owner", | ||
| }, | ||
| ) | ||
| for change in [ | ||
| {"merged": False}, | ||
| {"base": {"ref": "parent-task"}}, | ||
| {"merge_commit_sha": "not-a-commit"}, | ||
| ]: | ||
| with self.subTest(change=change), self.assertRaises(ValueError): | ||
| merged_task_event({"pull_request": pr | change}) | ||
| self.assertIsNone( | ||
| merged_task_event({"pull_request": pr | {"body": "Task-ID: none"}}) | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.