Skip to content

Commit ccbdecd

Browse files
chore(release): rebuild automatic release notes pipeline
1 parent 302f594 commit ccbdecd

6 files changed

Lines changed: 219 additions & 18 deletions

File tree

.github/pull_request_template.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## Summary
2+
3+
Describe what changed and why.
4+
5+
## PR Title Format (required)
6+
7+
Use Conventional Commits in the PR title:
8+
9+
`type(scope): short summary`
10+
11+
Examples:
12+
- `feat(cache): add Redis-backed model cache`
13+
- `fix(streaming): flush done marker in SSE`
14+
- `docs(config): clarify provider auto-discovery`
15+
16+
Allowed `type` values:
17+
- `feat`
18+
- `fix`
19+
- `perf`
20+
- `docs`
21+
- `refactor`
22+
- `test`
23+
- `build`
24+
- `ci`
25+
- `chore`
26+
- `revert`
27+
28+
Breaking changes:
29+
- Add `!` before `:` (example: `feat(api)!: remove legacy endpoint`)
30+
31+
## Release Notes
32+
33+
- User-facing work should use `feat`, `fix`, `perf`, or `docs`.
34+
- Internal-only work (`test`, `ci`, `build`, `chore`, many `refactor`s) is auto-labeled and excluded from release notes.
35+
- Use `release:skip` label to explicitly exclude an item from release notes.

.github/release.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
changelog:
2+
exclude:
3+
labels:
4+
- release:skip
5+
- release:internal
6+
- skip-release-notes
7+
categories:
8+
- title: Breaking Changes
9+
labels:
10+
- release:breaking
11+
- title: Features
12+
labels:
13+
- release:feature
14+
- title: Bug Fixes
15+
labels:
16+
- release:fix
17+
- title: Performance
18+
labels:
19+
- release:performance
20+
- title: Documentation
21+
labels:
22+
- release:docs
23+
- title: Dependencies
24+
labels:
25+
- dependencies
26+
- title: Maintenance
27+
labels:
28+
- release:maintenance
29+
- title: Other Changes
30+
labels:
31+
- release:other

