Skip to content

Commit f96b7d3

Browse files
ci: build tidy3d release notes from changelog
1 parent 6672930 commit f96b7d3

2 files changed

Lines changed: 147 additions & 5 deletions

File tree

.github/workflows/tidy3d-python-client-release.yml

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -495,15 +495,51 @@ jobs:
495495
source_ref: ${{ needs.determine-workflow-scope.outputs.release_tag }}
496496
target_ref: ${{ needs.determine-workflow-scope.outputs.push_to_latest == 'true' && 'latest' || '' }}
497497
secrets: inherit # zizmor: ignore[secrets-inherit]
498-
498+
499+
prepare-github-release-notes:
500+
name: build-github-release-notes
501+
needs: [determine-workflow-scope, compile-tests-results]
502+
if: |
503+
always() &&
504+
(needs.compile-tests-results.outputs.proceed_deploy == 'true' || needs.compile-tests-results.result == 'skipped') &&
505+
needs.determine-workflow-scope.outputs.deploy_github_release == 'true'
506+
runs-on: ubuntu-latest
507+
permissions:
508+
contents: read
509+
env:
510+
RELEASE_TAG: ${{ needs.determine-workflow-scope.outputs.release_tag }}
511+
steps:
512+
- name: checkout-repo
513+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
514+
with:
515+
fetch-depth: 0
516+
persist-credentials: false
517+
518+
- name: build-github-release-notes
519+
run: |
520+
set -euo pipefail
521+
python3 scripts/build_github_release_notes.py \
522+
--tag "${RELEASE_TAG}" \
523+
--ref "${RELEASE_TAG}" \
524+
--changelog CHANGELOG.md \
525+
--output RELEASE_NOTES.md
526+
527+
- name: upload-github-release-notes
528+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
529+
with:
530+
name: github-release-notes-${{ env.RELEASE_TAG }}
531+
path: RELEASE_NOTES.md
532+
if-no-files-found: error
533+
499534
github-release:
500535
name: create-github-release
501-
needs: [determine-workflow-scope, compile-tests-results, deploy-packages]
536+
needs: [determine-workflow-scope, compile-tests-results, prepare-github-release-notes, deploy-packages]
502537
if: |
503538
always() &&
504539
(needs.compile-tests-results.outputs.proceed_deploy == 'true' || needs.compile-tests-results.result == 'skipped') &&
505540
needs.determine-workflow-scope.outputs.deploy_github_release == 'true' &&
506541
needs.determine-workflow-scope.outputs.deploy_pypi == 'true' &&
542+
needs.prepare-github-release-notes.result == 'success' &&
507543
needs.deploy-packages.result == 'success'
508544
runs-on: ubuntu-latest
509545
permissions:
@@ -512,17 +548,21 @@ jobs:
512548
RELEASE_TAG: ${{ needs.determine-workflow-scope.outputs.release_tag }}
513549
IS_RC_RELEASE: ${{ needs.determine-workflow-scope.outputs.is_rc_release }}
514550
steps:
515-
- name: checkout-tag
551+
- name: checkout-repo
516552
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
517553
with:
518-
ref: ${{ env.RELEASE_TAG }}
519554
persist-credentials: false
555+
556+
- name: download-github-release-notes
557+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
558+
with:
559+
name: github-release-notes-${{ env.RELEASE_TAG }}
520560

521561
- name: create-github-release
522562
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
523563
with:
524564
tag_name: ${{ env.RELEASE_TAG }}
525-
generate_release_notes: true
565+
body_path: RELEASE_NOTES.md
526566
env:
527567
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
528568

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import re
7+
import subprocess
8+
from pathlib import Path
9+
10+
RELEASE_HEADER_RE = re.compile(r"^## \[(?P<version>[^\]]+)\] - \d{4}-\d{2}-\d{2}\s*$")
11+
COMPARE_LINK_RE = re.compile(r"^\[(?P<version>[^\]]+)\]:\s+(?P<url>\S+)\s*$")
12+
13+
14+
def _normalize_version(value: str) -> str:
15+
return value[1:] if value.startswith("v") else value
16+
17+
18+
def _load_changelog_lines(changelog_path: Path, ref: str | None) -> list[str]:
19+
if ref is None:
20+
return changelog_path.read_text(encoding="utf-8").splitlines()
21+
22+
content = subprocess.run(
23+
["git", "show", f"{ref}:{changelog_path.as_posix()}"],
24+
check=True,
25+
capture_output=True,
26+
text=True,
27+
).stdout
28+
return content.splitlines()
29+
30+
31+
def _extract_release_body(changelog_lines: list[str], version: str) -> str:
32+
start_index: int | None = None
33+
34+
for index, line in enumerate(changelog_lines):
35+
match = RELEASE_HEADER_RE.match(line)
36+
if match and match.group("version") == version:
37+
start_index = index + 1
38+
break
39+
40+
if start_index is None:
41+
raise ValueError(f"Could not find CHANGELOG.md section for version {version!r}.")
42+
43+
end_index = len(changelog_lines)
44+
for index in range(start_index, len(changelog_lines)):
45+
if changelog_lines[index].startswith("## ["):
46+
end_index = index
47+
break
48+
49+
body = "\n".join(changelog_lines[start_index:end_index]).strip()
50+
if not body:
51+
raise ValueError(f"CHANGELOG.md section for version {version!r} is empty.")
52+
return body
53+
54+
55+
def _extract_compare_link(changelog_lines: list[str], version: str) -> str:
56+
for line in changelog_lines:
57+
match = COMPARE_LINK_RE.match(line)
58+
if match and match.group("version") == version:
59+
return match.group("url")
60+
raise ValueError(f"Could not find compare link for version {version!r} in CHANGELOG.md.")
61+
62+
63+
def build_release_notes(changelog_path: Path, tag: str, ref: str | None) -> str:
64+
version = _normalize_version(tag)
65+
changelog_lines = _load_changelog_lines(changelog_path=changelog_path, ref=ref)
66+
body = _extract_release_body(changelog_lines, version)
67+
compare_link = _extract_compare_link(changelog_lines, version)
68+
return f"## What's Changed\n\n{body}\n\n**Full Changelog**: {compare_link}\n"
69+
70+
71+
def main() -> None:
72+
parser = argparse.ArgumentParser(
73+
description="Build GitHub release notes from a CHANGELOG.md release section."
74+
)
75+
parser.add_argument("--tag", required=True, help="Release tag, for example v2.10.2.")
76+
parser.add_argument(
77+
"--changelog",
78+
default="CHANGELOG.md",
79+
help="Path to the changelog file. Defaults to CHANGELOG.md.",
80+
)
81+
parser.add_argument(
82+
"--ref",
83+
default=None,
84+
help="Optional git ref to read the changelog from, for example v2.10.2.",
85+
)
86+
parser.add_argument(
87+
"--output",
88+
required=True,
89+
help="Output path for the generated GitHub release notes markdown.",
90+
)
91+
args = parser.parse_args()
92+
93+
release_notes = build_release_notes(
94+
changelog_path=Path(args.changelog),
95+
tag=args.tag,
96+
ref=args.ref,
97+
)
98+
Path(args.output).write_text(release_notes, encoding="utf-8")
99+
100+
101+
if __name__ == "__main__":
102+
main()

0 commit comments

Comments
 (0)