Skip to content

Commit 89413dc

Browse files
authored
Merge branch 'master' into xls-draft-confidential-mpt
2 parents 0035272 + 595f6a9 commit 89413dc

File tree

95 files changed

+2097
-323
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+2097
-323
lines changed

.github/pull_request_template.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## High Level Overview of Change
2+
3+
<!--
4+
Please include a summary of the changes.
5+
If too broad, please consider splitting into multiple PRs.
6+
-->
7+
8+
### Context of Change
9+
10+
<!--
11+
Please include the context of the change.
12+
If proposing a new XLS, link the associated GitHub Discussion.
13+
If updating an existing XLS, explain why the change is needed.
14+
If a bug fix or process improvement, describe the issue being addressed.
15+
-->
16+
17+
### Type of Change
18+
19+
<!--
20+
Please check [x] relevant options, delete irrelevant ones.
21+
-->
22+
23+
- [ ] New XLS Draft
24+
- [ ] XLS Update (changes to an existing XLS)
25+
- [ ] XLS Status Change (e.g., Draft → Final, Draft → Stagnant)
26+
- [ ] Process/Meta (changes to CONTRIBUTING.md, XLS-1, templates, etc.)
27+
- [ ] Infrastructure (CI, workflows, scripts, website)
28+
- [ ] Documentation (README updates, typo fixes)
29+
30+
<!--
31+
## Future Tasks
32+
For follow-up work related to this PR.
33+
-->
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Assign XLS numbers to new draft XLS proposals.
4+
5+
This script scans the repository for existing XLS directories,
6+
determines the next available XLS number, and outputs it for use
7+
in GitHub Actions.
8+
"""
9+
10+
import os
11+
import re
12+
import sys
13+
from pathlib import Path
14+
15+
16+
def get_existing_xls_numbers(repo_root: Path) -> set[int]:
17+
"""
18+
Scan the repository root for existing XLS directories and extract their numbers.
19+
20+
Returns a set of integers representing all claimed XLS numbers.
21+
"""
22+
xls_pattern = re.compile(r"^XLS-(\d{4})-")
23+
numbers = set()
24+
25+
for item in repo_root.iterdir():
26+
if item.is_dir():
27+
match = xls_pattern.match(item.name)
28+
if match:
29+
numbers.add(int(match.group(1)))
30+
31+
return numbers
32+
33+
34+
# Minimum XLS number to assign (to avoid filling old historical gaps)
35+
MIN_XLS_NUMBER = 96
36+
37+
38+
def get_next_xls_number(existing_numbers: set[int]) -> int:
39+
"""
40+
Determine the next available XLS number.
41+
42+
Returns the first unused number >= MIN_XLS_NUMBER.
43+
"""
44+
if not existing_numbers:
45+
return MIN_XLS_NUMBER
46+
47+
# Find the first available number starting from MIN_XLS_NUMBER
48+
max_num = max(existing_numbers)
49+
for num in range(MIN_XLS_NUMBER, max_num + 2):
50+
if num not in existing_numbers:
51+
return num
52+
53+
return max_num + 1
54+
55+
56+
def find_draft_xls_files(changed_files: list[str]) -> list[str]:
57+
"""
58+
Filter changed files to find new XLS draft README files.
59+
60+
Args:
61+
changed_files: List of file paths that were added in the PR
62+
63+
Returns:
64+
List of draft XLS directory names (e.g., ["XLS-draft-my-feature"])
65+
"""
66+
draft_pattern = re.compile(r"^(XLS-draft-[^/]+)/README\.md$")
67+
drafts = []
68+
69+
for file_path in changed_files:
70+
match = draft_pattern.match(file_path)
71+
if match:
72+
drafts.append(match.group(1))
73+
74+
return list(set(drafts)) # Remove duplicates
75+
76+
77+
def main():
78+
"""Main entry point for the script."""
79+
# Get repository root (parent of .github directory)
80+
script_dir = Path(__file__).resolve().parent
81+
repo_root = script_dir.parent.parent
82+
83+
# Get changed files from command line arguments or environment variable
84+
if len(sys.argv) > 1:
85+
changed_files = sys.argv[1:]
86+
else:
87+
# Try to get from environment variable (set by GitHub Actions)
88+
changed_files_env = os.environ.get("CHANGED_FILES", "")
89+
changed_files = changed_files_env.split() if changed_files_env else []
90+
91+
# Helper to set GitHub output
92+
def set_github_output(name: str, value: str):
93+
github_output = os.environ.get("GITHUB_OUTPUT")
94+
if github_output:
95+
with open(github_output, "a") as f:
96+
f.write(f"{name}={value}\n")
97+
else:
98+
print(f" {name}={value}")
99+
100+
if not changed_files:
101+
print("No changed files provided.")
102+
set_github_output("has_drafts", "false")
103+
return
104+
105+
# Find draft XLS files
106+
draft_dirs = find_draft_xls_files(changed_files)
107+
108+
if not draft_dirs:
109+
print("No XLS draft files found in changed files.")
110+
set_github_output("has_drafts", "false")
111+
return
112+
113+
# Get existing XLS numbers
114+
existing_numbers = get_existing_xls_numbers(repo_root)
115+
print(f"Found {len(existing_numbers)} existing XLS numbers.")
116+
print(f"Highest existing number: {max(existing_numbers) if existing_numbers else 0}")
117+
118+
# Assign numbers to each draft
119+
next_number = get_next_xls_number(existing_numbers)
120+
assignments = []
121+
122+
for draft_dir in sorted(draft_dirs):
123+
assigned_number = next_number
124+
new_dir_name = re.sub(r"^XLS-draft-", f"XLS-{assigned_number:04d}-", draft_dir)
125+
assignments.append({
126+
"draft": draft_dir,
127+
"number": assigned_number,
128+
"new_name": new_dir_name,
129+
})
130+
next_number += 1
131+
132+
# Output results
133+
print("\n=== XLS Number Assignments ===")
134+
for assignment in assignments:
135+
draft = assignment['draft']
136+
new_name = assignment['new_name']
137+
num = assignment['number']
138+
print(f" {draft} -> {new_name} (XLS-{num:04d})")
139+
140+
# Set GitHub Actions outputs
141+
set_github_output("has_drafts", "true")
142+
set_github_output("assignments", str(assignments))
143+
144+
# For single draft case, also output individual values for easy access
145+
if len(assignments) == 1:
146+
set_github_output("xls_number", f"{assignments[0]['number']:04d}")
147+
set_github_output("draft_dir", assignments[0]['draft'])
148+
set_github_output("new_dir_name", assignments[0]['new_name'])
149+
150+
151+
if __name__ == "__main__":
152+
main()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Filter discussions that should be closed
2+
# A discussion should be closed if:
3+
# 1. It has a warning comment containing the unique marker
4+
# 2. That warning comment was posted by the bot
5+
# 3. That warning comment is older than WARNING_DAYS
6+
# 4. The discussion hasn't been updated since the warning (or updates are also old)
7+
#
8+
# Input: GraphQL response with discussions data
9+
# Arguments:
10+
# $warningCutoff - ISO 8601 timestamp for warning age threshold
11+
# $marker - Unique marker string to identify warning comments
12+
# $botLogin - GitHub login of the bot user
13+
# Output: JSON objects (one per line) for discussions that should be closed
14+
15+
.data.repository.discussions.nodes[]
16+
17+
# Only process open discussions
18+
| select(.closed == false)
19+
20+
# Store the discussion for later reference
21+
| . as $discussion
22+
23+
# Find the most recent warning comment from the bot
24+
# Note: We only look at the last 100 comments (fetched by the shell script).
25+
# This is intentional - if there are 100+ comments after a warning, the discussion
26+
# is clearly active and should not be closed.
27+
| (
28+
(.comments.nodes // [])
29+
| map(
30+
select(.body | contains($marker))
31+
| select(.author.login == $botLogin)
32+
)
33+
| last
34+
) as $warningComment
35+
36+
# Only proceed if a warning comment exists
37+
| select($warningComment != null)
38+
39+
# Only proceed if the warning comment is old enough
40+
| select($warningComment.createdAt <= $warningCutoff)
41+
42+
# Only proceed if the discussion hasn't been updated since the warning
43+
| select($discussion.updatedAt < $warningComment.createdAt)
44+
45+
# Output as JSON
46+
| @json
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Filter discussions that should receive a warning
2+
# A discussion should be warned if:
3+
# 1. It hasn't been updated in STALE_DAYS
4+
# 2. Either:
5+
# a. It doesn't have a warning comment (with unique marker) from the bot yet, OR
6+
# b. It has a warning from the bot but was updated after that warning (user responded, so we warn again)
7+
#
8+
# Input: GraphQL response with discussions data
9+
# Arguments:
10+
# $staleCutoff - ISO 8601 timestamp for staleness threshold
11+
# $marker - Unique marker string to identify warning comments
12+
# $botLogin - GitHub login of the bot user
13+
# Output: JSON objects (one per line) for discussions that should be warned
14+
15+
.data.repository.discussions.nodes[]
16+
17+
# Only process open discussions
18+
| select(.closed == false)
19+
20+
# Only process discussions that are stale (not updated recently)
21+
| select(.updatedAt < $staleCutoff)
22+
23+
# Store the discussion for later reference
24+
| . as $discussion
25+
26+
# Find the most recent warning comment from the bot
27+
# Note: We only look at the last 100 comments (fetched by the shell script).
28+
# This is intentional - if there are 100+ comments after a warning, the discussion
29+
# is clearly active and should not be warned again.
30+
| (
31+
(.comments.nodes // [])
32+
| map(
33+
select(.body | contains($marker))
34+
| select(.author.login == $botLogin)
35+
)
36+
| last
37+
) as $warningComment
38+
39+
# Only proceed if:
40+
# - No warning comment exists yet, OR
41+
# - Discussion was updated after the last warning (user responded)
42+
| select(
43+
$warningComment == null
44+
or $discussion.updatedAt > $warningComment.createdAt
45+
)
46+
47+
# Output as JSON
48+
| @json

0 commit comments

Comments
 (0)