fix(release): restore version bumping via scripts/bump-version.mjs (Phase 0 A2)#291
Conversation
The v0.15.0 tag points at a commit where both package.json and
apps/desktop/package.json still read 0.14.0. Symptom: electron-updater
sees a version mismatch, and any consumer that reads the version from
package.json would be out of sync with the tag.
Root cause is a broken contract in the release pipeline:
1. @semantic-release/git's `assets: [package.json, ...]` only COMMITS
files; it does not mutate them.
2. The original scripts/bump-version.js used to mutate them at the
`prepareCmd` step via @semantic-release/exec, but the script was
deleted in the knip cleanup (#279) — knip didn't follow
release.config.js as an entry point so flagged it unused.
3. PR #289 patched release.config.js to remove the dangling
prepareCmd, which made semantic-release runnable but kept versions
stale.
4. This commit restores step 1 properly.
scripts/bump-version.mjs is a pure-ESM script with zero dependencies.
It updates ONLY the `version` field of exactly two files
(package.json and apps/desktop/package.json), preserves the existing
trailing-newline convention, and fails non-zero on missing args,
unparseable JSON, or missing version field. Packages with independent
release cycles (notably packages/api on Cloudflare Workers) are
deliberately not in the target list.
release.config.js wires the script via @semantic-release/exec's
prepareCmd — same shape as the pre-#279 setup but with a justified,
documented comment block.
Verified locally:
$ node scripts/bump-version.mjs 0.15.1
bump-version: package.json 0.14.0 -> 0.15.1
bump-version: apps/desktop/package.json 0.14.0 -> 0.15.1
bump-version: updated 2 of 2 target(s)
This is Phase 0 A2 in the post-audit roadmap. Pairs with A1 (#290).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 57 minutes and 20 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…se 0 A4) (#293) ## Summary Phase 0 A4. Two guardrails on \`release.yml\` to make a silent or partial release impossible. ## Why The v0.15.0 release pipeline failed silently twice: 1. **Silent no-op**: PR #245's squash-merge title (\`release: audit + Ed25519 signed envelopes + lefthook (v0.15.0) (#245)\`) didn't match a conventional commit type in \`releaseRules\`. semantic-release exited cleanly with \"no release\" and the workflow ended green; the operator only noticed because no tag appeared. We patched this with #289 by adding a forcing \`feat:\` commit, but the trap will fire again on the next big squash unless the workflow refuses to silently no-op. 2. **Stale version**: even when the release finally cut, both \`package.json\` files still read \`0.14.0\` because the deleted \`scripts/bump-version.js\` was never re-introduced (fixed in PR #291). Tag points at one version, file content reads another — visible by anyone running \`jq .version package.json\` against the tag. ## Guardrails ### Pre-flight (dry-run) \`\`\`yaml - name: Pre-flight (dry-run) check run: | npx semantic-release --dry-run 2>&1 | tee /tmp/sr-dry.log if grep -qE \"There are no relevant changes\" /tmp/sr-dry.log; then echo \"::error::semantic-release dry-run: no release will be cut.\" exit 1 fi if ! grep -qE \"next release version is\" /tmp/sr-dry.log; then echo \"::error::semantic-release dry-run did not announce a next release version.\" exit 1 fi \`\`\` Stops the workflow loud and clear before \`--ci\` if commit-analyzer would have returned \"no release\". Actionable error messages point at \`release.config.js > releaseRules\` and the commit log. ### Post-flight (version assertion) \`\`\`yaml - name: Verify version bump applied run: | expected=$(grep -oE \"next release version is [0-9]+\\.[0-9]+\\.[0-9]+\" /tmp/sr-dry.log | tail -n 1 | awk '{print $NF}') root_v=$(jq -r .version package.json) desk_v=$(jq -r .version apps/desktop/package.json) if [ \"$root_v\" != \"$expected\" ] || [ \"$desk_v\" != \"$expected\" ]; then echo \"::error::Version mismatch after semantic-release.\" exit 1 fi \`\`\` Re-uses the captured dry-run log to know what version SHOULD have been written. Catches a misconfigured or skipped \`scripts/bump-version.mjs\` (PR #291) BEFORE the tag-triggered build downloads the stale package.json. ## What gets caught | Trap | Caught by | |---|---| | Non-conventional PR title (#245 trap) | Pre-flight: \"no relevant changes\" | | Wrong releaseRules / type filter | Pre-flight: \"did not announce next release\" | | \`prepareCmd\` missing or wrong path | Post-flight: version mismatch | | \`bump-version.mjs\` only updated one file | Post-flight: per-file diff | | semantic-release crashed mid-cycle | Native exit code from \`Run semantic-release\` step | ## Verification I dry-ran the gate logic locally against the current main commit log; the dry-run output contains the expected lines for both happy-path and no-op cases. No way to test end-to-end without actually invoking the workflow. ## Stack context Phase 0 A4. Independent from A1 (#290), A2 (#291), B-bundle (#292). Different files / steps; no overlapping changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Why develop's lint job has been failing since **2026-04-24** (over a month). Until this lands, **every Phase 0 PR (#290–#294) is structurally unmergeable** because branch protection requires \`lint\` to pass and \`strict: true\` requires PRs to match develop's tip. This is the unblock for the whole post-audit release stack. ## What Two orthogonal fixes that together get \`pnpm lint\` to **0 errors**. ### 1. \`preserve-caught-error\` × 4 in \`encryptionService.ts\` Four \`try/catch\` blocks re-throw a wrapped error without attaching the caught one: \`\`\`ts } catch (error) { throw new Error( \`Failed to encrypt content: \${error instanceof Error ? error.message : 'Unknown error'}\`, + { cause: error } // ← lints clean and preserves the stack ); } \`\`\` Affected throw sites: \`initialize\` (114), \`encrypt\` (261), \`decrypt\` (292), \`importKey\` (351). The \`{ cause }\` payload is the standard ES2022 way to chain errors; runtime semantics unchanged. ### 2. mcp-server tsconfig refactor for ESLint projectService \`packages/mcp-server/tsconfig.json\` was excluding \`src/__tests__\`. ESLint uses \`@typescript-eslint/parser\` with \`projectService: true\`, which delegates project discovery to the TypeScript LSP. The LSP walked up from the test file, found mcp-server/tsconfig.json with the explicit exclude, and rejected the test → **parsing error: \"was not found by the project service\"**. Fix: split build vs editor configs. - **tsconfig.json** — single source of truth for editors/lint/test. Includes everything under \`src\`. Adds \`vitest/globals\` to \`types\`. - **tsconfig.build.json** — extends tsconfig.json, re-adds \`exclude: [\"src/__tests__\"]\`. Used by the build script. - **package.json** — \`\"build\": \"tsc\"\` → \`\"build\": \"tsc -p tsconfig.build.json\"\`. Confirmed locally: - \`pnpm lint\` → 0 errors (39 warnings unchanged, all pre-existing import-x/order). - \`pnpm --filter @readied/mcp-server build\` succeeds; \`dist/__tests__/\` does not exist. - \`pnpm --filter @readied/mcp-server test\` → 5/5 pass. - \`pnpm -r typecheck\` succeeds. ## Why a separate PR (not bundled with #290 / A1) A1 is the Electron-pin commit. Mixing in a multi-file lint fix would muddy what's a release-pipeline change vs a code-hygiene change. Keeping this separate also means: this PR can go in first, then #290–#294 can rebase one by one and pass CI cleanly. ## Roadmap status - [ ] **this PR** — lint baseline unblock - [ ] #290 A1 (electron 41.7.1) - [ ] #291 A2 (bump-version.mjs) - [ ] #292 B (workflow surface) - [ ] #293 A4 (release guardrails) - [ ] #294 C1 (pr-title commitlint) - [ ] C2 follow-up — add \`commitlint\` to required checks after #294 lands - [ ] D — cut v0.15.1
## Why Phase 0 **C1** of the DevOps cleanup roadmap. After v0.15.0 traced back to PR #245's squash-merge producing a non-conventional commit message (`release: audit...`) which semantic-release silently rejected, the merge gate needs to block non-conventional PR titles **upstream of merge** — not just whine in CI. The check already existed as a step inside `ci.yml`'s `lint` job, but it shipped as a sub-step of a multi-purpose job. Branch protection can only require whole status checks, so requiring \`lint\` would also block on ESLint/Prettier failures. Pulling commitlint into its own workflow gives branch protection a clean, single-responsibility check name to require: \`PR title / commitlint\`. ## What changes - **New: \`.github/workflows/pr-title.yml\`** — runs commitlint against \`github.event.pull_request.title\` on pull_request \`opened\`, \`edited\`, \`reopened\`, \`synchronize\`. - **\`ci.yml\`** — drops the duplicated step from the \`lint\` job and leaves a one-line breadcrumb pointing at the new workflow. ## Security shape The workflow follows the [GitHub command-injection guidance](https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/): - \`github.event.pull_request.title\` is **never** interpolated directly into a \`run:\` script. It passes through \`env:\` as \`PR_TITLE\` and the shell reads \`\$PR_TITLE\` from the process environment. - The script uses \`printf '%s' "\$PR_TITLE" | pnpm commitlint\` instead of \`echo\` — a PR title beginning with \`-e\` or \`-n\` would otherwise be treated as an echo flag in bash/sh. ## Verification - \`pnpm commitlint --config commitlint.config.js\` on the local checkout exits non-zero for \`release: foo\`, \`hotfix: foo\`, \`Add feature\`; zero for \`feat: foo\`, \`fix(scope): foo\`, \`chore!: foo\`. - Type enum used: \`feat | fix | refactor | docs | test | chore | style | perf | ci | build | revert\` (sourced from \`commitlint.config.js\`). ## Follow-ups (Phase 0 C2) Once this merges, **C2** sets \`PR title / commitlint\` as a required status check on \`develop\` and \`main\` via \`gh api\` — that's the step that actually blocks #245-style merges. C1 is the pre-req: required checks must exist on the default branch before they can be required. ## Roadmap status - [x] A1 #290 — electron 41.7.1 pin - [x] A2 #291 — \`scripts/bump-version.mjs\` + release.config.js wire - [x] B #292 — workflow surface cleanup - [x] A4 #293 — release dry-run + version-assertion guardrails - [x] **C1 (this PR)** — PR-title commitlint standalone - [ ] C2 — branch protection \`gh api\` (next) - [ ] D — cut v0.15.1
## Summary Phase 0 B-bundle of the post-audit roadmap. Six independent fixes batched into one PR because they all touch the workflow YAML surface and reviewing them together is faster than three ping-pong PRs that all conflict on the same files. ## docs.yml | Change | Why | |---|---| | \`pnpm install\` → \`pnpm install --filter '@readied/web...' --ignore-scripts\` | Marketing-site install was the last workflow still firing apps/desktop's \`electron-builder install-app-deps\` step that fails on Linux + Node 22. Same shape as #287 (deploy-api) and #288 (release). | | Added \`permissions: contents: read\` | Cloudflare Pages deploy doesn't need anything beyond checkout | | Build step moved into \`working-directory: apps/web\` | Was inline \`cd apps/web && ...\` — explicit working-directory reads better | ## build.yml | Change | Why | |---|---| | \`windows-latest\` → \`windows-2025-vs2026\` | GitHub announced \`windows-latest\` migration to that image on **2026-06-15** (6 days from this commit). Pinning ahead avoids a surprise toolchain swap mid-release. | | Removed \`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true\` env | This was the migration toggle for the Node 20→24 actions rollout. With all actions now on @v5 (Node 24-native) it's no-op. | | Artifact upload \`if-no-files-found: ignore\` → \`error\` | Silent zero-asset releases are worse than a failed upload. If electron-builder swallowed an error, signing failed, working-directory drifted, etc., we want loud failure here, not a release un-drafted with no installers. | ## release.yml | Change | Why | |---|---| | Removed \`HUSKY: '0'\` env | Leftover from the husky → lefthook migration in #267. Lefthook only reads .git/hooks if those files exist; on fresh CI clones they don't. | ## deploy-api.yml | Change | Why | |---|---| | Added \`permissions: contents: read\` | Cloudflare deploy doesn't push commits or create issues; minimum-privilege default. | ## Action versions sweep (all 8 workflows) | From | To | |---|---| | \`actions/checkout@v4\` | \`@v5\` | | \`actions/setup-node@v4\` | \`@v5\` | | \`actions/cache@v4\` | \`@v5\` | | \`actions/cache/save@v4\` | \`@v5\` | | \`actions/cache/restore@v4\` | \`@v5\` | | \`actions/upload-artifact@v4\` | \`@v5\` | GitHub announced Node 20-based actions deprecation on **2026-06-16** (7 days from this commit). The \`@v5\` family runs on Node 24. ## Verification - ✅ \`pnpm -r typecheck\` — green - ✅ \`pnpm test\` — 17/17 (untouched) - ✅ YAML parsed locally; no syntax errors ## Stack context Phase 0 B-bundle. Pairs with #290 (A1 electron pin) and #291 (A2 bump-version). Independent files, can land in any order. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Release v0.15.1 — Phase 0 DevOps stabilization Brings the 7-PR DevOps cleanup chain to main and cuts a clean release. This is the verification gate for the whole Phase 0 effort — if anything breaks at tag, build, or publish, Phase 0 isn't done. ### What landed since v0.15.0 | PR | Phase | Summary | |----|-------|---------| | #290 | **A1** | \`fix(desktop)\`: pin Electron to ^41.7.1 so better-sqlite3 prebuilts apply (closes the v0.15.0 V8 ABI failure on all 3 build platforms) | | #291 | **A2** | \`fix(release)\`: restore version bumping via \`scripts/bump-version.mjs\` + \`@semantic-release/exec\` (closes the "tag at 0.14.0" trap) | | #292 | **B** | \`chore(ci)\`: workflow surface cleanup — actions @v4→@v5 sweep, \`windows-latest\` → \`windows-2025-vs2026\` pin, drop \`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24\`, \`if-no-files-found: error\`, \`permissions:\` blocks, HUSKY: '0' removal | | #293 | **A4** | \`chore(ci)\`: \`release.yml\` pre-flight dry-run gate + post-flight version assertion (closes the "silent no-release" trap) | | #294 | **C1** | \`ci\`: PR-title commitlint as a standalone workflow → required check on develop + main | | #295 | | \`fix(lint)\`: develop lint baseline (preserve-caught-error × 4 in encryptionService + mcp-server tsconfig split for ESLint projectService) | | #296 | | \`chore(ci)\`: unblock CI on develop — ignore CHANGELOG.md in Prettier (semantic-release writes it), \`pnpm install --ignore-scripts\` in setup job (same shape as release.yml + deploy-api.yml) | ### C2 — branch protection updates (already applied via gh api) Both \`develop\` and \`main\`: - **Required status checks**: \`lint\`, \`test\`, \`typecheck\`, \`CodeRabbit\`, \`commitlint\` - Force-pushes blocked - \`strict: true\` (PRs must be up to date) ### Release pipeline guardrails now in place - **Pre-merge**: PR-title commitlint blocks \`release:\`-style non-conventional squash titles upstream. - **Mid-release**: \`release.yml\` dry-run check fails loud if no release would be cut. \`scripts/bump-version.mjs\` mutates both \`package.json\` files. Post-flight assertion verifies both match the dry-run-announced version. - **Post-release**: \`build.yml\` artifact upload uses \`if-no-files-found: error\` (silent zero-asset releases die at upload). - **Native deps**: \`apps/desktop\` pinned to Electron 41.7.1 with prebuilt better-sqlite3. CI \`setup\` skips postinstall so workflow-side install never rebuilds native modules. ### Expected behavior of the Release pipeline after merge 1. Merge this PR → main tip advances. 2. Manually dispatch the **Release** workflow. 3. \`release.yml\` runs: - \`pnpm install --ignore-scripts\` (no native rebuild needed for semantic-release). - **Pre-flight dry-run** → "next release version is 0.15.1" (single \`fix(release):\` commit since v0.15.0). - \`npx semantic-release\`: - \`@semantic-release/exec\` runs \`node scripts/bump-version.mjs 0.15.1\` → both package.json files updated. - \`@semantic-release/git\` commits + pushes tag \`v0.15.1\`. - \`@semantic-release/github\` creates draft Release. - **Post-flight assertion** → both package.jsons read \`0.15.1\`. 4. Tag push triggers \`build.yml\` on macOS-14, windows-2025-vs2026, ubuntu-latest. 5. All 3 platforms succeed → publish job un-drafts the GitHub Release. 6. Auto-sync PR opens to merge main → develop. ### What still needs verification (post-release) - [ ] Tag push actually triggers Build (needs GH_TOKEN with workflow scope — A3 deferred, may need PAT regen) - [ ] Build completes on all 3 platforms with prebuilt better-sqlite3 (smoke-test desktop bundle after publish) - [ ] Auto-sync PR back to develop is created 🤖 This is the Phase 0 verification gate. Mobile + Plugin Marketplace UI remain deferred. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added PR title validation workflow for automated commit message compliance checks. * **Bug Fixes** * Enhanced error diagnostics in encryption operations. * Added pre-flight checks to release process to prevent failed deployments. * Stricter artifact validation in builds. * **Chores** * Updated GitHub Actions to latest stable versions. * Improved code formatting configuration and build scripts. * Adjusted Electron dependency version. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
Phase 0 A2. Restores the version-bump step semantic-release needs to actually write the new version into the package.json files BEFORE the git plugin commits them.
What was broken
The v0.15.0 tag points at a commit where both `package.json` and `apps/desktop/package.json` still read `0.14.0`:
```
$ git show v0.15.0:package.json | jq -r .version
0.14.0
$ git show v0.15.0:apps/desktop/package.json | jq -r .version
0.14.0
```
Symptom: electron-updater sees mismatched versions; any consumer reading from package.json drifts from the tag; ship-it-and-forget-it release becomes "wait, what version is this?".
Why
semantic-release pipeline is a chain. Each plugin has a contract.
The `assets:` list in `@semantic-release/git` ONLY commits files; it doesn't create the diff. Without an explicit mutation step, the git plugin sees no changes to package.json and commits an effectively empty diff (just the CHANGELOG update).
The original `scripts/bump-version.js` performed that mutation, wired via `@semantic-release/exec`. It was deleted in the knip cleanup (#279) under the false assumption nothing referenced it — knip didn't scan `release.config.js` as an entry point.
PR #289 patched `release.config.js` to remove the dangling `prepareCmd`, which made the workflow stop crashing but left versions stale.
Fix
`scripts/bump-version.mjs` (new) — pure-ESM, zero dependencies, updates ONLY the `version` field of exactly two files (`package.json` and `apps/desktop/package.json`). Preserves existing trailing-newline. Fails non-zero on missing version arg, unparseable JSON, or absent version field.
`release.config.js` — re-wires the `@semantic-release/exec` plugin (already a devDep) with `prepareCmd: 'node scripts/bump-version.mjs ${nextRelease.version}'`. Comment block in the file documents the history so the next maintainer doesn't repeat the mistake.
Packages with independent release cycles (notably `packages/api` on Cloudflare Workers, `packages/mcp-server`) are deliberately NOT in the target list. If we ever need to bump them, that's a separate concern with a separate script.
Verification
```
$ node scripts/bump-version.mjs 0.15.1
bump-version: package.json 0.14.0 -> 0.15.1
bump-version: apps/desktop/package.json 0.14.0 -> 0.15.1
bump-version: updated 2 of 2 target(s)
```
Edge cases:
`node scripts/bump-version.mjs` (no arg) → `missing version argument`, exit 1
`node scripts/bump-version.mjs 0.14.0` (already at version) → `already at 0.14.0, skipped`, exit 0
Same version on already-bumped files → idempotent, no diff
✅ `pnpm -r typecheck` — green
✅ `pnpm test` — 17/17 packages
Stack context
Phase 0 A2 of the post-audit devops roadmap. Pairs with #290 (A1, electron pin). Independent files, can land in any order.
🤖 Generated with Claude Code