Skip to content

Commit b9e373a

Browse files
committed
feat(ci): extract changelog generation script and use for draft releases
- Create script/generate-changelog.ts with reusable changelog generation logic - Update ci.yml draft-release job to use the new script instead of GitHub's generate-notes API - Ensures draft release notes follow the same format as published releases 🤖 Generated with assistance of oh-my-opencode
1 parent 9d10de5 commit b9e373a

File tree

2 files changed

+98
-16
lines changed

2 files changed

+98
-16
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,26 +79,16 @@ jobs:
7979
with:
8080
fetch-depth: 0
8181

82-
- name: Get latest published release tag
83-
id: latest-release
84-
run: |
85-
LATEST_TAG=$(gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty')
86-
echo "tag=${LATEST_TAG}" >> $GITHUB_OUTPUT
87-
echo "Latest published release: ${LATEST_TAG:-none}"
88-
env:
89-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82+
- run: git fetch --force --tags
83+
84+
- uses: oven-sh/setup-bun@v2
85+
with:
86+
bun-version: latest
9087

9188
- name: Generate release notes
9289
id: notes
9390
run: |
94-
if [ -n "${{ steps.latest-release.outputs.tag }}" ]; then
95-
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
96-
-f tag_name=next \
97-
-f previous_tag_name=${{ steps.latest-release.outputs.tag }} \
98-
--jq '.body')
99-
else
100-
NOTES="Initial release"
101-
fi
91+
NOTES=$(bun run script/generate-changelog.ts)
10292
echo "notes<<EOF" >> $GITHUB_OUTPUT
10393
echo "$NOTES" >> $GITHUB_OUTPUT
10494
echo "EOF" >> $GITHUB_OUTPUT

script/generate-changelog.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env bun
2+
3+
import { $ } from "bun"
4+
5+
const TEAM = ["actions-user", "github-actions[bot]", "code-yeongyu"]
6+
7+
async function getLatestReleasedTag(): Promise<string | null> {
8+
try {
9+
const tag = await $`gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty'`.text()
10+
return tag.trim() || null
11+
} catch {
12+
return null
13+
}
14+
}
15+
16+
async function generateChangelog(previousTag: string): Promise<string[]> {
17+
const notes: string[] = []
18+
19+
try {
20+
const log = await $`git log ${previousTag}..HEAD --oneline --format="%h %s"`.text()
21+
const commits = log
22+
.split("\n")
23+
.filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:|release:)/i))
24+
25+
if (commits.length > 0) {
26+
for (const commit of commits) {
27+
notes.push(`- ${commit}`)
28+
}
29+
}
30+
} catch {
31+
// No previous tags found
32+
}
33+
34+
return notes
35+
}
36+
37+
async function getContributors(previousTag: string): Promise<string[]> {
38+
const notes: string[] = []
39+
40+
try {
41+
const compare =
42+
await $`gh api "/repos/code-yeongyu/oh-my-opencode/compare/${previousTag}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
43+
const contributors = new Map<string, string[]>()
44+
45+
for (const line of compare.split("\n").filter(Boolean)) {
46+
const { login, message } = JSON.parse(line) as { login: string | null; message: string }
47+
const title = message.split("\n")[0] ?? ""
48+
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
49+
50+
if (login && !TEAM.includes(login)) {
51+
if (!contributors.has(login)) contributors.set(login, [])
52+
contributors.get(login)?.push(title)
53+
}
54+
}
55+
56+
if (contributors.size > 0) {
57+
notes.push("")
58+
notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
59+
for (const [username, userCommits] of contributors) {
60+
notes.push(`- @${username}:`)
61+
for (const commit of userCommits) {
62+
notes.push(` - ${commit}`)
63+
}
64+
}
65+
}
66+
} catch {
67+
// Failed to fetch contributors
68+
}
69+
70+
return notes
71+
}
72+
73+
async function main() {
74+
const previousTag = await getLatestReleasedTag()
75+
76+
if (!previousTag) {
77+
console.log("Initial release")
78+
process.exit(0)
79+
}
80+
81+
const changelog = await generateChangelog(previousTag)
82+
const contributors = await getContributors(previousTag)
83+
const notes = [...changelog, ...contributors]
84+
85+
if (notes.length === 0) {
86+
console.log("No notable changes")
87+
} else {
88+
console.log(notes.join("\n"))
89+
}
90+
}
91+
92+
main()

0 commit comments

Comments
 (0)