Skip to content
This repository was archived by the owner on Mar 9, 2026. It is now read-only.

Commit ba01474

Browse files
roottoolclaude
andauthored
ci: automate CHANGELOG extraction for GitHub Release and release PR workflow (#60)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dcdd746 commit ba01474

8 files changed

Lines changed: 53 additions & 22 deletions

File tree

.github/workflows/prepare-release-pr.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,23 @@ jobs:
5757
run: |
5858
echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
5959
60+
- name: Update CHANGELOG
61+
run: |
62+
VERSION="v${{ steps.version.outputs.version }}"
63+
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
64+
sed -i "s/^## \[Unreleased\]/## [$VERSION]/" CHANGELOG.md
65+
else
66+
echo "::notice::No [Unreleased] section found in CHANGELOG.md, skipping"
67+
fi
68+
6069
- name: Create release branch and commit
6170
id: create_release_branch
6271
run: |
6372
BRANCH_NAME="release/v${{ steps.version.outputs.version }}"
6473
git config user.name "safe-formdata[bot]"
6574
git config user.email "252148499+safe-formdata[bot]@users.noreply.github.com"
6675
git checkout -b "$BRANCH_NAME"
67-
git add package.json
76+
git add package.json CHANGELOG.md
6877
git commit -m "chore(release): v${{ steps.version.outputs.version }}"
6978
git push --set-upstream origin "$BRANCH_NAME"
7079
echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT

.github/workflows/publish.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ jobs:
5555
run: |
5656
echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
5757
58+
- name: Extract changelog section
59+
id: changelog
60+
run: |
61+
VERSION="v${{ steps.version.outputs.version }}"
62+
BODY=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## /{exit} found{print}" CHANGELOG.md)
63+
BODY=$(echo "$BODY" | awk 'NF{found=1} found' | awk '{a[NR]=$0} NF{last=NR} END{for(i=1;i<=last;i++) print a[i]}')
64+
if [ -z "$BODY" ]; then
65+
echo "::warning::No changelog section found for $VERSION in CHANGELOG.md"
66+
fi
67+
{
68+
echo "body<<EOF"
69+
echo "$BODY"
70+
echo "EOF"
71+
} >> "$GITHUB_OUTPUT"
72+
5873
- name: Ensure version not published
5974
run: |
6075
VERSION="v${{ steps.version.outputs.version }}"
@@ -73,14 +88,16 @@ jobs:
7388
git push origin "v${{ steps.version.outputs.version }}"
7489
7590
- name: Create GitHub Release
91+
env:
92+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93+
CHANGELOG_BODY: ${{ steps.changelog.outputs.body }}
7694
run: |
7795
gh api \
7896
--method POST \
7997
-H "Accept: application/vnd.github+json" \
8098
-H "X-GitHub-Api-Version: 2022-11-28" \
8199
"/repos/${{ github.repository }}/releases" \
82100
-f tag_name="v${{ steps.version.outputs.version }}" \
101+
-f body="$CHANGELOG_BODY" \
83102
-F draft=true \
84103
-F generate_release_notes=true
85-
env:
86-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export type ParseResult =
150150
}
151151
| {
152152
data: null;
153-
issues: ParseIssue[];
153+
issues: [ParseIssue, ...ParseIssue[]];
154154
};
155155
```
156156

CHANGELOG.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,37 @@ The `path` field has been removed from `ParseIssue`.
2222
interface ParseIssue {
2323
code: IssueCode;
2424
path: readonly []; // removed
25+
key?: unknown;
2526
}
2627

2728
// v0.2.0
2829
interface ParseIssue {
2930
code: IssueCode;
30-
key: string; // added (see below)
31+
key: string; // narrowed (see below)
3132
}
3233
```
3334

3435
**Migration**: Remove all references to `issue.path`. Because `path` was always an empty array, any reference to it was effectively a no-op and can be deleted outright.
3536

36-
### `ParseIssue.key` added (required, `string`)
37+
### `ParseIssue.key` narrowed to required `string`
3738

38-
A required `key: string` field has been added to identify which FormData key caused the issue.
39+
`key` existed in v0.1.x as `key?: unknown`. It is now required and typed as `string`.
3940

4041
```ts
41-
// v0.1.x — no key field
42-
issue.code; // "forbidden_key"
42+
// v0.1.x
43+
interface ParseIssue {
44+
code: IssueCode;
45+
key?: unknown; // optional, untyped
46+
}
4347

44-
// v0.2.0 — key identifies the offending field
45-
issue.code; // "forbidden_key"
46-
issue.key; // "__proto__"
48+
// v0.2.0
49+
interface ParseIssue {
50+
code: IssueCode;
51+
key: string; // required, typed
52+
}
4753
```
4854

49-
**Migration**: Use `issue.key` wherever you need to identify the offending field. This is an additive change with no runtime impact, but type definitions that reference `ParseIssue` must be updated.
55+
**Migration**: Remove any `undefined` guards on `issue.key` and update type annotations that reference `ParseIssue.key` as `unknown`.
5056

5157
### `issues` on failure narrowed to a non-empty tuple
5258

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,14 @@ const { data, issues } = parse(formData);
224224
### Result
225225

226226
```ts
227-
export interface ParseResult {
228-
data: Record<string, string | File> | null;
229-
issues: ParseIssue[];
230-
}
227+
export type ParseResult =
228+
| { data: Record<string, string | File>; issues: [] }
229+
| { data: null; issues: [ParseIssue, ...ParseIssue[]] };
231230
```
232231

233232
- `data` is non-null only when no boundary violations are detected
234233
- `data` is always a flat object; no structural inference is performed
235-
- `issues` must always be checked by the caller
234+
- Use `data !== null` to narrow the type; `issues` is `[]` on success and non-empty on failure
236235

237236
### Issues
238237

docs/PUBLISHING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ This runs the following steps.
3131

3232
## Workflow Overview
3333

34-
1. **Prepare Release PR**: Bump version → Create release branch → Open PR for review
34+
1. **Prepare Release PR**: Bump version → Update CHANGELOG → Create release branch → Open PR for review
3535
2. **Review & Merge**: Maintainer reviews and merges release PR to main
3636
3. **Publish**: Manual trigger of publish workflow → All checks run → npm publish
3737

skills/boundary-validator/examples/good-code.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export type IssueCode = "invalid_key" | "forbidden_key" | "duplicate_key";
232232
// ✅ ParseResult discriminated union
233233
export type ParseResult =
234234
| { data: Record<string, string | File>; issues: [] }
235-
| { data: null; issues: ParseIssue[] };
235+
| { data: null; issues: [ParseIssue, ...ParseIssue[]] };
236236

237237
// ✅ Type narrowing with data !== null
238238
if (result.data !== null) {
@@ -315,7 +315,7 @@ function handleResult(result: ParseResult) {
315315
console.log(result.issues.length); // 0
316316
} else {
317317
// TypeScript knows: result.data is null
318-
// TypeScript knows: result.issues is ParseIssue[]
318+
// TypeScript knows: result.issues is [ParseIssue, ...ParseIssue[]]
319319
for (const issue of result.issues) {
320320
console.error(issue.code);
321321
}

skills/boundary-validator/references/api-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ if (result.data !== null) {
8888
console.log(result.data.username);
8989
} else {
9090
// TypeScript knows: data is null
91-
// TypeScript knows: issues is ParseIssue[]
91+
// TypeScript knows: issues is [ParseIssue, ...ParseIssue[]]
9292
console.error(result.issues);
9393
}
9494
```

0 commit comments

Comments
 (0)