Skip to content

Commit 60b7244

Browse files
committed
R591: automate changelog rollover on tagged release
1 parent 70bc0cc commit 60b7244

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

.github/workflows/build_push.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ jobs:
9090
run: |
9191
set -ex
9292
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
93+
echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
94+
echo "RELEASE_DATE=$(date -u +%F)" >> $GITHUB_ENV
95+
96+
- name: Finalize changelog for release notes
97+
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'ryacub/rayniyomi'
98+
run: |
99+
set -euxo pipefail
100+
python3 scripts/finalize_changelog_release.py "$RELEASE_VERSION" --date "$RELEASE_DATE"
93101
94102
- name: Sign APK
95103
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'ryacub/rayniyomi'
@@ -168,3 +176,29 @@ jobs:
168176
prerelease: false
169177
env:
170178
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
179+
180+
- name: Checkout main for changelog sync
181+
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'ryacub/rayniyomi'
182+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
183+
with:
184+
ref: main
185+
token: ${{ secrets.VERSION_BUMP_TOKEN }}
186+
path: changelog-sync
187+
188+
- name: Persist changelog rollover to main
189+
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'ryacub/rayniyomi'
190+
run: |
191+
set -euxo pipefail
192+
cd changelog-sync
193+
python3 scripts/finalize_changelog_release.py "$RELEASE_VERSION" --date "$RELEASE_DATE"
194+
195+
if git diff --quiet -- CHANGELOG.md; then
196+
echo "No changelog update needed."
197+
exit 0
198+
fi
199+
200+
git config user.name "github-actions[bot]"
201+
git config user.email "github-actions[bot]@users.noreply.github.com"
202+
git add CHANGELOG.md
203+
git commit -m "chore: finalize changelog for v${RELEASE_VERSION}"
204+
git push origin main
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Finalize CHANGELOG.md for a tagged release by rolling Unreleased forward.
4+
5+
Behavior:
6+
- If release section `## [<version>]` already exists: no-op.
7+
- Move the first `## Unreleased` section body into `## [<version>] - <date>`.
8+
- Recreate an empty `## Unreleased` template using existing subsection headings.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import datetime as dt
15+
import re
16+
import sys
17+
from pathlib import Path
18+
19+
20+
DEFAULT_HEADINGS = [
21+
"### Added",
22+
"### Fixed",
23+
"### Changed",
24+
"### CI",
25+
"### Other",
26+
]
27+
28+
29+
def parse_args() -> argparse.Namespace:
30+
parser = argparse.ArgumentParser()
31+
parser.add_argument("version", help="Release version, with or without leading 'v'")
32+
parser.add_argument(
33+
"--date",
34+
default=dt.date.today().isoformat(),
35+
help="Release date in YYYY-MM-DD format (default: today)",
36+
)
37+
parser.add_argument(
38+
"--file",
39+
default="CHANGELOG.md",
40+
help="Path to changelog file (default: CHANGELOG.md)",
41+
)
42+
return parser.parse_args()
43+
44+
45+
def find_unreleased_bounds(lines: list[str]) -> tuple[int, int]:
46+
start = -1
47+
for i, line in enumerate(lines):
48+
if line.strip() == "## Unreleased":
49+
start = i
50+
break
51+
if start == -1:
52+
raise ValueError("Could not find '## Unreleased' section.")
53+
54+
end = len(lines)
55+
for i in range(start + 1, len(lines)):
56+
if lines[i].startswith("## "):
57+
end = i
58+
break
59+
return start, end
60+
61+
62+
def main() -> int:
63+
args = parse_args()
64+
version = args.version.lstrip("v")
65+
changelog_path = Path(args.file)
66+
text = changelog_path.read_text(encoding="utf-8")
67+
lines = text.splitlines()
68+
69+
if re.search(rf"^## \[{re.escape(version)}\](?:\s|$)", text, flags=re.MULTILINE):
70+
print(f"Version section [{version}] already exists; no changes.")
71+
return 0
72+
73+
start, end = find_unreleased_bounds(lines)
74+
before = lines[:start]
75+
unreleased_body = lines[start + 1 : end]
76+
after = lines[end:]
77+
78+
headings = [line for line in unreleased_body if line.startswith("### ")]
79+
if not headings:
80+
headings = DEFAULT_HEADINGS
81+
82+
unreleased_text = "## Unreleased\n\n" + "\n\n".join(headings) + "\n"
83+
84+
release_body = "\n".join(unreleased_body).strip("\n")
85+
if not release_body:
86+
release_body = "### Other\n\n- No user-facing changes."
87+
release_text = f"## [{version}] - {args.date}\n\n{release_body}\n"
88+
89+
before_text = "\n".join(before).rstrip("\n")
90+
after_text = "\n".join(after).lstrip("\n")
91+
pieces = [before_text, unreleased_text, release_text, after_text]
92+
new_text = "\n\n".join(p for p in pieces if p.strip()) + "\n"
93+
94+
changelog_path.write_text(new_text, encoding="utf-8")
95+
print(f"Updated {changelog_path} for release {version} ({args.date}).")
96+
return 0
97+
98+
99+
if __name__ == "__main__":
100+
sys.exit(main())

0 commit comments

Comments
 (0)