Skip to content

chore(ci): add release.yml pre-flight and post-flight guardrails (Phase 0 A4)#293

Merged
github-actions[bot] merged 1 commit into
developfrom
chore/release-guardrails
Jun 9, 2026
Merged

chore(ci): add release.yml pre-flight and post-flight guardrails (Phase 0 A4)#293
github-actions[bot] merged 1 commit into
developfrom
chore/release-guardrails

Conversation

@tomymaritano

Copy link
Copy Markdown
Collaborator

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 release: audit + Ed25519 signed envelopes + lefthook (v0.15.0) #245's squash-merge title (`release: audit + Ed25519 signed envelopes + lefthook (v0.15.0) (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 feat(release): cut v0.15.0 audit release #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 fix(release): restore version bumping via scripts/bump-version.mjs (Phase 0 A2) #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

Two guardrails added to release.yml to make a quiet release impossible:

Pre-flight (dry-run gate):
- runs `npx semantic-release --dry-run` before the real --ci invocation
- greps the output for the "next release version" line that
  commit-analyzer emits when it would actually release
- if the dry-run says "no relevant changes" (the silent-no-op v0.15.0
  trap), exits 1 with an actionable error message pointing at
  release.config.js > releaseRules and the offending commit message
- if no "next release version" line at all, also exits 1

This is the gate that would have stopped #245's squash-merge from
producing a silent release: the title was "release: audit..." which
isn't in releaseRules, so commit-analyzer would have said "no release"
during dry-run instead of letting the workflow exit green with nothing
tagged.

Post-flight (version-bump assertion):
- after semantic-release runs, re-reads package.json and
  apps/desktop/package.json
- compares against the version the dry-run announced
- if either is stale, exits 1 with a diff

This catches the v0.15.0-style "tag at old version" trap where the
bump-version step is misconfigured or didn't fire. The build workflow
fires on tag push immediately afterwards, so we need to catch
mismatches BEFORE the tag exists, not after a release un-drafts with
the wrong version metadata.

Phase 0 A4. Independent from A1/A2/B-bundle (different files / steps,
no overlapping changes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
readide Error Error Jun 9, 2026 1:11pm

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@tomymaritano, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 47 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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef736a1a-d712-480d-a17c-f1dc563b25ea

📥 Commits

Reviewing files that changed from the base of the PR and between d05ff2b and ba4d658.

📒 Files selected for processing (1)
  • .github/workflows/release.yml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/release-guardrails

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot enabled auto-merge (squash) June 9, 2026 13:10
@github-actions github-actions Bot merged commit ce850c2 into develop Jun 9, 2026
14 of 16 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba4d658564

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +88 to +89
- name: Verify version bump applied
run: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the bump guard before publishing the tag

In the stale-version scenario this check runs only after npx semantic-release has already completed; semantic-release's real run executes prepare and then creates/pushes the git tag before publish (the docs also note dry-run is the mode that skips prepare/publish). That means if scripts/bump-version.mjs is missing or leaves either package stale, this step can fail the workflow but the bad tag has already been pushed and can already trigger .github/workflows/build.yml, so it does not provide the advertised pre-tag guardrail.

Useful? React with 👍 / 👎.

github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## 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
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## 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
tomymaritano added a commit that referenced this pull request Jun 9, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant