Skip to content

Ship v3.2.1 with hardened release pipeline#35

Merged
chenliuyun merged 27 commits intomainfrom
fix/3.2.1-shebang
Apr 26, 2026
Merged

Ship v3.2.1 with hardened release pipeline#35
chenliuyun merged 27 commits intomainfrom
fix/3.2.1-shebang

Conversation

@chenliuyun
Copy link
Copy Markdown
Collaborator

Summary

Re-lands the full v3.2.1 work (previously rolled back from main after a post-release regression) together with a hardened release pipeline so this class of breakage can't reach the npm registry again.

P0 / P1 fixes included

  • capabilities regression (P0) — export COMMAND_META, add missing rules explain entry, add exhaustive coverage guard tests (88667cc)
  • API error exit codes (P0) — assert exit code 1 for API error 190 and add happy-path guard (a947cfc)
  • Policy alias deviceId pattern (P1) — relax to accept hex MAC and lowercase IDs (746f2ac)
  • agent-bootstrap --sections (P1) — project top-level payload keys (55a4875)
  • esbuild bundler (perf) — inline pure-JS deps into a single dist/index.js (76d4384)
  • schema export --capabilities — replace the inline catalog in capabilities with a pointer note (68b8fe8)
  • Test coverage expansion — daemon, rules (conflicts/doctor/summary/last-fired), webhook/suggest/health-check/scenes validate/simulate/status-sync/upgrade-check, bundle guards

Release pipeline hardening (b92948a, 12dae35)

The v3.2.0 / v3.2.1 failures both shipped because the broken artifact was only exercised after npm publish. This PR moves all critical smoke checks BEFORE publish and adds defense-in-depth after:

Pre-publish gates (new/extended):

  • scripts/copy-assets.mjs — force-injects #!/usr/bin/env node into dist/index.js and chmods 0o755 on every build
  • tests/version.test.ts — asserts shebang presence as a regression guard
  • scripts/smoke-pack-install.mjsnpm pack → install into throwaway project → execute node_modules/.bin/switchbot --version
  • Local pre-commit hook (verify:pre-commit: build:prod + version test) and pre-push hook (verify:pre-push: + smoke:pack-install), wired via core.hooksPath = .githooks on npm install
  • New CI job pack-install-smoke (esbuild) — runs the same smoke on every PR
  • New CI job pack-install-smoke-tsc (tsc, matches publish.yml) — closes the gap where the PR gate validated esbuild while publish shipped tsc
  • publish.yml step 5 now runs smoke:pack-install BEFORE npm publish --tag next; if it fails, nothing reaches the registry

Post-publish defense-in-depth:

  • New npm-published-smoke.yml workflow: waits for @next to appear, installs in a clean temp project, runs offline smoke (--version, --help, schema export, capabilities) + live smoke (doctor, devices list with real credentials)
  • On success: auto-promotes @next → @latest
  • On failure: auto-deprecates the version, but only when install_package or offline_smoke fails — never on live_smoke failures, since transient SwitchBot API outages must not deprecate a working package

Documentation:

  • New docs/release-pipeline.md explains the tsc vs esbuild split, the full gate sequence, and four invariants that future pipeline changes must preserve (linked from README)

Test plan

  • npm run verify:pre-commit green locally (build:prod + version test, 2/2 passed, shebang + parity)
  • npm run verify:pre-push green locally (adds smoke:pack-install — switchbot --version -> 3.2.1 from packed tarball)
  • npm run lint:md:changelog green
  • CI docs-lint green
  • CI test matrix (Node 18/20/22) green
  • CI bundle-smoke — advisory (Node 22 CJS interop tracking issue still open)
  • CI offline-smoke (size budgets) green
  • CI pack-install-smoke (esbuild) green
  • CI pack-install-smoke-tsc (tsc, matches publish) green
  • CI policy-schema-sync green or SKIP

After merge: cut release v3.2.1 → publish.yml runs → npm-published-smoke.yml auto-promotes next → latest.

chenliuyun and others added 23 commits April 25, 2026 17:26
…tus-sync start/run, test count

- Add afterEach vi.useRealTimers() to daemon stop/reload describe blocks to prevent fake timer leak
- Fix bundle-size test to skip size assertion when tsc output is present (detects esbuild bundle by absence of relative imports)
- Add status-sync start (--json + human) and run (exit 0 and exit 1) test coverage
- Update README test count to 1959
…log-unify

feat: P0 fixes, policy schema, agent-bootstrap --sections, esbuild bundler, capabilities/catalog unification, and comprehensive test coverage (1959 tests)
…d output

- Remove shebang from src/index.ts; bundle.mjs banner is the sole source
- Add createRequire-based require shim in banner so bundled CJS packages
  (yaml, commander) can call bare require('process') on Node 20/22
- Add scripts/cjs-shim.mjs as esbuild inject target for require polyfill
- Fix agent-bootstrap and upgrade-check to import version from src/version.ts
  instead of require('../../package.json') which breaks when bundled to
  a non-root dist/ location
- Rewrite bundle-size test: build to dist/bundle-test.js (same level as
  dist/index.js so ../package.json resolves correctly), add shebang-count,
  node --check, --version semver, and size < 15 MB assertions
