Skip to content

Commit 59e8537

Browse files
committed
Refine release notes generation context
Use GitHub release notes plus commit data so direct-push changes are included, and update the prompt to dedupe and reference commits or PRs consistently.
1 parent 78f9500 commit 59e8537

2 files changed

Lines changed: 112 additions & 80 deletions

File tree

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
Generate release notes for version ${{ inputs.tag }}.
2-
Read the file `github_changes.tmp.json` in the current directory. It contains an array
3-
of commits between the previous and current release tags, each with:
4-
- sha: the commit SHA
5-
- fullMessage: the full commit message (multiline)
6-
- shortMessage: the first line of the commit message
7-
- author: the commit author name
8-
- date: the commit date (ISO 8601)
9-
- url: the commit URL on GitHub
10-
- pr: the associated PR info (number, title, body, url, author, labels, merged_at) or null if directly committed
11-
12-
Use ONLY this data — do not look up additional history.
2+
3+
Read the file `github_changes.tmp.json` in the current directory. It has this shape:
4+
5+
```json
6+
{
7+
"current_tag": "YYYY.MM.DD.N",
8+
"previous_tag": "YYYY.MM.DD.N",
9+
"github_generated_notes": "<markdown string from GitHub's generate-notes API>",
10+
"commits": [
11+
{
12+
"sha": "<short sha>",
13+
"message": "<first line of commit message>",
14+
"author": "<name>",
15+
"date": "<ISO 8601>",
16+
"url": "<commit URL>"
17+
}
18+
]
19+
}
20+
```
21+
22+
**Two sources, both equally important:**
23+
24+
- `github_generated_notes` — A markdown list of **merged PRs** in this release, generated server-side by GitHub. This is the authoritative source for changes that came in via pull request.
25+
- `commits` — Every commit in the range between the previous and current tag. This is the authoritative source for **direct-push changes** (commits not associated with a PR). Many real changes land this way.
26+
27+
**Deduplication:** A commit and a PR entry may describe the same change (e.g., a PR merge commit). When they clearly refer to the same thing, include it once — prefer the PR entry since it has richer metadata (number, author). Do not list a change twice.
28+
29+
Use ONLY data from these two sources — do not look up additional history.
1330

1431
---
1532

@@ -33,23 +50,24 @@ The release notes must follow this structure, in this order, with no additional
3350
backwards-incompatible changes. Each bullet should:
3451
- Clearly state what changed and what the impact is
3552
- Link to a migration guide or workaround if one exists
36-
- Reference the PR or issue number in parentheses at the end
53+
- Reference the PR or issue number in parentheses at the end, or the commit SHA if no PR exists
3754
If there are NO breaking changes, omit this entire section entirely. Do not include the heading with an empty list.
3855