.github/workflows/pr-title.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: PR Title
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, edited, reopened, synchronize]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: read
10+
11+
jobs:
12+
semantic-pr-title:
13+
if: github.event.pull_request.draft == false
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Validate PR title (Conventional Commits)
17+
uses: amannn/action-semantic-pull-request@v5
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
with:
21+
types: |
22+
feat
23+
fix
24+
perf
25+
refactor
26+
docs
27+
test
28+
build
29+
ci
30+
chore
31+
revert
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
name: Release Labels
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, edited, reopened, synchronize]
6+
7+
permissions:
8+
contents: read
9+
issues: write
10+
pull-requests: write
11+
12+
jobs:
13+
apply-release-labels:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Map PR title to release labels
17+
uses: actions/github-script@v7
18+
with:
19+
script: |
20+
const pr = context.payload.pull_request;
21+
const title = pr.title || "";
22+
const match = title.match(/^([a-z]+)(\([^)]+\))?(!)?:\s.+/);
23+
const type = match ? match[1] : "";
24+
const isBreaking = Boolean(match && match[3]);
25+
26+
const typeToPrimaryLabel = {
27+
feat: "release:feature",
28+
fix: "release:fix",
29+
perf: "release:performance",
30+
docs: "release:docs",
31+
refactor: "release:internal",
32+
test: "release:internal",
33+
build: "release:internal",
34+
ci: "release:internal",
35+
chore: "release:internal",
36+
revert: "release:other",
37+
};
38+
39+
const labelDefinitions = [
40+
{ name: "release:feature", color: "1d76db", description: "User-visible feature for release notes" },
41+
{ name: "release:fix", color: "d73a4a", description: "User-visible bug fix for release notes" },
42+
{ name: "release:performance", color: "5319e7", description: "Performance improvement for release notes" },
43+
{ name: "release:docs", color: "0075ca", description: "Documentation change for release notes" },
44+
{ name: "release:other", color: "fbca04", description: "Other release-noteworthy changes" },
45+
{ name: "release:internal", color: "cfd3d7", description: "Internal changes excluded from release notes" },
46+
{ name: "release:maintenance", color: "bfdadc", description: "Maintenance changes for release notes" },
47+
{ name: "release:breaking", color: "b60205", description: "Breaking change" },
48+
{ name: "release:skip", color: "ffffff", description: "Explicitly exclude from release notes" },
49+
];
50+
51+
for (const label of labelDefinitions) {
52+
try {
53+
await github.rest.issues.createLabel({
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
name: label.name,
57+
color: label.color,
58+
description: label.description,
59+
});
60+
} catch (error) {
61+
if (error.status !== 422) {
62+
throw error;
63+
}
64+
}
65+
}
66+
67+
const primaryLabel = typeToPrimaryLabel[type] || "release:other";
68+
const desired = [primaryLabel];
69+
if (isBreaking) desired.push("release:breaking");
70+
71+
const managedLabels = [
72+
"release:feature",
73+
"release:fix",
74+
"release:performance",
75+
"release:docs",
76+
"release:other",
77+
"release:internal",
78+
"release:breaking",
79+
];
80+
81+
const currentLabels = pr.labels.map((label) => label.name);
82+
const toRemove = managedLabels.filter(
83+
(label) => currentLabels.includes(label) && !desired.includes(label),
84+
);
85+
86+
for (const label of toRemove) {
87+
try {
88+
await github.rest.issues.removeLabel({
89+
owner: context.repo.owner,
90+
repo: context.repo.repo,
91+
issue_number: pr.number,
92+
name: label,
93+
});
94+
} catch (error) {
95+
if (error.status !== 404) {
96+
throw error;
97+
}
98+
}
99+
}
100+
101+
const toAdd = desired.filter((label) => !currentLabels.includes(label));
102+
if (toAdd.length > 0) {
103+
await github.rest.issues.addLabels({
104+
owner: context.repo.owner,
105+
repo: context.repo.repo,
106+
issue_number: pr.number,
107+
labels: toAdd,
108+
});
109+
}

.goreleaser.yaml

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,9 @@ snapshot:
4141
version_template: "{{ .Tag }}-next"
4242

4343
changelog:
44-
use: github
45-
sort: asc
46-
abbrev: -1
47-
filters:
48-
exclude:
49-
- "^docs:"
50-
- "^doc:"
51-
- "^Docs:"
52-
- "^test:"
53-
- "^tests:"
54-
- "^refactor:"
55-
- "^ci:"
56-
- "^chore:"
57-
- "^build:"
58-
- "^style:"
59-
- "^perf:"
60-
- "^Revert"
61-
- "^Merge"
44+
# Use GitHub's release notes generator (PR-based) instead of commit-based notes.
45+
# This makes release entries cleaner and aligned with .github/release.yml categories.
46+
use: github-native
6247

6348
release:
6449
footer: |

DEVELOPMENT.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ make lint # Check code quality
3535
make lint-fix # Auto-fix issues
3636
```
3737

38+
## Release Hygiene
39+
40+
Releases are generated automatically from merged PRs, categorized by labels and PR titles.
41+
42+
- PR titles are validated in CI using Conventional Commit format (`type(scope): summary`)
43+
- Release labels are auto-applied from PR title type (`feat` -> feature, `fix` -> bug fix, etc.)
44+
- Internal changes (`chore`, `ci`, `build`, `test`, most `refactor`) are excluded from release notes by default
45+
- Prefer **Squash and merge** so each PR lands as one commit aligned with the PR title
46+
- If needed, apply `release:skip` on a PR to force exclusion from release notes
47+
3848
## Log output
3949

4050
Log format is chosen automatically based on the environment:

0 commit comments

Comments
 (0)