…elease pipeline

Add a parallel pack-install-smoke-tsc CI job that runs `npm run build` (tsc)
+ smoke:pack-install, mirroring exactly what publish.yml does before npm publish.
Previously only the esbuild bundle was smoke-tested on PRs while publish shipped
the tsc output, so a tsc-specific packaging regression would only surface after
the release tag was already pushed.

Add docs/release-pipeline.md with the tsc vs esbuild split, the full gate
sequence, and the invariants that must hold for future pipeline changes. Link
it from README and the CHANGELOG 3.2.1 entry.
…sh source

publish.yml was running `npm run build` (tsc) + smoke:pack-install, but
`npm publish` triggers prepublishOnly, which does `clean && build:prod`
unconditionally. So the tarball that smoke validated was thrown away and
replaced with the esbuild bundle right before upload — smoke was verifying
a different artifact than what shipped.

Fix by making esbuild the single publish source:

- publish.yml step 2: `npm run build` -> `npm run build:prod`, so the
  artifact validated by smoke:pack-install is byte-identical to what
  prepublishOnly produces and what npm publish uploads.
- ci.yml: remove the `pack-install-smoke-tsc` job added in the previous
  commit — it was validating the tsc output that never ships.
- docs/release-pipeline.md: rewrite to reflect the single-publish-source
  model; add explicit invariant that publish.yml and prepublishOnly must
  use the same builder.
- CHANGELOG: update the 3.2.1 release-pipeline entry.
@chenliuyun
Copy link
Copy Markdown
Collaborator Author

Follow-up: align publish.yml with prepublishOnly (845237d)

Found a bigger issue while reviewing: publish.yml was using npm run build (tsc) + smoke:pack-install, but prepublishOnly runs clean && build:prod unconditionally on npm publish. So the tarball that smoke validated was thrown away and replaced with the esbuild bundle right before upload — smoke was verifying a different artifact than what shipped.

Fixed in this commit:

  • publish.yml step 2: npm run buildnpm run build:prod (now matches prepublishOnly)
  • Removed pack-install-smoke-tsc CI job added earlier in this PR — it was validating the tsc output that never actually ships
  • docs/release-pipeline.md rewritten to state explicitly: single publish source is esbuild, and added invariant feat(devices): cache deviceId→type locally and pre-validate commands … #1 that publish.yml step 2 and prepublishOnly must use the same builder
  • CHANGELOG.md 3.2.1 entry updated

Net effect: the tarball validated by smoke:pack-install is now byte-identical to what npm publish uploads. No artifact swap during publish.

Pre-push chain still green locally:

  • build:prod → 2.1 MB bundle
  • tests/version.test.ts → 2/2 passed
  • smoke:pack-installswitchbot --version -> 3.2.1

Now that the esbuild bundle is the single publish source, bundle-smoke
must catch bundle regressions on every supported Node version before a
PR can merge. Previously it was advisory (continue-on-error: true) and
single-node (20.x).

Changes:
- Remove continue-on-error. The stale "Node 22 CJS interop issues"
  comment dates from before 7bbbc44 fixed the CJS require shim; the
  bundle now runs cleanly on Node 18/20/22 (verified locally on 22.21).
- Add strategy.matrix.node-version = [18.x, 20.x, 22.x] with fail-fast:
  false, so any Node version that cannot run the bundle is surfaced
  independently.
- Update docs/release-pipeline.md: remove the "Known gaps" section,
  add invariant #5 making the matrix + blocking status explicit.
- CHANGELOG entry added.
chenliuyun added 3 commits April 26, 2026 09:40
On some Linux CI runners, vi.spyOn(process, 'kill') failed to reliably
intercept process.kill(pid, 0), so isProcessRunning hit the real kill
syscall against a PID that happened to exist in the container, got back
EPERM, and reported the stale process as still running. Replace the
spy with a direct property assignment and restore the original kill in
afterAll so the interception is deterministic across Node 18/20/22.
…pipeline

npm run build is now the only path that produces the published artifact.
It drives a 5-stage orchestrator in scripts/build.mjs:
  1. clean           remove dist/
  2. typecheck       tsc --noEmit
  3. bundle          scripts/bundle.mjs (esbuild)
  4. copy-assets     scripts/copy-assets.mjs (policy assets only)
  5. ensure-binary   scripts/ensure-binary.mjs (shebang + chmod 0755 guard)

prepublishOnly, verify:pre-commit, verify:pre-push, publish.yml, and the
bundle-smoke / pack-install-smoke CI jobs all call npm run build by name;
no job re-implements any step.

scripts/copy-assets.mjs no longer injects the shebang or chmods the entry.
The new scripts/ensure-binary.mjs is a regression guard: it asserts the
shebang is present and fails loudly (pointing at scripts/bundle.mjs) if
the esbuild banner ever drops it, rather than silently repairing the
output the way copy-assets used to.

Dropped scripts: build:prod, clean (folded into build.mjs).
Added script:   typecheck (tsc --noEmit).
@chenliuyun chenliuyun merged commit 8b43539 into main Apr 26, 2026
10 checks passed
@chenliuyun chenliuyun deleted the fix/3.2.1-shebang branch April 26, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant