Skip to content
This repository was archived by the owner on Mar 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/prepare-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,23 @@ 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: |
BRANCH_NAME="release/v${{ steps.version.outputs.version }}"
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
Expand Down
21 changes: 19 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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<<EOF"
echo "$BODY"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Ensure version not published
run: |
VERSION="v${{ steps.version.outputs.version }}"
Expand All @@ -73,14 +88,16 @@ 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 \
-H "Accept: application/vnd.github+json" \
-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 }}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export type ParseResult =
}
| {
data: null;
issues: ParseIssue[];
issues: [ParseIssue, ...ParseIssue[]];
};
```

Expand Down
24 changes: 15 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,14 @@ const { data, issues } = parse(formData);
### Result

```ts
export interface ParseResult {
data: Record<string, string | File> | null;
issues: ParseIssue[];
}
export type ParseResult =
| { data: Record<string, string | File>; 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

Expand Down
2 changes: 1 addition & 1 deletion docs/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions skills/boundary-validator/examples/good-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export type IssueCode = "invalid_key" | "forbidden_key" | "duplicate_key";
// ✅ ParseResult discriminated union
export type ParseResult =
| { data: Record<string, string | File>; issues: [] }
| { data: null; issues: ParseIssue[] };
| { data: null; issues: [ParseIssue, ...ParseIssue[]] };

// ✅ Type narrowing with data !== null
if (result.data !== null) {
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion skills/boundary-validator/references/api-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
```
Expand Down