ci+docs: extend regression guards to ubuntu+macos+windows

Previously only the workspace 'build' job ran cross-platform.
The new regression guards (smoke-install-no-bsqlite,
plugin-hooks-smoke, witness-verify) were ubuntu-only.

Matrix expansion per job, justified by what behaviour each guard
actually tests:

  smoke-install-no-bsqlite  → ubuntu + macos
  plugin-hooks-smoke        → ubuntu + macos
  witness-verify            → ubuntu + macos + windows  ← full

Why each scope:
  - smoke-install-no-bsqlite uses bash patterns + tests an
    OS-independent JS code path; Linux+macOS is sufficient signal.
  - plugin-hooks-smoke validates a bash-based hooks.json. Windows
    users hit it via WSL/git-bash (same as Linux/macOS); native
    cmd/PowerShell is not the user environment.
  - witness-verify is pure JS (@noble/ed25519 only). Windows
    coverage catches platform-specific bugs like CRLF
    normalization breaking the canonical manifest hash, or path
    resolution drift in file-marker lookups.

Implementation:
  - 'shell: bash' on Windows steps so git-bash is used.
  - $RUNNER_TEMP instead of /tmp for OS-portable temp paths.
  - 'pnpm pack --pack-destination $RUNNER_TEMP' replaces hardcoded /tmp.

ADR-102 §5 added documenting the matrix decisions, alongside the
existing §1-4 design. Implementation table updated to reflect the
new matrix coverage.

Cost: ~6 extra runner-minutes per CI invocation. Acceptable for
catching OS drift before users.

Co-Authored-By: RuFlo <ruv@ruv.net>
This commit is contained in:
ReuvenandRuFlo committed 2026-05-09 10:36:25 -04:00
1 parent f51c11e2c7
commit c518415909
2 files changed
+75 -22

No files matched your search

+45 -16
View File
@@ -146,11 +146,18 @@ jobs:
#
# Independent of the workspace `build` job because we only need to
# build a single package (memory) — no workspace-wide dist required.
name: Smoke (no better-sqlite3) / Node ${{ matrix.node }}
runs-on: ubuntu-latest
#
# Cross-platform: ubuntu + macos (Windows excluded because the smoke
# script uses bash-specific patterns and runs in user environments
# that typically use WSL/git-bash anyway. The runtime fallback path
# the smoke verifies is platform-independent — a passing run on
# Linux+macOS proves the JS code path works everywhere.)
name: Smoke (no better-sqlite3) / ${{ matrix.os }} / Node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node: ['22', '24']
steps:
@@ -177,22 +184,23 @@ jobs:
id: pack
working-directory: v3/@claude-flow/memory
run: |
# pnpm pack rewrites workspace:* → resolved versions (npm pack doesn't)
TARBALL=$(pnpm pack --pack-destination /tmp 2>&1 | grep -E "\.tgz$" | head -1)
# pnpm pack rewrites workspace:* → resolved versions (npm pack doesn't).
# Pack to RUNNER_TEMP so the path works on both ubuntu and macos.
TARBALL=$(pnpm pack --pack-destination "$RUNNER_TEMP" 2>&1 | grep -E "\.tgz$" | head -1)
[ -z "$TARBALL" ] && { echo "pnpm pack produced no tarball"; exit 1; }
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
- name: Install with --omit=optional (simulates Node 26 native build failure)
run: |
mkdir -p /tmp/smoke
cd /tmp/smoke
mkdir -p "$RUNNER_TEMP/smoke"
cd "$RUNNER_TEMP/smoke"
npm init -y >/dev/null
npm pkg set type=module
npm install "${{ steps.pack.outputs.tarball }}" --omit=optional --no-audit --no-fund
- name: Assert better-sqlite3 was NOT installed
run: |
if [ -d /tmp/smoke/node_modules/better-sqlite3 ]; then
if [ -d "$RUNNER_TEMP/smoke/node_modules/better-sqlite3" ]; then
echo "FAIL: better-sqlite3 installed despite --omit=optional — smoke setup is wrong"
exit 1
fi
@@ -200,8 +208,8 @@ jobs:
- name: Run smoke script
run: |
cp v3/@claude-flow/memory/scripts/smoke-no-bsqlite.mjs /tmp/smoke/
cd /tmp/smoke
cp v3/@claude-flow/memory/scripts/smoke-no-bsqlite.mjs "$RUNNER_TEMP/smoke/"
cd "$RUNNER_TEMP/smoke"
node smoke-no-bsqlite.mjs
plugin-hooks-smoke:
@@ -213,11 +221,17 @@ jobs:
#
# Independent of the workspace `build` job — only the cli package needs
# to be built. We use `pnpm --filter @claude-flow/cli...` to scope.
name: Plugin hooks smoke / Node ${{ matrix.node }}
runs-on: ubuntu-latest
name: Plugin hooks smoke / ${{ matrix.os }} / Node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Cross-platform: ubuntu + macos. Windows excluded because the
# plugin's hooks.json uses `/bin/bash -c '...'` and the synthetic
# JSON-on-stdin pipeline assumes POSIX shell. On Windows users
# run Claude Code via WSL or git-bash, which the test would also
# need to simulate; out of scope for this regression guard.
os: [ubuntu-latest, macos-latest]
# Node 22 is the project's documented baseline (engines.node: '>=20')
# but Node 20 had environment-specific issues with multi-line stdin
# round-tripping that didn't reproduce on Node 22 or locally — same
@@ -273,8 +287,18 @@ jobs:
# whose dist files are referenced by witness markers). Avoids depending
# on the workspace `build` job so unrelated package failures don't
# block this regression guard.
name: Witness verify (signed manifest)
runs-on: ubuntu-latest
#
# Cross-platform: ubuntu + macos + windows. The standalone verifier
# is pure JS (only @noble/ed25519) — no native deps, no shell calls.
# Catches platform-specific JSON canonicalization or path-resolution
# bugs (e.g. CRLF normalization on Windows breaking the manifest
# hash) before they reach users.
name: Witness verify (signed manifest) / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
needs: [smoke-install-no-bsqlite, plugin-hooks-smoke]
steps:
@@ -295,6 +319,7 @@ jobs:
- name: Install + build packages witness markers reference
working-directory: v3
shell: bash
run: |
pnpm install --frozen-lockfile
# --no-bail tolerates unrelated package failures. The standalone
@@ -303,6 +328,9 @@ jobs:
pnpm --recursive --no-bail run build || true
- name: Verify witness manifest (via standalone plugin script — no CLI dep)
# `shell: bash` makes Windows use git-bash so the same script
# works across all 3 OSes. RUNNER_TEMP is set on every runner.
shell: bash
run: |
# Use the project-agnostic verifier from ruflo-core plugin. This
# is the same script external adopters use — dogfooding ensures
@@ -313,16 +341,16 @@ jobs:
set +e
node plugins/ruflo-core/scripts/witness/verify.mjs \
--manifest verification.md.json \
--json > /tmp/witness-result.json 2> /tmp/witness-result.err
--json > "$RUNNER_TEMP/witness-result.json" 2> "$RUNNER_TEMP/witness-result.err"
VERIFY_EXIT=$?
set -e
echo "--- verify.mjs exit code: $VERIFY_EXIT ---"
echo "--- stderr ---"
cat /tmp/witness-result.err || true
cat "$RUNNER_TEMP/witness-result.err" || true
echo "--- summary ---"
node -e "
const fs = require('fs');
const raw = fs.readFileSync('/tmp/witness-result.json', 'utf8');
const raw = fs.readFileSync(process.env.RUNNER_TEMP + '/witness-result.json', 'utf8');
if (!raw.trim()) { console.error('verify.mjs produced no JSON output'); process.exit(1); }
const r = JSON.parse(raw);
console.log(JSON.stringify({signature: r.signature, summary: r.summary}, null, 2));
@@ -339,6 +367,7 @@ jobs:
- name: History summary (ADR-103 — surface regression-introduction commits)
# Soft gate: prints transitions since the previous snapshot. The
# `summary` subcommand exits non-zero on any newly-regressed fix.
shell: bash
run: |
if [ -f verification-history.jsonl ]; then
node plugins/ruflo-core/scripts/witness/history.mjs \
@@ -90,21 +90,29 @@ Negative assertions (`absent: 'Recording … : true'`) are critical. A naive `co
### 2. Wire it into `v3-ci.yml` as a blocking job
`plugin-hooks-smoke` runs after `build` succeeds and before `publish`, on Node 20 + 22:
`plugin-hooks-smoke` runs before `publish`, decoupled from the workspace `build` job (which is gated on unrelated test failures). The matrix is **cross-platform** to catch OS-specific shell behaviour:
```yaml
plugin-hooks-smoke:
needs: [build]
strategy:
matrix:
node: ['20', '22']
os: [ubuntu-latest, macos-latest] # bash needed; Windows out of scope
node: ['22']
steps:
- install + build v3
- install + scoped build of @claude-flow/cli...
- node plugins/ruflo-core/scripts/test-hooks.mjs \
"node $GITHUB_WORKSPACE/v3/@claude-flow/cli/bin/cli.js"
```
This *blocks* publishing on hook regression. A future PR that adds a non-existent flag to `hooks.json`, or re-introduces the `ctx.args[0] || ctx.flags.X` priority bug, fails this job before reaching users.
Cross-platform reasoning per job (full matrix in §5):
| Job | Linux | macOS | Windows | Why |
|---|---|---|---|---|
| `plugin-hooks-smoke` | ✓ | ✓ | — | Plugin's `hooks.json` uses `/bin/bash -c` and synthetic JSON-on-stdin assumes POSIX shell. Windows users run Claude Code via WSL/git-bash, which the test would also need to simulate; out of scope for this guard. |
| `smoke-install-no-bsqlite` | ✓ | ✓ | — | Smoke script uses bash patterns; same reasoning. |
| `witness-verify` | ✓ | ✓ | ✓ | Pure JS (only `@noble/ed25519`). Catches platform-specific JSON canonicalisation or path-resolution bugs (e.g. Windows CRLF normalisation breaking the manifest hash) before they reach users. |
This *blocks* publishing on hook regression. A future PR that adds a non-existent flag to `hooks.json`, or re-introduces the `ctx.args[0] || ctx.flags.X` priority bug, fails this job before reaching users on whichever OS the bug surfaces.
### 3. Codify the CLI flag-priority convention
@@ -137,14 +145,30 @@ This is the second instance (after `smoke-install-no-bsqlite` from #1867) of the
Future regression categories should follow the same shape rather than relying on in-process unit tests that miss subprocess/install/parser realities.
### 5. Cross-platform matrix
GitHub Actions provides `ubuntu-latest`, `macos-latest`, and `windows-latest` runners. The matrix per job reflects whether platform behaviour is load-bearing for *that specific guard*:
| Job | ubuntu | macos | windows | Rationale |
|---|---|---|---|---|
| `smoke-install-no-bsqlite` | ✓ | ✓ | — | Tests `npm install --omit=optional` round-trip behaviour. The smoke script uses bash; Windows runners have git-bash but the regression class (native build failure on Node 26 without prebuilds) is OS-independent. Linux + macOS coverage is sufficient signal for the JS code path. |
| `plugin-hooks-smoke` | ✓ | ✓ | — | The plugin's `hooks.json` uses `/bin/bash -c '...'`. Windows users run Claude Code via WSL/git-bash (where the test would behave the same as Linux/macOS); native cmd/PowerShell isn't the user environment for these hooks. |
| `witness-verify` | ✓ | ✓ | ✓ | Pure JS — only `@noble/ed25519`. Catches platform-specific bugs that pure unit tests miss: e.g. Windows CRLF line-ending normalization breaking the canonical manifest hash, or Node `path` differences breaking the marker-cited file lookup. Always full coverage. |
| `build` | ✓ | ✓ | ✓ | Pre-existing matrix; covers tsc behaviour across OSes. |
Bash steps in cross-platform jobs use `shell: bash` to make Windows fall through to git-bash, and `$RUNNER_TEMP` instead of `/tmp` for OS-portable temp paths.
The matrix expansion costs ~6 extra runner-minutes per CI invocation (3 OSes × ~2 min for witness-verify, 1 OS × ~2 min each for the two smoke jobs that gained macOS). Acceptable for catching OS drift before users do.
## Implementation
| Artifact | Path | Purpose |
|----------|------|---------|
| Plugin hook fix | `plugins/ruflo-core/hooks/hooks.json` | Real flags, multi-line-safe wrappers |
| Smoke harness | `plugins/ruflo-core/scripts/test-hooks.mjs` | 7 assertions; runs against any CLI invocation string |
| CI job | `.github/workflows/v3-ci.yml` (`plugin-hooks-smoke`) | Node 20 + 22 matrix; blocks `publish` |
| CI job | `.github/workflows/v3-ci.yml` (`plugin-hooks-smoke`) | ubuntu + macos × Node 22; blocks `publish` |
| CLI parser fix | `v3/@claude-flow/cli/src/commands/hooks.ts` | 14-site flag/positional priority swap |
| Witness verify cross-platform | `v3-ci.yml` (`witness-verify`) | ubuntu + macos + windows; pure-JS verifier dogfooding |
The smoke harness accepts an arbitrary CLI invocation string, so it can also run against the *published* CLI (`npx --yes @claude-flow/cli@latest`) as a post-release sanity check — the same script doubles as a pre-merge guard and a release canary.