|
| 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