Skip to content
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
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@
/electron-builder.yml @hoangsnowy
/.github/ @hoangsnowy
/SECURITY.md @hoangsnowy

# Dependency changes — any tweak here can break CI's `npm ci` (see v0.3.0
# release post-mortem in RELEASE.md). Require explicit review.
/package.json @hoangsnowy
/package-lock.json @hoangsnowy
/.nvmrc @hoangsnowy
11 changes: 11 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ For multi-line bodies, prefer reasoning over restating the diff.
4. Expose it on the preload bridge in `src/preload/index.ts`.
5. Consume it from React via TanStack Query (see existing hooks under `src/renderer/hooks/`).

## Dependency changes

`v0.3.0`, `v0.3.1`, and `v0.3.2` each shipped as ghost tags because `package-lock.json` drifted out of sync with `package.json` and the release workflow's `npm ci` step bailed. To keep that from recurring:

- **Always use `npm install`** when adding or removing a dep — never edit `package.json` by hand without running it. Avoid `npm install <pkg> --no-save`; commit both files together.
- **Use the pinned npm version.** CI runs `npm@11.6.2` (see `release.yml` + `ci.yml`). If your local npm differs significantly the lockfile may resolve differently. Match it with `npm install -g npm@11.6.2`.
- **Verify locally before pushing.** `npm ci --ignore-scripts --dry-run` must succeed against the committed lockfile.
- **The Husky `pre-commit` hook** rejects commits that stage `package.json` without also staging `package-lock.json`.
- **CI runs a `lockfile-check` job** on every PR that re-resolves the lock from `package.json` and fails if there is any diff. PRs cannot merge with a stale lock.
- **Optional WASM peer deps** (e.g. `@emnapi/core`, `@emnapi/runtime`) sometimes need to be pinned as direct devDependencies to satisfy `npm ci` strict mode — see the v0.3.2 fix.

## Security-sensitive changes

If your change touches: auth flow, secret storage, IPC surface, network egress, CSP, or sandbox flags, flag the PR with a `security` label and request review. See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure process.
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ on:
branches: [main]

jobs:
lockfile-check:
name: lockfile in sync with package.json
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Pin npm to match the version the lockfile was generated with
run: npm install -g npm@11.6.2

- name: Re-resolve lockfile from package.json
run: npm install --package-lock-only --ignore-scripts --no-audit --no-fund

- name: Fail if lockfile drifted
run: |
if ! git diff --exit-code package-lock.json; then
echo "::error::package-lock.json is out of sync with package.json. Run 'npm install' locally and commit the updated lockfile."
exit 1
fi

- name: Sanity-check npm ci against the committed lockfile
run: npm ci --ignore-scripts --dry-run

test:
name: typecheck + test + build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
14 changes: 14 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
#!/usr/bin/env sh

# Block commits that touch package.json without also touching package-lock.json
# (or vice versa). v0.3.0–v0.3.2 each shipped a broken release because a dep
# change landed without a regenerated lockfile.
staged="$(git diff --cached --name-only)"
pkg_changed=$(printf '%s\n' "$staged" | grep -E '^package\.json$' || true)
lock_changed=$(printf '%s\n' "$staged" | grep -E '^package-lock\.json$' || true)

if [ -n "$pkg_changed" ] && [ -z "$lock_changed" ]; then
echo "✖ package.json is staged but package-lock.json is not."
echo " Run 'npm install' and 'git add package-lock.json' before committing."
exit 1
fi

npx --no -- lint-staged
52 changes: 52 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Release playbook

How to cut a loopbridge release without producing a ghost tag.

## Pre-flight checklist

Run on a clean checkout of `main`:

- [ ] `git status` is clean and `git log` shows the merge commits you expect on `main`.
- [ ] `nvm use` (or `fnm use` / `volta`) — match the `.nvmrc` Node version.
- [ ] `npm install -g npm@11.6.2` — match the CI-pinned npm version.
- [ ] `npm ci --ignore-scripts --dry-run` succeeds. If it fails with `Missing: <pkg> from lock file`, **stop**: run `npm install`, inspect the diff, commit the lockfile fix as a separate `fix(release): regen lockfile` commit, and start over.
- [ ] `npm run lint -- --max-warnings 0` green.
- [ ] `npm run typecheck` green.
- [ ] `npm test` green.
- [ ] `npm run build` green.

If any check fails, fix it on `main` via a PR — **do not tag**.

## Cut the release

1. Update `package.json` `version` to `X.Y.Z` (no `v` prefix).
2. Update `package-lock.json` — the top-level `version` and `packages.""."version"` fields. Easiest: `npm install` and it rewrites them.
3. Update `CHANGELOG.md`: rename `[Unreleased]` to `[X.Y.Z] — YYYY-MM-DD`, add a fresh empty `[Unreleased]` section above, and append a link reference at the bottom.
4. Commit: `chore(release): X.Y.Z`. Push to `main`.
5. Tag: `git tag -a vX.Y.Z <merge-commit-sha> -m "vX.Y.Z"`.
6. Push the tag: `git push origin vX.Y.Z`. This triggers `release.yml`.

## Watch the workflow

- `gh run watch <run-id> --exit-status --interval 30`.
- If `Install deps (with native rebuild)` fails: the lockfile is out of sync. Stop, regenerate locally, push a `fix(release): regen lockfile` commit, bump to the next patch version (the failed tag is now a ghost — leave it), re-tag.
- If `Verify package.json matches tag` fails: `package.json` `version` does not match the tag. Bump `package.json`, commit, re-tag.

## Publish

The release pipeline creates a **draft** GitHub Release with the NSIS `.exe`, MSI `.msi`, and CycloneDX SBOM attached.

1. Open the draft URL printed by the workflow.
2. Verify the assets: `loopbridge-X.Y.Z-setup.exe`, `loopbridge-X.Y.Z-setup.msi`, `loopbridge-X.Y.Z-setup.exe.blockmap`, `sbom.cdx.json`.
3. Click **Publish release** in the GitHub UI. `electron-updater` will pick it up on the next app launch via the GitHub provider feed.

## Post-mortem index

| Tag | Status | Root cause |
| ------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| v0.3.0 | Ghost — `npm ci` missing `conventional-commits-parser` | Lockfile drifted; CI uses strict `npm ci` |
| v0.3.1 | Ghost — same plus `@emnapi/core` / `@emnapi/runtime` | Modern npm omits optional peer WASM deps from install tree |
| v0.3.2 | Ghost — `npm ci` still missing `conventional-commits-parser` | Local npm 11 hoisted differently than CI's npm 10 |
| v0.3.3 | ✅ Shipped | All three of the above fixed (lockfile regen + emnapi pin + npm version pin) |

Read this if a release fails — odds are you are reliving one of these.
Loading