|
4 | 4 | python .github/scripts/release_notes.py --since v0.142.3 --version 0.143.0 \ |
5 | 5 | --notes "$RUNNER_TEMP/notes.md" --changelog CHANGELOG.md |
6 | 6 |
|
7 | | -Groups conventional-commit subjects (Features, Fixes, Other) with links to |
8 | | -each commit, skips the release bot's own "chore: bump version" commits, writes |
9 | | -the list to --notes (the GitHub release body) and, with --changelog, replaces |
10 | | -the version's "See release notes ... for details." placeholder that |
11 | | -release-doc-sync writes with the same list plus the link. Without this, every |
12 | | -CHANGELOG entry and release body said nothing about what shipped. |
| 7 | +Splits the commits in two (#455): |
| 8 | +
|
| 9 | +- **For agents and users**: commits that touched what the plugin ships |
| 10 | + (skills/, rules/, snippets/, templates/, claude/), grouped Features, Fixes, |
| 11 | + Other. This is what a plugin user gets from the update, so it leads. |
| 12 | +- **Maintenance**: everything else (CI, tests, the site and gallery, |
| 13 | + showcase pieces, examples, docs), in a collapsed <details> block. |
| 14 | +
|
| 15 | +A release with no plugin change says so in one line above the collapsed |
| 16 | +block. Skips the release bot's own "chore: bump version" commits, writes the |
| 17 | +body to --notes (the GitHub release body) and, with --changelog, replaces the |
| 18 | +version's "See release notes ... for details." placeholder that |
| 19 | +release-doc-sync writes with the same body plus the link. |
13 | 20 | """ |
14 | 21 | from __future__ import annotations |
15 | 22 |
|
|
24 | 31 | ("Fixes", re.compile(r"^fix(\(.+?\))?!?:", re.I)), |
25 | 32 | ) |
26 | 33 | SKIP = re.compile(r"^chore: bump version to ", re.I) |
| 34 | +# What the plugin ships (scripts/build_plugin_dist.py DIRS): a change here is |
| 35 | +# a change a plugin user receives. |
| 36 | +USER_PATHS = ("skills/", "rules/", "snippets/", "templates/", "claude/") |
| 37 | +NO_USER_CHANGES = ("No changes to the plugin content (skills, rules, snippets, templates) " |
| 38 | + "in this release.") |
27 | 39 |
|
28 | 40 |
|
29 | | -def commits(since: str | None) -> list[tuple[str, str]]: |
| 41 | +def commits(since: str | None) -> list[tuple[str, str, list[str]]]: |
| 42 | + """(sha, subject, changed paths) per commit, newest first.""" |
30 | 43 | rng = f"{since}..HEAD" if since else "HEAD" |
31 | 44 | out = subprocess.run( |
32 | | - ["git", "log", rng, "--no-merges", "--format=%H%x09%s"], |
| 45 | + ["git", "log", rng, "--no-merges", "--name-only", "--format=%x00%H%x09%s"], |
33 | 46 | capture_output=True, text=True, check=True, |
34 | 47 | ).stdout |
35 | 48 | rows = [] |
36 | | - for line in out.splitlines(): |
37 | | - sha, _, subject = line.partition("\t") |
| 49 | + for block in out.split("\0")[1:]: |
| 50 | + head, _, files = block.partition("\n") |
| 51 | + sha, _, subject = head.partition("\t") |
38 | 52 | if subject and not SKIP.match(subject): |
39 | | - rows.append((sha, subject.strip())) |
| 53 | + rows.append((sha, subject.strip(), [f for f in files.splitlines() if f.strip()])) |
40 | 54 | return rows |
41 | 55 |
|
42 | 56 |
|
43 | | -def render(rows: list[tuple[str, str]]) -> str: |
44 | | - buckets: dict[str, list[str]] = {name: [] for name, _ in GROUPS} |
45 | | - buckets["Other"] = [] |
46 | | - for sha, subject in rows: |
47 | | - name = next((n for n, rx in GROUPS if rx.match(subject)), "Other") |
48 | | - buckets[name].append(f"- {subject} ([`{sha[:7]}`]({REPO_URL}/commit/{sha}))") |
| 57 | +def is_user_facing(files: list[str]) -> bool: |
| 58 | + return any(f.startswith(USER_PATHS) for f in files) |
| 59 | + |
| 60 | + |
| 61 | +def _line(sha: str, subject: str) -> str: |
| 62 | + return f"- {subject} ([`{sha[:7]}`]({REPO_URL}/commit/{sha}))" |
| 63 | + |
| 64 | + |
| 65 | +def render(rows: list[tuple[str, str, list[str]]]) -> str: |
| 66 | + if not rows: |
| 67 | + return "No changes recorded." |
| 68 | + user = [r for r in rows if is_user_facing(r[2])] |
| 69 | + maint = [r for r in rows if not is_user_facing(r[2])] |
| 70 | + |
49 | 71 | parts = [] |
50 | | - for name, lines in buckets.items(): |
51 | | - if lines: |
52 | | - parts.append(f"### {name}\n\n" + "\n".join(lines)) |
53 | | - return "\n\n".join(parts) if parts else "No changes recorded." |
| 72 | + if user: |
| 73 | + buckets: dict[str, list[str]] = {name: [] for name, _ in GROUPS} |
| 74 | + buckets["Other"] = [] |
| 75 | + for sha, subject, _ in user: |
| 76 | + name = next((n for n, rx in GROUPS if rx.match(subject)), "Other") |
| 77 | + buckets[name].append(_line(sha, subject)) |
| 78 | + groups = [f"**{name}**\n\n" + "\n".join(lines) for name, lines in buckets.items() if lines] |
| 79 | + parts.append("### For agents and users\n\n" + "\n\n".join(groups)) |
| 80 | + else: |
| 81 | + parts.append(NO_USER_CHANGES) |
| 82 | + if maint: |
| 83 | + parts.append( |
| 84 | + f"<details>\n<summary>Maintenance ({len(maint)} " |
| 85 | + f"commit{'s' if len(maint) != 1 else ''}): CI, tests, site, examples, showcase, docs" |
| 86 | + "</summary>\n\n" |
| 87 | + + "\n".join(_line(sha, subject) for sha, subject, _ in maint) |
| 88 | + + "\n\n</details>" |
| 89 | + ) |
| 90 | + return "\n\n".join(parts) |
54 | 91 |
|
55 | 92 |
|
56 | 93 | def patch_changelog(path: str, version: str, body: str) -> bool: |
|
0 commit comments