diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml index 658656a..98c7ffc 100644 --- a/.github/workflows/prepare-release-pr.yml +++ b/.github/workflows/prepare-release-pr.yml @@ -57,6 +57,15 @@ jobs: run: | echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - name: Update CHANGELOG + run: | + VERSION="v${{ steps.version.outputs.version }}" + if grep -q "^## \[Unreleased\]" CHANGELOG.md; then + sed -i "s/^## \[Unreleased\]/## [$VERSION]/" CHANGELOG.md + else + echo "::notice::No [Unreleased] section found in CHANGELOG.md, skipping" + fi + - name: Create release branch and commit id: create_release_branch run: | @@ -64,7 +73,7 @@ jobs: git config user.name "safe-formdata[bot]" git config user.email "252148499+safe-formdata[bot]@users.noreply.github.com" git checkout -b "$BRANCH_NAME" - git add package.json + git add package.json CHANGELOG.md git commit -m "chore(release): v${{ steps.version.outputs.version }}" git push --set-upstream origin "$BRANCH_NAME" echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f416770..153c8c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,6 +55,21 @@ jobs: run: | echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - name: Extract changelog section + id: changelog + run: | + VERSION="v${{ steps.version.outputs.version }}" + BODY=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## /{exit} found{print}" CHANGELOG.md) + 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]}') + if [ -z "$BODY" ]; then + echo "::warning::No changelog section found for $VERSION in CHANGELOG.md" + fi + { + echo "body<> "$GITHUB_OUTPUT" + - name: Ensure version not published run: | VERSION="v${{ steps.version.outputs.version }}" @@ -73,6 +88,9 @@ jobs: git push origin "v${{ steps.version.outputs.version }}" - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHANGELOG_BODY: ${{ steps.changelog.outputs.body }} run: | gh api \ --method POST \ @@ -80,7 +98,6 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/${{ github.repository }}/releases" \ -f tag_name="v${{ steps.version.outputs.version }}" \ + -f body="$CHANGELOG_BODY" \ -F draft=true \ -F generate_release_notes=true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 6f37971..ba224a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,7 +150,7 @@ export type ParseResult = } | { data: null; - issues: ParseIssue[]; + issues: [ParseIssue, ...ParseIssue[]]; }; ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index a6fa474..d95192a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,31 +22,37 @@ The `path` field has been removed from `ParseIssue`. interface ParseIssue { code: IssueCode; path: readonly []; // removed + key?: unknown; } // v0.2.0 interface ParseIssue { code: IssueCode; - key: string; // added (see below) + key: string; // narrowed (see below) } ``` **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. -### `ParseIssue.key` added (required, `string`) +### `ParseIssue.key` narrowed to required `string` -A required `key: string` field has been added to identify which FormData key caused the issue. +`key` existed in v0.1.x as `key?: unknown`. It is now required and typed as `string`. ```ts -// v0.1.x — no key field -issue.code; // "forbidden_key" +// v0.1.x +interface ParseIssue { + code: IssueCode; + key?: unknown; // optional, untyped +} -// v0.2.0 — key identifies the offending field -issue.code; // "forbidden_key" -issue.key; // "__proto__" +// v0.2.0 +interface ParseIssue { + code: IssueCode; + key: string; // required, typed +} ``` -**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. +**Migration**: Remove any `undefined` guards on `issue.key` and update type annotations that reference `ParseIssue.key` as `unknown`. ### `issues` on failure narrowed to a non-empty tuple diff --git a/README.md b/README.md index 5220e95..9c62a67 100644 --- a/README.md +++ b/README.md @@ -224,15 +224,14 @@ const { data, issues } = parse(formData); ### Result ```ts -export interface ParseResult { - data: Record | null; - issues: ParseIssue[]; -} +export type ParseResult = + | { data: Record; issues: [] } + | { data: null; issues: [ParseIssue, ...ParseIssue[]] }; ``` - `data` is non-null only when no boundary violations are detected - `data` is always a flat object; no structural inference is performed -- `issues` must always be checked by the caller +- Use `data !== null` to narrow the type; `issues` is `[]` on success and non-empty on failure ### Issues diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index cc20421..7c3c72e 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -31,7 +31,7 @@ This runs the following steps. ## Workflow Overview -1. **Prepare Release PR**: Bump version → Create release branch → Open PR for review +1. **Prepare Release PR**: Bump version → Update CHANGELOG → Create release branch → Open PR for review 2. **Review & Merge**: Maintainer reviews and merges release PR to main 3. **Publish**: Manual trigger of publish workflow → All checks run → npm publish diff --git a/skills/boundary-validator/examples/good-code.md b/skills/boundary-validator/examples/good-code.md index 1d52088..b0be59c 100644 --- a/skills/boundary-validator/examples/good-code.md +++ b/skills/boundary-validator/examples/good-code.md @@ -232,7 +232,7 @@ export type IssueCode = "invalid_key" | "forbidden_key" | "duplicate_key"; // ✅ ParseResult discriminated union export type ParseResult = | { data: Record; issues: [] } - | { data: null; issues: ParseIssue[] }; + | { data: null; issues: [ParseIssue, ...ParseIssue[]] }; // ✅ Type narrowing with data !== null if (result.data !== null) { @@ -315,7 +315,7 @@ function handleResult(result: ParseResult) { console.log(result.issues.length); // 0 } else { // TypeScript knows: result.data is null - // TypeScript knows: result.issues is ParseIssue[] + // TypeScript knows: result.issues is [ParseIssue, ...ParseIssue[]] for (const issue of result.issues) { console.error(issue.code); } diff --git a/skills/boundary-validator/references/api-contract.md b/skills/boundary-validator/references/api-contract.md index d6c3422..2f76ca7 100644 --- a/skills/boundary-validator/references/api-contract.md +++ b/skills/boundary-validator/references/api-contract.md @@ -88,7 +88,7 @@ if (result.data !== null) { console.log(result.data.username); } else { // TypeScript knows: data is null - // TypeScript knows: issues is ParseIssue[] + // TypeScript knows: issues is [ParseIssue, ...ParseIssue[]] console.error(result.issues); } ```