39-
5. **New Features** — An H3 section (🆕 icon) listing new capabilities. Each bullet should:
56+
5. **New Features** — An H3 section ( icon) listing new capabilities. Each bullet should:
4057
- Bold the feature name, followed by an em dash, then a one-sentence description
41-
- Reference the PR or issue number in parentheses at the end
42-
- Example: **Feature Name** — Description of the feature. (#1234)
58+
- Reference the PR number in parentheses at the end, or the short commit SHA if no PR exists
59+
- Example (PR): **Feature Name** — Description of the feature. (#1234)
60+
- Example (direct push): **Feature Name** — Description of the feature. (abc1234)
4361

4462
6. **Bug Fixes** — An H3 section (🐛 icon) listing resolved issues. Each bullet should:
4563
- Start with "Fixed", "Resolved", or "Corrected"
4664
- Briefly describe the bug and its fix
47-
- Reference the PR or issue number in parentheses at the end
65+
- Reference the PR number or commit SHA in parentheses at the end
4866
- Example: Fixed an issue where [description of bug]. (#5678)
4967

50-
7. **Other Changes** — An H3 section (📦 icon) for deprecations, notable internal improvements, or anything that doesn't fit above, but only when it is meaningful to end users, operators, or integrators. Do NOT include items that are purely dependency updates or purely CI/workflow/build-pipeline maintenance. Same bullet format as the other sections.
68+
7. **Other Changes** — An H3 section (🔧 icon) for deprecations, notable internal improvements, or anything that doesn't fit above, but only when it is meaningful to end users, operators, or integrators. Do NOT include items that are purely dependency updates or purely CI/workflow/build-pipeline maintenance. Same bullet format as the other sections.
5169

52-
8. **Contributors** — An H3 section (🙏 icon) listing the people who contributed, prefixed with @. Format as a comma-separated line:
70+
8. **Contributors** — An H3 section (👥 icon) listing the people who contributed, prefixed with @. Format as a comma-separated line:
5371
Thanks to the following people who contributed to this release:
5472
@username1, @username2, @username3
5573

@@ -60,10 +78,11 @@ The release notes must follow this structure, in this order, with no additional
6078
- Do NOT add sections not listed above (no Upgrade Instructions, no Links, no FAQ, etc.).
6179
- Breaking Changes comes immediately after Overview, before New Features.
6280
- If a section has no items, omit the section and its heading entirely — do not leave an empty section.
63-
- Exclude changes that are purely dependency/version bumps, lockfile refreshes, package manager maintenance, or similar dependency housekeeping unless the input clearly shows a user-visible impact that should be called out.
64-
- Exclude changes that are purely CI, GitHub Actions, workflow, release automation, test harness, or build-pipeline maintenance unless the input clearly shows a user-visible operational impact that belongs in release notes.
65-
- If a PR or commit mixes substantive product changes with dependency or CI/workflow churn, include only the substantive product-facing change and ignore the maintenance-only parts.
66-
- Every bullet must end with a PR or issue number in parentheses, e.g. (#1234), unless the input provides none.
81+
- Treat direct-push commits as full equals to PR-based changes — do not bury or omit them because they lack a PR number.
82+
- Exclude changes that are purely dependency/version bumps, lockfile refreshes, or package manager maintenance unless there is a clear user-visible impact.
83+
- Exclude changes that are purely CI, GitHub Actions, workflow, release automation, test harness, or build-pipeline maintenance unless there is a clear user-visible operational impact.
84+
- If a PR or commit mixes substantive product changes with dependency or CI/workflow churn, include only the substantive product-facing change.
85+
- Every bullet must end with a PR number (#NNN) or short commit SHA in parentheses. Only omit a reference if the input provides absolutely none.
6786
- Keep the narrative overview concise (3-5 sentences). It should read like a human wrote it, not an automated log.
6887
- Use consistent tense: present tense for features ("Adds"), past tense for fixes ("Fixed").
69-
- Do not use the header divider (---) between sections; only use it after the title/date block and after the overview.
88+
- Do not use the header divider (---) between sections; only use it after the title/date block and after the overview.

.github/workflows/release-generate-notes.yml

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -22,75 +22,88 @@ jobs:
2222
token: ${{ secrets.GITHUB_TOKEN }}
2323
fetch-depth: 0
2424

25-
- name: Get previous release tag
26-
id: prev_tag
27-
run: |
28-
CURRENT_TAG="${{ inputs.tag }}"
29-
CURRENT_SHA=$(git rev-parse "${CURRENT_TAG}^{}")
30-
CURRENT_DATE=$(git log -1 --format=%ci "${CURRENT_SHA}")
31-
32-
PREV_TAG=""
33-
for tag in $(git tag --sort=-creatordate --list "[0-9]*.[0-9]*.[0-9]*.[0-9]*" "v*.*.*" 2>/dev/null); do
34-
if [ "$tag" = "$CURRENT_TAG" ]; then continue; fi
35-
tag_sha=$(git rev-parse "${tag}^{}" 2>/dev/null || true)
36-
[ -z "$tag_sha" ] && continue
37-
tag_date=$(git log -1 --format=%ci "${tag_sha}")
38-
if [ "$tag_date" '<' "$CURRENT_DATE" ]; then
39-
PREV_TAG="$tag"
40-
break
41-
fi
42-
done
43-
44-
echo "Previous release tag: $PREV_TAG"
45-
echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
46-
echo "current_tag=$CURRENT_TAG" >> "$GITHUB_OUTPUT"
47-
echo "current_sha=$CURRENT_SHA" >> "$GITHUB_OUTPUT"
48-
if [ -n "$PREV_TAG" ]; then
49-
echo "prev_sha=$(git rev-parse "${PREV_TAG}^{}")" >> "$GITHUB_OUTPUT"
50-
fi
51-
52-
- name: Gather commits and PR info
53-
id: commits
25+
- name: Gather release context via GitHub API
26+
id: context
5427
uses: actions/github-script@v9
5528
with:
5629
script: |
57-
const { data: comparison } = await github.rest.repos.compareCommits({
30+
const currentTag = '${{ inputs.tag }}';
31+
32+
// ── 1. Find the previous CalVer release using GitHub's Releases API ──
33+
// This is immune to git history rewrites/rebases: GitHub tracks releases
34+
// as server-side objects keyed by publish time, not by git ancestry.
35+
const calverRe = /^\d{4}\.\d{2}\.\d{2}\.\d+$/;
36+
37+
const { data: releases } = await github.rest.repos.listReleases({
5838
owner: context.repo.owner,
5939
repo: context.repo.repo,
60-
base: '${{ steps.prev_tag.outputs.tag }}',
61-
head: '${{ steps.prev_tag.outputs.current_tag }}'
40+
per_page: 100,
6241
});
6342
64-
const results = [];
65-
for (const commit of comparison.commits) {
66-
const sha = commit.sha;
67-
const fullMessage = commit.commit.message;
68-
const shortMessage = fullMessage.split('\n')[0];
69-
const author = commit.commit.author.name;
70-
const date = commit.commit.author.date;
71-
const url = commit.html_url;
43+
const calverReleases = releases
44+
.filter(r => calverRe.test(r.tag_name) && !r.draft)
45+
.sort((a, b) => new Date(b.published_at) - new Date(a.published_at));
7246
73-
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
74-
owner: context.repo.owner,
75-
repo: context.repo.repo,
76-
commit_sha: sha
77-
});
47+
const currentIdx = calverReleases.findIndex(r => r.tag_name === currentTag);
48+
const prevRelease = currentIdx >= 0 ? calverReleases[currentIdx + 1] : null;
49+
const prevTag = prevRelease?.tag_name ?? null;
50+
51+
core.info(`Current tag: ${currentTag}`);
52+
core.info(`Previous release tag: ${prevTag ?? '(none — first release)'}`);
53+
54+
// ── 2. Call generate-notes: GitHub tracks PR merges server-side, so
55+
// this is correct even when commits were rebased/force-pushed. ──
56+
const notesParams = {
57+
owner: context.repo.owner,
58+
repo: context.repo.repo,
59+
tag_name: currentTag,
60+
};
61+
if (prevTag) notesParams.previous_tag_name = prevTag;
7862
79-
const pr = prs[0] ? {
80-
number: prs[0].number,
81-
title: prs[0].title,
82-
body: prs[0].body,
83-
url: prs[0].html_url,
84-
author: prs[0].user?.login,
85-
labels: prs[0].labels?.map(l => l.name) || [],
86-
merged_at: prs[0].merged_at
87-
} : null;
63+
const { data: generated } = await github.rest.repos.generateReleaseNotes(notesParams);
64+
core.info(`generate-notes body length: ${generated.body.length}`);
8865
89-
results.push({ sha, fullMessage, shortMessage, author, date, url, pr });
66+
// ── 3. Get commits via compareCommits for the direct-push context ──
67+
// compareCommits uses merge-base, which may include rebased duplicates,
68+
// but Claude is instructed to use the PR list as the authoritative source
69+
// and treat commits only as supplementary detail for non-PR changes.
70+
let commits = [];
71+
if (prevTag) {
72+
try {
73+
const { data: comparison } = await github.rest.repos.compareCommits({
74+
owner: context.repo.owner,
75+
repo: context.repo.repo,
76+
base: prevTag,
77+
head: currentTag,
78+
});
79+
commits = comparison.commits.map(c => ({
80+
sha: c.sha.slice(0, 7),
81+
message: c.commit.message.split('\n')[0],
82+
author: c.commit.author.name,
83+
date: c.commit.author.date,
84+
url: c.html_url,
85+
}));
86+
} catch (e) {
87+
core.warning(`compareCommits failed: ${e.message} — proceeding without commit list`);
88+
}
9089
}
9190
91+
// ── 4. Write context file for Claude ──
9292
const fs = require('fs');
93-
fs.writeFileSync('${{ github.workspace }}/github_changes.tmp.json', JSON.stringify(results, null, 2));
93+
const ctx = {
94+
current_tag: currentTag,
95+
previous_tag: prevTag,
96+
// PR-based changes: generated server-side by GitHub, immune to git rebases.
97+
// Authoritative for anything that came in via pull request.
98+
github_generated_notes: generated.body,
99+
// Commit-based changes: all commits in the tag range.
100+
// Authoritative for direct-push changes (no PR). Many real changes land
101+
// this way. May overlap with PRs above — deduplicate by message when both
102+
// describe the same change, preferring the PR entry.
103+
commits,
104+
};
105+
fs.writeFileSync('${{ github.workspace }}/github_changes.tmp.json', JSON.stringify(ctx, null, 2));
106+
core.info(`Wrote context file. ${commits.length} commits, prev tag: ${prevTag}`);
94107
95108
- name: Generate release notes via Claude
96109
id: claude
@@ -122,4 +135,4 @@ jobs:
122135
with:
123136
name: release-notes
124137
path: ${{ runner.temp }}/release-notes/notes.md
125-
retention-days: 1
138+
retention-days: 1

0 commit comments

Comments
 (0)