Skip to content

Commit e574e6b

Browse files
committed
feat: add prep-release skill for managing SQLite releases
1 parent c28980d commit e574e6b

1 file changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
---
2+
name: prep-release
3+
description: Prepare a new release of @photostructure/sqlite. Syncs upstream Node.js + SQLite sources, updates npm deps, reviews commits since last release, decides semver bump (patch/minor/major), writes a CHANGELOG.md entry, and runs the full test+lint suite. Use when the user asks to "prep a release", "cut a release", "update everything and release", "sync upstream and release", or similar.
4+
---
5+
6+
# Prep Release
7+
8+
Prepare @photostructure/sqlite for a new release. This skill does NOT publish — it leaves the repo in a state where a human can trigger the GitHub Actions `Build & Release` workflow with the chosen version bump.
9+
10+
## Critical constraints
11+
12+
- **NEVER bump the `version` field in `package.json`** — the release GitHub Action (`.github/workflows/build.yml`) handles `npm version` based on the workflow_dispatch input (`patch` | `minor` | `major`). Manual bumps break the workflow.
13+
- **NEVER modify files under `src/upstream/`** — they are overwritten by sync scripts.
14+
- **Work on the designated branch** the session was started with (e.g. `claude/release-prep-automation-*`), NOT on `main`.
15+
- **Do NOT create a git tag, run `npm publish`, or create a GitHub release.** Those steps are the release workflow's job.
16+
17+
## Workflow
18+
19+
Create a todo list with TodoWrite for the steps below and work through them sequentially. Many steps run long (`npm run test:all`, `npm run precommit`) — surface failures immediately rather than pressing on.
20+
21+
### 1. Preflight
22+
23+
- Confirm current branch (`git branch --show-current`) matches the development branch specified for this session.
24+
- `git status` must be clean (or have only intentional in-progress work). Stash/commit anything unexpected before proceeding.
25+
- `git fetch --tags origin` so the latest release tag is visible.
26+
- Identify the last release:
27+
- Latest `vX.Y.Z` tag: `git ls-remote --tags origin | awk '/refs\/tags\/v[0-9]/ {print $2}' | sort -V | tail -1`
28+
- Cross-check with the top entry in `CHANGELOG.md` and the `version` field in `package.json` (they should already agree).
29+
- Capture baseline values from `package.json` BEFORE syncing, for later diffing:
30+
- `.versions.nodejs` (e.g. `v25.x-staging@ca2d6ea`) — the Node.js upstream commit we last synced from.
31+
- `.versions.sqlite` (e.g. `3.52.0`).
32+
- Current `.version` (last released version).
33+
- Note the README's current `Synced with Node.js vX.Y.Z` / `compatible with Node.js vX.Y.Z` strings — you'll need to bump these manually if the upstream sync advances past the referenced release (see §6).
34+
35+
### 2. Update deps, sync upstream, run full checks
36+
37+
Run the existing precommit orchestrator — it already does ~90% of release prep:
38+
39+
```bash
40+
npm run precommit
41+
```
42+
43+
This runs (see `scripts/precommit.ts`):
44+
45+
- `npm install` + `npm run update:actions` (pinact)
46+
- `npm-check-updates --upgrade` (respects `.ncurc.js` — pins eslint 9, cools down non-@photostructure deps 7 days)
47+
- `npm install` to re-sync the lockfile
48+
- `npm audit fix`, `npx snyk test --dev`
49+
- `npm run clean`
50+
- `npm run sync:node` — pulls `lib/sqlite.js`, `src/node_sqlite.{h,cc}` from `nodejs/node` (default branch `v25.x-staging`)
51+
- `npm run sync:tests` — pulls Node.js test files
52+
- `npm run sync:sqlite` — pulls the latest SQLite amalgamation from sqlite.org
53+
- `npm run fmt`, `npm run docs`, `npm run lint`, `npm run security`
54+
- `npm run build:dist`, `npm run build:native[:linux]`
55+
- `npm run test:all` (CJS + ESM)
56+
- On Node 22+: `lint:api`, `test:api`, `test:node`
57+
- On Linux/macOS: `lint:native` (clang-tidy)
58+
- `npm run memory:check`
59+
60+
### 2.5. When precommit can't run end-to-end
61+
62+
`precommit` depends on a set of tools that aren't always installed in ephemeral environments (osv-scanner, snyk, pinact, docker, valgrind, clang-tidy). It also hits the GitHub API unauthenticated, which rate-limits to 60/hour and will fail sync:tests if you've already burned the budget.
63+
64+
If it can't complete, do NOT skip steps blindly. Run its sub-steps individually in this order and surface each failure:
65+
66+
```bash
67+
npm install
68+
npx --no-install npm-check-updates -u # respects .ncurc.js
69+
npm install # re-resolve lockfile
70+
npm run sync:node # may require GITHUB_TOKEN
71+
npm run sync:sqlite
72+
npm run sync:tests # see §3.5 for common failures here
73+
npx prettier --cache --write test/node-compat/ # upstream tests use single quotes; normalize
74+
npm run build:native
75+
npm run build:dist
76+
npm run lint
77+
node --expose-gc node_modules/jest/bin/jest.js --no-coverage
78+
npm run test:node
79+
npm run test:api # pre-existing Node-22 failures are not regressions
80+
```
81+
82+
Notes:
83+
- The sync scripts cache the last-seen upstream SHA in `.sync-cache.json`. If you edited a sync script (skip list, a new text transform, etc.) but the upstream SHA hasn't moved, re-run with `--force` (e.g. `npx tsx scripts/sync-node-tests.ts --force`) or the cached SHA will short-circuit the download.
84+
- If `ncu` proposes a major bump on `typescript`, `typedoc`, `eslint`, `jest`, or `typescript-eslint`, check peer-dep compatibility before accepting. These are tightly coupled. Recent real-world examples:
85+
- `eslint` 10 — pinned in `.ncurc.js` because `typescript-eslint` 8 doesn't support it.
86+
- `typescript` 6 — pin because `typedoc` 0.28 doesn't support it.
87+
When you pin, add a comment in `.ncurc.js` citing the blocker so the next engineer doesn't un-pin it prematurely.
88+
89+
### 3. Review upstream changes
90+
91+
Now the repo has the latest upstream code. Summarize what changed since last release:
92+
93+
**Node.js upstream**: Diff from the old commit (captured in step 1) to the newly-synced commit. The sync script updates `package.json`'s `versions.nodejs` to the new commit. Run:
94+
95+
```bash
96+
# Use the OLD and NEW short SHAs from package.json versions.nodejs
97+
git -C ../node log --oneline <OLD_SHA>..<NEW_SHA> -- lib/sqlite.js src/node_sqlite.cc src/node_sqlite.h
98+
```
99+
100+
If `../node` isn't cloned locally, use GitHub's compare URL: `https://github.com/nodejs/node/compare/<OLD_SHA>...<NEW_SHA>` (view via WebFetch) and filter for the three files above.
101+
102+
Also diff `git diff src/upstream/` directly after the sync — the actual delta landing in our tree is usually smaller than the full compare range, and that's what you actually need to reason about.
103+
104+
Classify each upstream commit:
105+
- **API addition** (new method/option exposed) → MINOR
106+
- **API change or removal** (signature, defaults, error shape) → MAJOR
107+
- **Bug fix, internal refactor, test-only change** → PATCH
108+
109+
For non-trivial upstream code deltas, also check whether `src/sqlite_impl.cpp` — our port of `node_sqlite.cc` — needs the same change. Node.js fixes that touch callback lifetimes, error propagation, or memory management usually DO need a port. Pure stylistic refactors usually don't.
110+
111+
**SQLite**: Compare `versions.sqlite` before/after. SQLite's own release notes (https://www.sqlite.org/changes.html) classify changes. SQLite patch releases (3.52.0 → 3.52.1) are always PATCH. Minor bumps (3.51 → 3.52) are usually PATCH for us too unless they add a feature we newly expose.
112+
113+
**Our local commits**: `git log <last-tag>..HEAD --oneline` — categorize feat/fix/chore/breaking per Conventional Commits.
114+
115+
**Dep updates** alone are PATCH unless they bubble up a behavior change we care about.
116+
117+
### 3.5. When upstream tests fail after sync
118+
119+
`npm run sync:tests` copies every `test-sqlite-*.{js,mjs}` file from Node.js and lightly adapts them. Upstream Node.js moves fast; expect at least one failure class per major sync. Diagnose before skipping:
120+
121+
**A. SyntaxError at parse time** (e.g. `Unexpected identifier 'session'` pointing at a `using` declaration): Node.js has started using ERM (`using`/`await using`) and other newish syntax in tests. Our CI runs on Node 20+, which can't parse these in CJS. The fix is a **post-sync text transform** in `scripts/sync-node-tests.ts`, not a skip — adding to `skipTests` only renames `test()``test.skip()`; the body is still parsed and still fails.
122+
123+
Pattern to follow (already present in the script for `using``const`):
124+
125+
```ts
126+
// Rewrite ERM `using` declarations so the file parses in Node.js < 24 CJS.
127+
// The affected tests are transformed to test.skip() below, so the
128+
// substituted `const` body never actually runs.
129+
adapted = adapted.replace(/\busing\s+(\w+)\s*=/g, "const $1 =");
130+
```
131+
132+
After adding a transform, re-run with `--force` (the SHA cache will otherwise skip the regen) and `npx prettier --cache --write test/node-compat/`.
133+
134+
**B. `TypeError: db.X is not a function`**: upstream added a test file for a node:sqlite API we haven't ported yet (recent example: `test-sqlite-serialize.js` for `serialize()`/`deserialize()`). Options:
135+
136+
1. **Implement the API** — best, but usually out-of-scope for a release-prep session.
137+
2. **Skip the whole file** via `skipFiles` in `scripts/sync-node-tests.ts`. Add a comment with the feature name and a TODO referencing an issue to port it. Example:
138+
```ts
139+
// Tests DatabaseSync.prototype.serialize() / deserialize(), which are
140+
// Node.js-internal SQLite APIs we have not yet ported. Remove this entry
141+
// once the APIs are implemented.
142+
"test-sqlite-serialize.js",
143+
```
144+
Then delete the already-synced `test/node-compat/<name>.test.js` file so it doesn't sit stale in the tree, and re-run `sync:tests --force`.
145+
146+
**C. Per-test skip for behavior we've intentionally diverged on** (e.g. worker-thread races, GC-dependent tests): use the per-file `skipTests` map with an explicit `reason`. This is the only case where the existing skip mechanism is sufficient.
147+
148+
**D. Prettier diff noise**: upstream uses single quotes, our prettier config uses double. After every `sync:tests`, run `npx prettier --cache --write test/node-compat/` so the committed diff reflects only semantic changes.
149+
150+
### 4. Decide semver bump
151+
152+
Pick ONE of `patch | minor | major` based on the highest-severity change from step 3:
153+
154+
- **major** if ANY: breaking API change, removed/renamed exports, default behavior flipped, minimum Node version bumped, TypeScript signature change that breaks callers.
155+
- **minor** if ANY: new exported API, new option/method, new SQLite feature exposed. No breaking changes.
156+
- **patch** otherwise: bug fixes, dep updates, SQLite patch-level bumps, internal refactors, doc updates.
157+
158+
Compute the next version by applying the bump to `package.json`'s current version. **Do not write it back to `package.json`** — just use it for the CHANGELOG heading.
159+
160+
If the bump is ambiguous (e.g. a subtle behavior change that could be called a bug fix OR breaking), stop and ask the user with AskUserQuestion. Include the evidence (commit hash, before/after behavior) so they can decide without scrolling.
161+
162+
### 5. Write the CHANGELOG.md entry
163+
164+
Open `CHANGELOG.md`. Follow the existing style exactly:
165+
166+
- New section header: `## [X.Y.Z]` (no date yet — the release action commits on the release date, and prior entries show the release action leaves the date off until tagged; match whatever the most recent entries do).
167+
- Use these subsections in this order, only including ones that apply: `### Added`, `### Changed`, `### Fixed`, `### Removed`.
168+
- Mark breaking changes with `**BREAKING**:` prefix.
169+
- Lead each bullet with a bold feature name / area: e.g. `- **SQLite 3.52.1**: patch release, no API impact`.
170+
- Keep it terse. Users skim changelogs. One line per change. Link to upstream PRs (`[Node.js PR #12345](...)`) when the change traces back to upstream.
171+
- Add a reference link at the bottom: `[X.Y.Z]: https://github.com/PhotoStructure/node-sqlite/releases/tag/vX.Y.Z`
172+
- If `node:sqlite` API parity changed, mention the Node.js version we're now compatible with (e.g. "API compatible with `node:sqlite` from Node.js v25.10.0").
173+
174+
### 6. Update other docs
175+
176+
- **`README.md` (`Synced with` / `compatible with` strings)**: **manual bump required when syncing from a staging branch**. `scripts/sync-from-node.ts` only auto-updates the README when the sync source is a release tag (`v25.9.0`), not a staging branch (`v25.x-staging`). After a staging sync, determine the latest released Node.js tag whose `src/node_sqlite.cc` and `lib/sqlite.js` contents are fully contained in the synced commit. The simplest check: read `src/node_version.h` at the synced SHA — if it says `MAJOR.MINOR.PATCH` and `NODE_VERSION_IS_RELEASE=0`, then every prior released `vMAJOR.MINOR.(PATCH-1)` is fully contained. Use that as the README reference. Bump both the lead paragraph and the "Features" bullet.
177+
- **`doc/features.md`**: if SQLite bumped, update the SQLite version string. Check for other version-specific callouts that might need refreshing.
178+
- **`doc/api-reference.md`**: update if new APIs were added. Point to CHANGELOG for detail — don't duplicate.
179+
- Do NOT commit `build/docs/` (gitignored).
180+
- Do NOT update `package.json` version.
181+
182+
Cross-check with a single grep after edits:
183+
184+
```bash
185+
# No old Node-version or SQLite-version strings should linger in user-facing docs.
186+
grep -rn --include='*.md' "v25\.[0-9]\|SQLite 3\.[0-9]" README.md doc/ CHANGELOG.md
187+
```
188+
189+
### 7. Final verification
190+
191+
After CHANGELOG and README edits:
192+
193+
```bash
194+
npm run lint # cheap sanity check after doc edits
195+
git diff --stat # confirm only expected files changed
196+
git status # no stray untracked files
197+
```
198+
199+
The heavy tests (`test:all`, `memory:check`) already ran in step 2 — no need to re-run unless you touched code after.
200+
201+
### 8. Commit, push, and open PR
202+
203+
Use Conventional Commits (see CLAUDE.md §"Git Commit Messages"). Typical release-prep commits:
204+
205+
```
206+
chore(release): prep vX.Y.Z
207+
208+
- Sync Node.js upstream to <new-sha> (lib/sqlite.js, node_sqlite.{h,cc})
209+
- Sync SQLite to <new-version>
210+
- Update npm deps (<brief summary>)
211+
- Add CHANGELOG entry for vX.Y.Z
212+
```
213+
214+
If the sync produced meaningful changes to `src/sqlite_impl.cpp` or shims, split into separate commits (`chore(upstream): sync ...`, `chore(deps): ...`, `docs(changelog): ...`) for reviewability.
215+
216+
Stage explicitly — don't `git add -A`:
217+
218+
```bash
219+
git add package.json package-lock.json CHANGELOG.md README.md src/upstream/ src/sqlite_impl.* src/shims/ doc/ scripts/ test/node-compat/ .ncurc.js
220+
git diff --cached # review before committing
221+
git commit -m "..."
222+
git push -u origin <branch> # retry up to 4x with 2s/4s/8s/16s backoff on network errors
223+
```
224+
225+
**Do NOT push to `main` directly.** Push to the session's development branch.
226+
227+
**Open a PR when the branch is a `claude/*` branch** (the usual convention for web-session branches). Use `mcp__github__create_pull_request` with `base: main` and the branch name as `head`. Include in the PR body:
228+
- Version bump chosen + one-line justification
229+
- Upstream sync deltas (Node SHA old → new, SQLite old → new)
230+
- Dep bumps
231+
- Test results summary
232+
- Any pre-existing test failures you confirmed are NOT regressions (for reviewer context)
233+
234+
For non-`claude/*` branches where the user owns the workflow, just push and let them open the PR.
235+
236+
### 9. Hand off to user
237+
238+
In your final message, report:
239+
240+
1. **Version bump chosen**: `patch` | `minor` | `major` → next version `X.Y.Z`, with the 1–2 line justification.
241+
2. **Upstream sync summary**:
242+
- Node.js: `<old-sha>``<new-sha>` (N commits to sqlite files). Flag any commits that required a port to `src/sqlite_impl.cpp`.
243+
- SQLite: `<old>``<new>`
244+
3. **Dep updates**: list of major/minor bumps (skip patch bumps unless notable). Flag any that were pinned back in `.ncurc.js` and why.
245+
4. **CHANGELOG entry**: quote the new section verbatim for the user to review.
246+
5. **Test results**: pass/fail summary. Call out pre-existing failures (not regressions) with evidence.
247+
6. **Node-compat test changes**: any new files added to `skipFiles` or new transforms added to `sync-node-tests.ts`. These are likely follow-up work items.
248+
7. **PR link** (if opened) or push destination.
249+
8. **How to release**: Tell the user to merge this branch/PR to `main`, then trigger the `Build & Release` workflow with input `version = <patch|minor|major>`. The workflow runs `npm version`, tags, publishes to npm with provenance, and creates the GitHub release. Link: https://github.com/photostructure/node-sqlite/actions/workflows/build.yml
250+
251+
## Common gotchas
252+
253+
Learned from real release-prep sessions — consult this list when something surprises you:
254+
255+
- **`src/upstream/` is not the source of truth for implementation.** It's the exact Node.js source verbatim. The actual shipped code is `src/sqlite_impl.cpp` (ported). When upstream changes, ask yourself: "Does my port also need this change?" — fix-for-crash-on-musl commits upstream usually do; pure refactors usually don't.
256+
- **Sync scripts honor `.sync-cache.json`.** If you change the script's transform/skip logic, pass `--force` to re-apply against unchanged upstream.
257+
- **Rate limits.** Unauthenticated GitHub API gives you 60 req/hour across `sync:node`, `sync:tests`, and compare URLs. If you're iterating, export `GITHUB_TOKEN`.
258+
- **Prettier after every sync:tests.** Upstream uses single quotes; our prettier rewrites to double. Without the formatter pass, every re-sync shows a massive noise diff.
259+
- **Node 22 CJS can't parse ERM `using`.** Don't assume the tests will parse just because they ran in Node 25. The rewriter at `scripts/sync-node-tests.ts` handles `using` today; extend it for future Node-only syntax (e.g. import attributes) as needed.
260+
- **`test:api` has a pre-existing failure on Node <25.** It compares constants against the host's `node:sqlite`, which exposes far fewer constants on Node 22 than Node 25. If you inherit this failure, confirm via `git stash` + re-run that it exists on the baseline before calling it a regression.
261+
- **The `Build & Release` action bumps `package.json`, tags, and publishes.** You don't. Ever. If the action's input takes `patch|minor|major`, give it that — don't pre-stage a version commit.
262+
- **Use `AskUserQuestion` when the semver call is ambiguous.** Release decisions are cheap to pause on and expensive to get wrong.
263+
264+
## Things worth doing but not required
265+
266+
Mention these to the user if relevant; don't block on them:
267+
268+
- **Benchmarks**: `npm run bench` if perf-sensitive code changed — catches regressions vs. better-sqlite3.
269+
- **Stress tests**: `npm run stress:validate` — worth running if memory/threading code changed.
270+
- **Docker cross-platform**: `npm run test:docker:debian` and `test:docker:alpine` — catches glibc/musl divergence before CI does.
271+
- **Check open Dependabot/Snyk alerts** are closed or intentionally dismissed.
272+
- **Check open issues/PRs** for anything the user might want to land in this release.

0 commit comments

Comments
 (0)