-
Notifications
You must be signed in to change notification settings - Fork 1.1k
handle concurrence in bot #467
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
base: master
Are you sure you want to change the base?
Changes from 6 commits
aaa1a3e
0473f76
fc8016e
0b82825
9736e14
afa0356
a743a57
f89db21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,9 @@ | |
| import os | ||
| import re | ||
| import sys | ||
| import json | ||
| from pathlib import Path | ||
| from urllib import error, parse, request | ||
|
|
||
|
|
||
| def get_existing_xls_numbers(repo_root: Path) -> set[int]: | ||
|
|
@@ -74,6 +76,138 @@ def find_draft_xls_files(changed_files: list[str]) -> list[str]: | |
| return list(set(drafts)) # Remove duplicates | ||
|
|
||
|
|
||
| def github_api_request(path: str, token: str, params: dict | None = None): | ||
| """Small helper to call the GitHub REST API. | ||
|
|
||
| Returns parsed JSON or an empty list/dict on failure. | ||
| """ | ||
|
|
||
| base_url = "https://api.github.com" | ||
| url = f"{base_url}{path}" | ||
| if params: | ||
| query = parse.urlencode(params) | ||
| url = f"{url}?{query}" | ||
|
|
||
| headers = { | ||
| "Accept": "application/vnd.github+json", | ||
| "User-Agent": "xrpl-standards-xls-number-bot", | ||
| } | ||
| if token: | ||
| headers["Authorization"] = f"Bearer {token}" | ||
|
|
||
| req = request.Request(url, headers=headers) | ||
|
|
||
| try: | ||
| with request.urlopen(req) as resp: | ||
| data = resp.read() | ||
| return json.loads(data.decode("utf-8")) | ||
| except error.HTTPError as e: | ||
| print(f"Warning: GitHub API HTTP error {e.code} for {url}: {e.reason}") | ||
| except error.URLError as e: | ||
| print(f"Warning: GitHub API URL error for {url}: {e.reason}") | ||
|
|
||
| # On any failure, return an empty list so callers can treat it as "no data". | ||
| return [] | ||
|
|
||
|
|
||
| def extract_xls_number_from_comments( | ||
| owner: str, repo: str, token: str, issue_number: int | ||
| ) -> int | None: | ||
| """Extract reserved XLS number for an issue/PR from bot comments. | ||
|
|
||
| Looks for a marker of the form: <!-- XLS_NUMBER:0123 --> in comments | ||
| authored by github-actions[bot]. | ||
| """ | ||
|
|
||
| page = 1 | ||
| while True: | ||
| comments = github_api_request( | ||
| f"/repos/{owner}/{repo}/issues/{issue_number}/comments", | ||
| token, | ||
| {"per_page": 100, "page": page}, | ||
| ) | ||
|
|
||
| if not comments: | ||
| break | ||
|
|
||
| for comment in comments: | ||
| if comment.get("user", {}).get("login") != "github-actions[bot]": | ||
| continue | ||
| body = comment.get("body") or "" | ||
| match = re.search(r"<!--\s*XLS_NUMBER:(\\d+)\s*-->", body) | ||
| if match: | ||
| try: | ||
| return int(match.group(1)) | ||
| except ValueError: | ||
|
Comment on lines
+133
to
+141
|
||
| continue | ||
|
Comment on lines
+133
to
+142
|
||
|
|
||
| if len(comments) < 100: | ||
| break | ||
| page += 1 | ||
|
|
||
| return None | ||
|
|
||
|
|
||
| def get_reserved_xls_numbers_from_prs( | ||
| token: str, repo: str, current_pr_number: int | None | ||
| ) -> tuple[set[int], int | None]: | ||
| """Find XLS numbers reserved by open PRs with the 'has-xls-number' label. | ||
|
|
||
| Returns a tuple of (set of reserved numbers, number assigned to the current PR | ||
| if any). | ||
| """ | ||
|
|
||
| if not token or not repo: | ||
| # Without a token or repo, we cannot query the API; treat as no reservations. | ||
| return set(), None | ||
|
|
||
| if "/" not in repo: | ||
| return set(), None | ||
|
|
||
| owner, repo_name = repo.split("/", 1) | ||
|
|
||
| reserved_numbers: set[int] = set() | ||
| current_pr_assigned: int | None = None | ||
|
|
||
| page = 1 | ||
| while True: | ||
| issues = github_api_request( | ||
| f"/repos/{owner}/{repo_name}/issues", | ||
| token, | ||
| { | ||
| "state": "open", | ||
| "labels": "has-xls-number", | ||
| "per_page": 100, | ||
| "page": page, | ||
| }, | ||
| ) | ||
|
|
||
| if not issues: | ||
| break | ||
|
|
||
| for issue in issues: | ||
| # Filter to PRs only | ||
| if "pull_request" not in issue: | ||
| continue | ||
|
|
||
| issue_number = issue.get("number") | ||
| num = extract_xls_number_from_comments(owner, repo_name, token, issue_number) | ||
| if num is None: | ||
| continue | ||
|
|
||
| reserved_numbers.add(num) | ||
|
|
||
| if current_pr_number is not None and issue_number == current_pr_number: | ||
| current_pr_assigned = num | ||
|
|
||
| if len(issues) < 100: | ||
| break | ||
|
|
||
| page += 1 | ||
|
|
||
| return reserved_numbers, current_pr_assigned | ||
|
|
||
|
|
||
| def main(): | ||
| """Main entry point for the script.""" | ||
| # Get repository root from environment variable (set by GitHub Actions) | ||
|
|
@@ -116,24 +250,56 @@ def set_github_output(name: str, value: str): | |
| set_github_output("has_drafts", "false") | ||
| return | ||
|
|
||
| # Get existing XLS numbers | ||
| # Discover reserved XLS numbers from other open PRs and from this PR (if any) | ||
| github_token = os.environ.get("GITHUB_TOKEN", "") | ||
| github_repo = os.environ.get("GITHUB_REPOSITORY", "") | ||
| current_pr_number = None | ||
| event_path = os.environ.get("GITHUB_EVENT_PATH") | ||
| if event_path and os.path.isfile(event_path): | ||
| try: | ||
| with open(event_path, "r", encoding="utf-8") as f: | ||
| event = json.load(f) | ||
| current_pr_number = event.get("pull_request", {}).get("number") | ||
| except Exception as exc: # pragma: no cover - defensive | ||
| print(f"Warning: Failed to parse GITHUB_EVENT_PATH: {exc}") | ||
|
|
||
| reserved_numbers, current_pr_assigned = get_reserved_xls_numbers_from_prs( | ||
| github_token, | ||
| github_repo, | ||
| current_pr_number, | ||
| ) | ||
|
|
||
| if reserved_numbers: | ||
| print(f"Found {len(reserved_numbers)} XLS numbers reserved by open PRs.") | ||
|
|
||
| # Get existing XLS numbers from the base branch | ||
| existing_numbers = get_existing_xls_numbers(repo_root) | ||
| print(f"Found {len(existing_numbers)} existing XLS numbers.") | ||
| print(f"Found {len(existing_numbers)} existing XLS numbers in the repository.") | ||
| print(f"Highest existing number: {max(existing_numbers) if existing_numbers else 0}") | ||
|
|
||
| # Assign numbers to each draft | ||
| next_number = get_next_xls_number(existing_numbers) | ||
| all_numbers = existing_numbers | reserved_numbers | ||
|
|
||
| assignments = [] | ||
|
|
||
| if current_pr_assigned is not None: | ||
| print( | ||
| f"Reusing previously reserved XLS number {current_pr_assigned:04d} for this PR." | ||
| ) | ||
| next_number = current_pr_assigned | ||
| else: | ||
| next_number = get_next_xls_number(all_numbers) | ||
|
|
||
| for draft_dir in sorted(draft_dirs): | ||
| assigned_number = next_number | ||
| all_numbers.add(assigned_number) | ||
| new_dir_name = re.sub(r"^xls-draft-", f"xls-{assigned_number:04d}-", draft_dir.lower()) | ||
| assignments.append({ | ||
|
Comment on lines
+294
to
296
|
||
| "draft": draft_dir, | ||
| "number": assigned_number, | ||
| "new_name": new_dir_name, | ||
| }) | ||
| next_number += 1 | ||
| # Calculate the next free number for any additional drafts | ||
| next_number = get_next_xls_number(all_numbers) | ||
|
|
||
| # Output results | ||
| print("\n=== XLS Number Assignments ===") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,10 @@ on: | |
| pull_request_target: | ||
| types: [opened, synchronize, reopened, ready_for_review] | ||
|
|
||
| concurrency: | ||
| group: assign-xls-number-${{ github.repository }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| assign-xls-number: | ||
| runs-on: ubuntu-latest | ||
|
|
@@ -78,10 +82,11 @@ jobs: | |
| body-includes: "XLS Number Assignment" | ||
|
|
||
| - name: Post or update PR comment | ||
| if: steps.check-drafts.outputs.has_drafts == 'true' && steps.find-comment.outputs.comment-id == '' | ||
| if: steps.check-drafts.outputs.has_drafts == 'true' | ||
| uses: peter-evans/create-or-update-comment@v5 | ||
| with: | ||
| issue-number: ${{ github.event.pull_request.number }} | ||
| comment-id: ${{ steps.find-comment.outputs.comment-id }} | ||
| body: | | ||
| ## 🎫 XLS Number Assignment | ||
|
|
||
|
|
@@ -101,4 +106,18 @@ jobs: | |
| - Set `status:` to `Draft` | ||
|
|
||
| --- | ||
| *This comment was automatically generated. The XLS number is based on the highest existing number in the repository at the time this PR was opened.* | ||
| *This comment was automatically generated. The XLS number below is reserved for this PR based on existing XLS numbers in the repository and other open draft PRs.* | ||
|
|
||
| <!-- XLS_NUMBER:${{ steps.assign-number.outputs.xls_number }} --> | ||
|
|
||
| - name: Add has-xls-number label | ||
| if: steps.check-drafts.outputs.has_drafts == 'true' | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| curl -sS \ | ||
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| -X POST \ | ||
| -d '{"labels":["has-xls-number"]}' \ | ||
| "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" | ||
|
Comment on lines
+118
to
+123
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
github_api_request()usesrequest.urlopen(req)without a timeout. Since this runs in CI, a stalled network call can hang the entire workflow indefinitely. Pass an explicit timeout tourlopen(and consider handlingjson.JSONDecodeErroras well) so failures are bounded and reported cleanly.