@seanyao/roll 2.602.5 → 2.603.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/bin/roll +473 -92
- package/lib/README.md +0 -1
- package/lib/__pycache__/changelog_generate.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/changelog_generate.py +221 -32
- package/lib/loop-fmt.py +2 -2
- package/lib/prices_fetcher.py +331 -63
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +1 -1
- package/skills/roll-loop/SKILL.md +7 -23
- package/lib/changelog_audit.py +0 -155
package/lib/changelog_audit.py
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""FIX-113: changelog audit — list PRs merged to main since the latest
|
|
3
|
-
release tag that don't appear in CHANGELOG.md's ## Unreleased section.
|
|
4
|
-
|
|
5
|
-
Run before `release.sh` so missing entries surface BEFORE the AI rewrite
|
|
6
|
-
gets a chance to silently drop them.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python3 lib/changelog_audit.py # report missing
|
|
10
|
-
python3 lib/changelog_audit.py --since v2026.520.1
|
|
11
|
-
python3 lib/changelog_audit.py --json # machine-readable
|
|
12
|
-
|
|
13
|
-
Exit 0 always (read-only audit). Output:
|
|
14
|
-
- "audit ok" + no missing list when CHANGELOG covers every merged PR
|
|
15
|
-
- "audit found N PR(s) without a CHANGELOG entry:" + list otherwise
|
|
16
|
-
"""
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
import argparse, json, re, subprocess, sys
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
PR_RE = re.compile(r"\(#(\d+)\)")
|
|
22
|
-
SQUASH_RE = re.compile(r"^([a-z0-9]{7,40})\s+(.*?)\s*\(#(\d+)\)\s*$")
|
|
23
|
-
|
|
24
|
-
# Heuristic: PR titles whose first token is one of these tags are usually
|
|
25
|
-
# user-visible (need CHANGELOG entry). Tags like "chore:", "docs:" still
|
|
26
|
-
# warrant a docs section entry; left to user judgement.
|
|
27
|
-
USER_VISIBLE_PATTERNS = (
|
|
28
|
-
re.compile(r"\b(US-[A-Z0-9-]+-\d+)\b"),
|
|
29
|
-
re.compile(r"\b(FIX-\d+)\b"),
|
|
30
|
-
re.compile(r"\b(REFACTOR-\d+)\b"),
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
def _latest_tag() -> str:
|
|
34
|
-
"""Return the most recent v* tag, or empty string if none."""
|
|
35
|
-
try:
|
|
36
|
-
out = subprocess.check_output(
|
|
37
|
-
["git", "tag", "--list", "v*", "--sort=-creatordate"],
|
|
38
|
-
text=True, stderr=subprocess.DEVNULL,
|
|
39
|
-
).strip()
|
|
40
|
-
for line in out.splitlines():
|
|
41
|
-
if line:
|
|
42
|
-
return line
|
|
43
|
-
except Exception:
|
|
44
|
-
pass
|
|
45
|
-
return ""
|
|
46
|
-
|
|
47
|
-
def _merged_prs_since(since: str):
|
|
48
|
-
"""Return list of (sha, subject, pr_number) for first-parent merges
|
|
49
|
-
on main between <since> and HEAD."""
|
|
50
|
-
cmd = ["git", "log", "--first-parent", "--oneline"]
|
|
51
|
-
if since:
|
|
52
|
-
cmd.append(f"{since}..HEAD")
|
|
53
|
-
try:
|
|
54
|
-
out = subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL)
|
|
55
|
-
except Exception:
|
|
56
|
-
return []
|
|
57
|
-
rows = []
|
|
58
|
-
for line in out.splitlines():
|
|
59
|
-
m = SQUASH_RE.match(line)
|
|
60
|
-
if not m:
|
|
61
|
-
# Older "Merge pull request #N from <branch>" format
|
|
62
|
-
m2 = re.match(r"^([a-f0-9]{7,40})\s+Merge pull request #(\d+) from .*$", line)
|
|
63
|
-
if m2:
|
|
64
|
-
rows.append((m2.group(1), line.split(None, 1)[1], int(m2.group(2))))
|
|
65
|
-
continue
|
|
66
|
-
# Also look for parenthesized #N anywhere in subject
|
|
67
|
-
m3 = re.match(r"^([a-f0-9]{7,40})\s+(.*)$", line)
|
|
68
|
-
if m3:
|
|
69
|
-
pr_match = PR_RE.search(m3.group(2))
|
|
70
|
-
if pr_match:
|
|
71
|
-
rows.append((m3.group(1), m3.group(2), int(pr_match.group(1))))
|
|
72
|
-
continue
|
|
73
|
-
rows.append((m.group(1), m.group(2), int(m.group(3))))
|
|
74
|
-
return rows
|
|
75
|
-
|
|
76
|
-
def _read_unreleased_section(changelog: Path) -> str:
|
|
77
|
-
"""Return the text of the ## Unreleased section (or empty string)."""
|
|
78
|
-
if not changelog.exists():
|
|
79
|
-
return ""
|
|
80
|
-
text = changelog.read_text(errors="ignore")
|
|
81
|
-
m = re.search(r"^## Unreleased\s*\n(.*?)(?=^## |\Z)", text, re.MULTILINE | re.DOTALL)
|
|
82
|
-
return m.group(1) if m else ""
|
|
83
|
-
|
|
84
|
-
def _is_in_changelog(subject: str, unreleased_text: str) -> bool:
|
|
85
|
-
"""A PR is considered covered if any story id from its subject appears in
|
|
86
|
-
the Unreleased section text."""
|
|
87
|
-
for pat in USER_VISIBLE_PATTERNS:
|
|
88
|
-
for m in pat.finditer(subject):
|
|
89
|
-
sid = m.group(1)
|
|
90
|
-
if sid in unreleased_text:
|
|
91
|
-
return True
|
|
92
|
-
# Fallback: PR number explicit mention
|
|
93
|
-
pr_m = PR_RE.search(subject)
|
|
94
|
-
if pr_m and f"#{pr_m.group(1)}" in unreleased_text:
|
|
95
|
-
return True
|
|
96
|
-
return False
|
|
97
|
-
|
|
98
|
-
def main():
|
|
99
|
-
ap = argparse.ArgumentParser()
|
|
100
|
-
ap.add_argument("--since", default="", help="Compare against this tag (default: latest v* tag)")
|
|
101
|
-
ap.add_argument("--changelog", default="CHANGELOG.md")
|
|
102
|
-
ap.add_argument("--json", action="store_true", help="Machine-readable output")
|
|
103
|
-
args = ap.parse_args()
|
|
104
|
-
|
|
105
|
-
since = args.since or _latest_tag()
|
|
106
|
-
prs = _merged_prs_since(since)
|
|
107
|
-
cl = _read_unreleased_section(Path(args.changelog))
|
|
108
|
-
|
|
109
|
-
missing = []
|
|
110
|
-
skipped_internal = []
|
|
111
|
-
for sha, subject, pr_n in prs:
|
|
112
|
-
# Skip merges that are themselves releases
|
|
113
|
-
if subject.startswith("[release]") or subject.startswith("[ release]"):
|
|
114
|
-
continue
|
|
115
|
-
if _is_in_changelog(subject, cl):
|
|
116
|
-
continue
|
|
117
|
-
# Heuristic: subjects that don't contain a story id and start with
|
|
118
|
-
# internal-only tags (chore: backlog ..., chore: rebase, etc.) are
|
|
119
|
-
# marked as "internal", less likely to need user-facing entry.
|
|
120
|
-
is_user_visible = any(p.search(subject) for p in USER_VISIBLE_PATTERNS)
|
|
121
|
-
if is_user_visible:
|
|
122
|
-
missing.append({"pr": pr_n, "sha": sha, "subject": subject})
|
|
123
|
-
else:
|
|
124
|
-
skipped_internal.append({"pr": pr_n, "sha": sha, "subject": subject})
|
|
125
|
-
|
|
126
|
-
if args.json:
|
|
127
|
-
json.dump({
|
|
128
|
-
"since": since,
|
|
129
|
-
"total_prs": len(prs),
|
|
130
|
-
"missing_user_visible": missing,
|
|
131
|
-
"skipped_internal": skipped_internal,
|
|
132
|
-
}, sys.stdout, indent=2, ensure_ascii=False)
|
|
133
|
-
print()
|
|
134
|
-
return 0
|
|
135
|
-
|
|
136
|
-
print(f"changelog audit since={since or '(no tag)'} scanned {len(prs)} PR(s)")
|
|
137
|
-
print()
|
|
138
|
-
if not missing:
|
|
139
|
-
print(f" ✓ audit ok — every user-visible PR is mentioned in CHANGELOG.md Unreleased")
|
|
140
|
-
if skipped_internal:
|
|
141
|
-
print(f" · {len(skipped_internal)} internal/infra PR(s) skipped from audit")
|
|
142
|
-
return 0
|
|
143
|
-
|
|
144
|
-
print(f" ⚠ {len(missing)} user-visible PR(s) without a CHANGELOG entry:")
|
|
145
|
-
for m in missing:
|
|
146
|
-
print(f" #{m['pr']} {m['subject']}")
|
|
147
|
-
print()
|
|
148
|
-
print(" Add bullets under '## Unreleased' in CHANGELOG.md before release,")
|
|
149
|
-
print(" or confirm these PRs are intentionally undocumented (rare).")
|
|
150
|
-
if skipped_internal:
|
|
151
|
-
print(f" ({len(skipped_internal)} internal PR(s) skipped from audit)")
|
|
152
|
-
return 0
|
|
153
|
-
|
|
154
|
-
if __name__ == "__main__":
|
|
155
|
-
sys.exit(main())
|