mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +00:00
Merge remote-tracking branch 'origin/main' into staging
This commit is contained in:
commit
fe9e47f947
@ -106,6 +106,22 @@ The `prs` command returns JSON with `number`, `title`, `body`, `state`,
|
|||||||
batches of 50 via GraphQL to stay within API limits (milestone mode uses
|
batches of 50 via GraphQL to stay within API limits (milestone mode uses
|
||||||
paginated GraphQL on the milestone's `pullRequests` connection).
|
paginated GraphQL on the milestone's `pullRequests` connection).
|
||||||
|
|
||||||
|
You can also list all PRs in a milestone in a single call:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All merged PRs in a milestone (default)
|
||||||
|
python3 tools/gh.py prs --milestone "2.16.0"
|
||||||
|
|
||||||
|
# All states (merged, open, closed)
|
||||||
|
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
||||||
|
|
||||||
|
# Open PRs only
|
||||||
|
python3 tools/gh.py prs --milestone "2.16.0" --state open
|
||||||
|
```
|
||||||
|
|
||||||
|
The milestone path uses paginated GraphQL on the milestone's `pullRequests`
|
||||||
|
connection (100 per page), avoiding one-by-one fetches.
|
||||||
|
|
||||||
### 5. Categorize entries — strictly by issue type, never by labels or emoji
|
### 5. Categorize entries — strictly by issue type, never by labels or emoji
|
||||||
|
|
||||||
Use the **Issue Type** field (GitHub's native issue type, exposed as
|
Use the **Issue Type** field (GitHub's native issue type, exposed as
|
||||||
@ -328,6 +344,72 @@ if closed:
|
|||||||
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
|
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 10. Cross-reference milestone PRs against the changelog
|
||||||
|
|
||||||
|
Issues can be fixed by PRs that aren't in the milestone, and merged PRs in
|
||||||
|
the milestone may not close any tracked issue. After writing, run a full
|
||||||
|
cross-reference to catch gaps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all merged PRs in the milestone
|
||||||
|
python3 tools/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
|
||||||
|
|
||||||
|
# Extract PR numbers from the changelog section
|
||||||
|
python3 -c "
|
||||||
|
import json, re
|
||||||
|
|
||||||
|
with open('CHANGES.md') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Extract the version section (adjust regex to match the actual version)
|
||||||
|
match = re.search(r'## <MILESTONE> \(Unreleased\)\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
|
||||||
|
section = match.group(1)
|
||||||
|
|
||||||
|
# Collect all PR numbers referenced
|
||||||
|
changelog_prs = set()
|
||||||
|
for m in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
|
||||||
|
changelog_prs.add(int(m))
|
||||||
|
|
||||||
|
# Collect all milestone PRs (filtered)
|
||||||
|
with open('/tmp/milestone-prs.json') as f:
|
||||||
|
milestone_prs = json.load(f)
|
||||||
|
|
||||||
|
milestone_merged = {pr['number'] for pr in milestone_prs}
|
||||||
|
|
||||||
|
# PRs in milestone but not in changelog
|
||||||
|
missing = sorted(milestone_merged - changelog_prs)
|
||||||
|
print(f'Milestone merged PRs: {len(milestone_merged)}')
|
||||||
|
print(f'Changelog referenced PRs: {len(changelog_prs)}')
|
||||||
|
print(f'PRs in milestone but NOT in changelog: {len(missing)}')
|
||||||
|
for num in missing:
|
||||||
|
pr = next(p for p in milestone_prs if p['number'] == num)
|
||||||
|
print(f' #{num} {pr[\"title\"][:80]}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
For each missing PR found, decide whether it should be added to the
|
||||||
|
changelog or is legitimately excluded (check its labels).
|
||||||
|
|
||||||
|
Also verify that no closed-unmerged PRs remain in the changelog:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
closed = [p for p in data if p['state'] == 'CLOSED']
|
||||||
|
if closed:
|
||||||
|
print('WARNING: CLOSED (unmerged) PRs in milestone:')
|
||||||
|
for p in closed:
|
||||||
|
print(f' #{p[\"number\"]} {p[\"title\"][:80]}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Post-edit audit checklist:**
|
||||||
|
- ✅ All referenced PRs are merged (no closed-unmerged artifacts)
|
||||||
|
- ✅ Every merged milestone PR is either in the changelog or excluded by label
|
||||||
|
- ✅ PR and issue counts are internally consistent
|
||||||
|
- ✅ No false-positive PR-to-issue associations
|
||||||
|
|
||||||
## Key Principles
|
## Key Principles
|
||||||
|
|
||||||
- **Issue = changelog unit.** The primary link always points to the
|
- **Issue = changelog unit.** The primary link always points to the
|
||||||
|
|||||||
@ -146,6 +146,7 @@
|
|||||||
### :sparkles: New features & Enhancements
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
- Add rate limiting and concurrency safety for file snapshot operations [#9723](https://github.com/penpot/penpot/issues/9723) (PR: [#9722](https://github.com/penpot/penpot/pull/9722))
|
- Add rate limiting and concurrency safety for file snapshot operations [#9723](https://github.com/penpot/penpot/issues/9723) (PR: [#9722](https://github.com/penpot/penpot/pull/9722))
|
||||||
|
- Prevent concurrent font uploads from causing excessive simultaneous requests [#9922](https://github.com/penpot/penpot/issues/9922) (PR: [#9921](https://github.com/penpot/penpot/pull/9921))
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
|||||||
261
tools/gh.py
261
tools/gh.py
@ -6,7 +6,7 @@ Uses GitHub GraphQL and REST APIs via the authenticated ``gh`` CLI.
|
|||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
issues List issues in a milestone
|
issues List issues in a milestone
|
||||||
prs Fetch details for one or more PRs (or all PRs in a milestone)
|
prs Fetch details for one or more PRs (by number or milestone)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 tools/gh.py issues <milestone-title> (default: state=closed)
|
python3 tools/gh.py issues <milestone-title> (default: state=closed)
|
||||||
@ -42,19 +42,6 @@ REPO_NAME = "penpot"
|
|||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def run_gh(method: str, endpoint: str, **kwargs: Any) -> Any:
|
|
||||||
"""Run a ``gh api`` call and return parsed JSON."""
|
|
||||||
cmd = ["gh", "api", endpoint, "--method", method]
|
|
||||||
for key, val in kwargs.items():
|
|
||||||
if val is not None:
|
|
||||||
cmd.extend(["-f", f"{key}={val}"])
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"gh error: {result.stderr}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return json.loads(result.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def run_gh_graphql(query: str, variables: dict) -> Any:
|
def run_gh_graphql(query: str, variables: dict) -> Any:
|
||||||
"""Run a GraphQL query via ``gh api graphql --input -``."""
|
"""Run a GraphQL query via ``gh api graphql --input -``."""
|
||||||
payload = json.dumps({"query": query, "variables": variables})
|
payload = json.dumps({"query": query, "variables": variables})
|
||||||
@ -71,6 +58,44 @@ def run_gh_graphql(query: str, variables: dict) -> Any:
|
|||||||
return body["data"]
|
return body["data"]
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Shared: milestone lookup
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
GQL_FIND_MILESTONE_QUERY = """\
|
||||||
|
query($owner: String!, $repo: String!, $title: String!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
milestones(query: $title, first: 20, states: [OPEN CLOSED]) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
title
|
||||||
|
state
|
||||||
|
issues(states: [OPEN]) { totalCount }
|
||||||
|
closed_issues: issues(states: [CLOSED]) { totalCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def find_milestone(title: str) -> dict:
|
||||||
|
"""Look up milestone by title via GraphQL, return {number, title, open_issues, closed_issues}."""
|
||||||
|
variables = {"owner": OWNER, "repo": REPO_NAME, "title": title}
|
||||||
|
data = run_gh_graphql(GQL_FIND_MILESTONE_QUERY, variables)
|
||||||
|
nodes = data["repository"]["milestones"]["nodes"]
|
||||||
|
for ms in nodes:
|
||||||
|
if ms["title"] == title:
|
||||||
|
return {
|
||||||
|
"number": ms["number"],
|
||||||
|
"title": ms["title"],
|
||||||
|
"open_issues": ms["issues"]["totalCount"],
|
||||||
|
"closed_issues": ms["closed_issues"]["totalCount"],
|
||||||
|
}
|
||||||
|
print(f"ERROR: Milestone \"{title}\" not found in {REPO}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# Subcommand: issues
|
# Subcommand: issues
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
@ -99,21 +124,6 @@ query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def find_milestone(title: str) -> dict:
|
|
||||||
"""Look up milestone by title, return {number, title, open_issues, closed_issues}."""
|
|
||||||
data = run_gh("GET", f"repos/{OWNER}/{REPO_NAME}/milestones?per_page=100&state=all")
|
|
||||||
for ms in data:
|
|
||||||
if ms["title"] == title:
|
|
||||||
return {
|
|
||||||
"number": ms["number"],
|
|
||||||
"title": ms["title"],
|
|
||||||
"open_issues": ms["open_issues"],
|
|
||||||
"closed_issues": ms["closed_issues"],
|
|
||||||
}
|
|
||||||
print(f"ERROR: Milestone \"{title}\" not found in {REPO}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]:
|
def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Fetch all issues in a milestone via paginated GraphQL.
|
Fetch all issues in a milestone via paginated GraphQL.
|
||||||
@ -212,86 +222,7 @@ def cmd_issues(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# Subcommand: prs — milestone PR listing
|
# Subcommand: prs
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
GQL_MILESTONE_PRS_QUERY = """\
|
|
||||||
query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) {
|
|
||||||
repository(owner: $owner, name: $repo) {
|
|
||||||
milestone(number: $milestone) {
|
|
||||||
pullRequests(first: 50, after: $cursor, states: __STATES__) {
|
|
||||||
totalCount
|
|
||||||
pageInfo { hasNextPage endCursor }
|
|
||||||
nodes {
|
|
||||||
... on PullRequest {
|
|
||||||
number
|
|
||||||
title
|
|
||||||
state
|
|
||||||
mergedAt
|
|
||||||
author { login }
|
|
||||||
labels(first: 20) { nodes { name } }
|
|
||||||
closingIssuesReferences(first: 5) { nodes { number } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_milestone_prs(milestone_num: int, states: str) -> list[dict]:
|
|
||||||
"""
|
|
||||||
Fetch all PRs in a milestone via paginated GraphQL.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
milestone_num: milestone number
|
|
||||||
states: GraphQL states enum array literal for pullRequests,
|
|
||||||
e.g. ``"[MERGED]"`` or ``"[OPEN CLOSED MERGED]"``
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of {number, title, state, merged_at, author, labels, closing_issues}
|
|
||||||
"""
|
|
||||||
query = GQL_MILESTONE_PRS_QUERY.replace("__STATES__", states)
|
|
||||||
all_nodes: list[dict] = []
|
|
||||||
cursor: str | None = None
|
|
||||||
|
|
||||||
while True:
|
|
||||||
variables: dict[str, Any] = {
|
|
||||||
"owner": OWNER,
|
|
||||||
"repo": REPO_NAME,
|
|
||||||
"milestone": milestone_num,
|
|
||||||
"cursor": cursor,
|
|
||||||
}
|
|
||||||
data = run_gh_graphql(query, variables)
|
|
||||||
prs = data["repository"]["milestone"]["pullRequests"]
|
|
||||||
page_info = prs["pageInfo"]
|
|
||||||
|
|
||||||
for node in prs["nodes"]:
|
|
||||||
if node is None:
|
|
||||||
continue
|
|
||||||
all_nodes.append({
|
|
||||||
"number": node["number"],
|
|
||||||
"title": node["title"],
|
|
||||||
"state": node["state"],
|
|
||||||
"merged_at": node.get("mergedAt"),
|
|
||||||
"author": node["author"]["login"] if node["author"] else None,
|
|
||||||
"labels": [lbl["name"] for lbl in node["labels"]["nodes"]],
|
|
||||||
"closing_issues": [iss["number"] for iss in node["closingIssuesReferences"]["nodes"]],
|
|
||||||
})
|
|
||||||
|
|
||||||
total = len(all_nodes)
|
|
||||||
print(f" ... fetched {total} PRs so far", file=sys.stderr)
|
|
||||||
|
|
||||||
if not page_info["hasNextPage"]:
|
|
||||||
break
|
|
||||||
cursor = page_info["endCursor"]
|
|
||||||
|
|
||||||
return all_nodes
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Subcommand: prs — batch fetch by number
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
PRS_BATCH_SIZE = 50
|
PRS_BATCH_SIZE = 50
|
||||||
@ -358,27 +289,103 @@ def fetch_prs_batch(pr_numbers: list[int]) -> list[dict]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
GQL_MILESTONE_PRS_QUERY = """\
|
||||||
|
query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
milestone(number: $milestone) {
|
||||||
|
pullRequests(first: 100, after: $cursor, states: __STATES__) {
|
||||||
|
totalCount
|
||||||
|
pageInfo { hasNextPage endCursor }
|
||||||
|
nodes {
|
||||||
|
... on PullRequest {
|
||||||
|
number
|
||||||
|
title
|
||||||
|
body
|
||||||
|
state
|
||||||
|
mergedAt
|
||||||
|
createdAt
|
||||||
|
author { login }
|
||||||
|
labels(first: 20) { nodes { name } }
|
||||||
|
closingIssuesReferences(first: 5) { nodes { number } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_milestone_prs(milestone_num: int, states: str) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Fetch all pull requests in a milestone via paginated GraphQL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
milestone_num: milestone number
|
||||||
|
states: GraphQL states enum array literal, e.g. ``"[MERGED]"`` or ``"[OPEN CLOSED MERGED]"``
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of {number, title, body, state, merged_at, created_at, author,
|
||||||
|
labels: [str], closing_issues: [int]}
|
||||||
|
"""
|
||||||
|
query = GQL_MILESTONE_PRS_QUERY.replace("__STATES__", states)
|
||||||
|
all_nodes: list[dict] = []
|
||||||
|
cursor: str | None = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
variables: dict[str, Any] = {
|
||||||
|
"owner": OWNER,
|
||||||
|
"repo": REPO_NAME,
|
||||||
|
"milestone": milestone_num,
|
||||||
|
"cursor": cursor,
|
||||||
|
}
|
||||||
|
data = run_gh_graphql(query, variables)
|
||||||
|
prs = data["repository"]["milestone"]["pullRequests"]
|
||||||
|
page_info = prs["pageInfo"]
|
||||||
|
|
||||||
|
for node in prs["nodes"]:
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
all_nodes.append({
|
||||||
|
"number": node["number"],
|
||||||
|
"title": node["title"],
|
||||||
|
"body": node.get("body"),
|
||||||
|
"state": node["state"],
|
||||||
|
"merged_at": node.get("mergedAt"),
|
||||||
|
"created_at": node.get("createdAt"),
|
||||||
|
"author": node["author"]["login"] if node["author"] else None,
|
||||||
|
"labels": [lbl["name"] for lbl in node["labels"]["nodes"]],
|
||||||
|
"closing_issues": [iss["number"] for iss in node["closingIssuesReferences"]["nodes"]],
|
||||||
|
})
|
||||||
|
|
||||||
|
total = len(all_nodes)
|
||||||
|
print(f" ... fetched {total} PRs so far", file=sys.stderr)
|
||||||
|
|
||||||
|
if not page_info["hasNextPage"]:
|
||||||
|
break
|
||||||
|
cursor = page_info["endCursor"]
|
||||||
|
|
||||||
|
return all_nodes
|
||||||
|
|
||||||
|
|
||||||
def cmd_prs(args: argparse.Namespace) -> None:
|
def cmd_prs(args: argparse.Namespace) -> None:
|
||||||
"""Handle the ``prs`` subcommand."""
|
"""Handle the ``prs`` subcommand."""
|
||||||
|
|
||||||
# ── Milestone mode: fetch all PRs in a milestone ──
|
# ── Milestone path ──────────────────────────────────────────────
|
||||||
if args.milestone:
|
if args.milestone:
|
||||||
print(f"Looking up milestone \"{args.milestone}\"...", file=sys.stderr)
|
print(f"Looking up milestone \"{args.milestone}\"...", file=sys.stderr)
|
||||||
ms = find_milestone(args.milestone)
|
ms = find_milestone(args.milestone)
|
||||||
print(f"Milestone #{ms['number']}: {ms['open_issues']} open, {ms['closed_issues']} closed",
|
|
||||||
file=sys.stderr)
|
|
||||||
|
|
||||||
state_map = {"open": "[OPEN]", "closed": "[CLOSED]",
|
state_map = {"open": "[OPEN]", "closed": "[CLOSED]", "merged": "[MERGED]", "all": "[OPEN CLOSED MERGED]"}
|
||||||
"merged": "[MERGED]", "all": "[OPEN CLOSED MERGED]"}
|
|
||||||
gql_states = state_map[args.state]
|
gql_states = state_map[args.state]
|
||||||
|
|
||||||
print(f"Fetching {args.state} PRs via GraphQL...", file=sys.stderr)
|
print(f"Fetching {args.state} PRs via GraphQL...", file=sys.stderr)
|
||||||
results = fetch_milestone_prs(ms["number"], gql_states)
|
prs = fetch_milestone_prs(ms["number"], gql_states)
|
||||||
print(f"Fetched {len(results)} PRs total", file=sys.stderr)
|
print(f"Fetched {len(prs)} PRs total", file=sys.stderr)
|
||||||
print(json.dumps(results, indent=2))
|
print(json.dumps(prs, indent=2))
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Batch mode: fetch specific PRs by number ──
|
# ── Number-based path ───────────────────────────────────────────
|
||||||
pr_numbers: list[int] = []
|
pr_numbers: list[int] = []
|
||||||
|
|
||||||
if args.numbers:
|
if args.numbers:
|
||||||
@ -448,19 +455,11 @@ def main() -> None:
|
|||||||
p_issues.set_defaults(func=cmd_issues)
|
p_issues.set_defaults(func=cmd_issues)
|
||||||
|
|
||||||
# --- prs ---
|
# --- prs ---
|
||||||
p_prs = sub.add_parser("prs", help="Fetch details for one or more PRs (or all PRs in a milestone)")
|
p_prs = sub.add_parser("prs", help="Fetch details for one or more PRs (by number or milestone)")
|
||||||
p_prs.add_argument(
|
p_prs.add_argument(
|
||||||
"numbers", type=int, nargs="*",
|
"numbers", type=int, nargs="*",
|
||||||
help="PR numbers to fetch (space-separated)"
|
help="PR numbers to fetch (space-separated)"
|
||||||
)
|
)
|
||||||
p_prs.add_argument(
|
|
||||||
"--milestone", type=str,
|
|
||||||
help="Milestone title to list all PRs from (e.g. '2.16.0')"
|
|
||||||
)
|
|
||||||
p_prs.add_argument(
|
|
||||||
"--state", choices=["open", "closed", "merged", "all"], default="merged",
|
|
||||||
help="PR state filter when using --milestone (default: merged)"
|
|
||||||
)
|
|
||||||
p_prs.add_argument(
|
p_prs.add_argument(
|
||||||
"--file", type=str,
|
"--file", type=str,
|
||||||
help="File with one PR number per line"
|
help="File with one PR number per line"
|
||||||
@ -469,6 +468,14 @@ def main() -> None:
|
|||||||
"--stdin", action="store_true",
|
"--stdin", action="store_true",
|
||||||
help="Read PR numbers from stdin (one per line)"
|
help="Read PR numbers from stdin (one per line)"
|
||||||
)
|
)
|
||||||
|
p_prs.add_argument(
|
||||||
|
"--milestone", type=str,
|
||||||
|
help="Milestone title, e.g. '2.16.0' (fetches all PRs in the milestone)"
|
||||||
|
)
|
||||||
|
p_prs.add_argument(
|
||||||
|
"--state", choices=["open", "closed", "merged", "all"], default="merged",
|
||||||
|
help="PR state filter when using --milestone (default: merged)"
|
||||||
|
)
|
||||||
p_prs.set_defaults(func=cmd_prs)
|
p_prs.set_defaults(func=cmd_prs)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user