Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 171 additions & 5 deletions .github/scripts/assign_xls_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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}")
Comment on lines +79 to +107
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github_api_request() uses request.urlopen(req) without a timeout. Since this runs in CI, a stalled network call can hang the entire workflow indefinitely. Pass an explicit timeout to urlopen (and consider handling json.JSONDecodeError as well) so failures are bounded and reported cleanly.

Copilot uses AI. Check for mistakes.

# 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
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex used to extract the reserved number from the bot comment is incorrectly escaped: r"...(\\d+)" matches a literal \d instead of digits, so this will never detect the XLS number marker and reservations won’t work. Use r"...(\d+)" (single backslash in the raw string) to match digits.

Copilot uses AI. Check for mistakes.
continue
Comment on lines +133 to +142
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reserved-number extraction regex is incorrect: the pattern uses \\d+ inside a raw string, which matches a literal "\d" instead of digits. As a result, the script will never find <!-- XLS_NUMBER:... --> markers and will treat all open PRs as having no reserved number. Change the regex to use \d+ (single backslash in a raw string).

Copilot uses AI. Check for mistakes.

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)
Expand Down Expand Up @@ -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
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new_dir_name is forced to lowercase (draft_dir.lower()), producing names like xls-0096-..., but the repository’s existing XLS directories are XLS-####-... and get_existing_xls_numbers() only detects the uppercase form. If editors follow the workflow comment and rename to lowercase, future runs won’t treat those directories as “existing” and may reassign duplicate numbers. Make directory naming consistent (either output XLS-####-... or update the scanner to accept both cases).

Copilot uses AI. Check for mistakes.
"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 ===")
Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/assign-xls-number.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This curl call won’t fail the step on HTTP errors (e.g., insufficient permissions / invalid repo / API error) because it doesn’t use --fail/--fail-with-body or check the response status. That can leave PRs unlabeled while the workflow still succeeds, breaking the reservation mechanism. Make the step fail on non-2xx (or explicitly handle and log failures) by checking the HTTP status code.

Copilot uses AI. Check for mistakes.