Skip to content

ci: comprehensive overhaul — integration tier + secret scan + dep review + lockfile portability + tarball validation - #137

Merged
claudiusthebot merged 6 commits into
mainfrom
feat/ci-overhaul
May 10, 2026
Merged

ci: comprehensive overhaul — integration tier + secret scan + dep review + lockfile portability + tarball validation#137
claudiusthebot merged 6 commits into
mainfrom
feat/ci-overhaul

Conversation

@claudiusthebot

Copy link
Copy Markdown
Collaborator

Summary

Per Dylan's "overhaul the CI and make it much more comprehensive and robust, don't hold back." Five new jobs gated through the existing `CI Status` aggregate, plus a real package-distribution bug the new tarball checker uncovered.

The CI matrix had: quality, test (4-cell matrix), functional-cross-platform (3 OS), fuzz, security (npm audit), docker. Now it also has integration, secrets, dep-review, lockfile, tarball.

New jobs

integration — MCP-functional tier in CI (Ubuntu/macOS/Windows × Node 22)

The MCP-functional tier from #131 (`talon-mcp-functional.test.ts`) now runs on every PR + push across the full OS matrix. Drives a full Talon turn through stub claude → real handler.ts → real MCP server subprocess → real bridge → real Gateway → recording handler. Catches SDK→MCP→bridge→handler integration bugs (the PR #122 prefix-mismatch class) without needing a live external service.

Builds the stub-claude SEA binary on Windows (Node 20.19+ CVE-2024-27980 mitigation blocks .cmd shim spawning).

secrets — gitleaks scan

gitleaks/gitleaks-action@v2 with fetch-depth: 0 so it sees full git history, not just the latest commit. Catches API keys, bot tokens, .env files, OAuth secrets, private keys.

dep-reviewgithub/dependency-review-action on PRs

Diffs lockfile changes against the GitHub Advisory Database. fail-on-severity: moderate blocks PRs introducing moderate-or-higher CVEs. Comments summary on failure.

PR-only; ci-ok treats the resulting skipped on push runs as ok.

lockfile — portability check in fresh tmpdir

Copies ONLY `package.json` + `package-lock.json` to a fresh tmpdir and runs `npm ci` there. Same logic as `tools/verify-lockfile.sh` locally. Catches the "lockfile drift" / "lockfile only validates with --include=optional" classes the heartbeat journal documented (hb #48 / #103).

The other test jobs run `npm ci` in the source tree where `node_modules` state could mask portability bugs. This one starts from zero, every run.

tarball — validates npm pack contents

New `.github/scripts/tarball-check.mjs` — runs `npm pack`, inspects the tarball for forbidden patterns:

  • `.env*`, `credentials.json`, `.key`, `.pem`, `id_rsa`
  • `tests/`, `.test.ts`, `.spec.ts`
  • `node_modules`, `.git`, `coverage`, sourcemaps, `.vscode`, `.idea`, `.DS_Store`
  • Tarball size > 5MB (catches large-file commits)
  • `package.json` / `README` missing

Prints a sorted top-10 largest files for the CI step summary.

Real bug found by the tarball checker

First run flagged 270+ test files were being shipped in the npm tarball. `package.json#files` listed `"src/"` which swept in the entire test suite.

Fixed by replacing the broad `"src/"` entry with explicit subdirs (src/backend/, src/core/, src/frontend/, src/plugins/, src/storage/, src/util/, plus the entry-point files). Tarball is now 99 source-only files. Future versions of `talon-agent` on npm won't ship test fixtures.

Coverage config

`vitest.config.ts`: added an exclude list (test files, integration scaffolding, entry points) and `lcov` reporter for tooling compatibility (codecov, IDE plugins). Thresholds left at 60% — ratcheting up is out of scope for this PR (would risk breaking CI on unrelated paths). Future PRs can ratchet in 5% increments.

ci-ok gate updated

Now requires all 10 jobs (was 6). Branch protection (branch-protection.json) gates merges on CI Status, so adding a new required job means adding it to the needs: list + the check calls in the gate script.

Test plan

  • `tsc --noEmit` clean
  • `prettier --check` clean on touched files
  • `npm run test:integration` passes locally (4 tests, 6s)
  • `npm run tarball:check` passes — clean tarball, no findings
  • Lockfile portability passes (replicated tmpdir check locally, 7s)
  • YAML syntax valid
  • Existing test suite unaffected (1711 tests pass + 1 pre-existing main flake)
  • CI run on this branch (will exercise everything end-to-end)

What I deliberately didn't add

Considered, scope-cut for follow-up:

  • Mutation testing — stryker is heavy, scoped well to a separate PR
  • Bundle/install size budget — useful but Talon has no `dist/`
  • Per-file coverage thresholds for critical modules (handler.ts, gateway.ts) — risk of false-positive failures during refactors; ratchet up the global threshold first
  • License compliance check — Talon is not currently distributing widely, lower priority
  • Codecov / coveralls integration — needs an account + setup; lcov reporter is staged for it

🤖 Generated with Claude Code

…w, lockfile portability, tarball validation

Per Dylan's "overhaul the CI and make it much more comprehensive and
robust." Five new jobs gated through the existing `CI Status` aggregate,
plus a real package-distribution fix the new tarball checker uncovered.

## What's new

### `integration` — MCP-functional tier in CI (Ubuntu/macOS/Windows × Node 22)

The new MCP-functional tier from #131 (`talon-mcp-functional.test.ts`)
now runs on every PR + push across the full OS matrix. Catches
SDK→MCP→bridge→handler integration bugs (the PR #122 prefix-mismatch
class) without needing a live external service.

Builds the stub-claude SEA binary on Windows (Node 20.19+ blocks
spawning .cmd shims via child_process.spawn — CVE-2024-27980 mitigation).

### `secrets` — gitleaks scan on every push + PR

`gitleaks/gitleaks-action@v2` with `fetch-depth: 0` so it sees full
git history, not just the latest commit. Catches API keys, bot tokens,
.env files committed by accident, OAuth secrets, private keys.

### `dep-review` — github/dependency-review-action on PRs

Diffs lockfile changes against the GitHub Advisory Database.
`fail-on-severity: moderate` — moderate or higher CVE in any newly
introduced dep blocks the PR. Comments summary on PR on failure.

PR-only (no main-branch run); `ci-ok` treats `skipped` as ok.

### `lockfile` — portability check in fresh tmpdir

Copies ONLY `package.json` + `package-lock.json` to a fresh tmpdir
and runs `npm ci --ignore-scripts --no-audit` there. Same check as
`tools/verify-lockfile.sh` locally. Catches the "lockfile drift" /
"lockfile only validates with `--include=optional`" classes the
heartbeat journal documented (hb #48 / #103).

The other test jobs run `npm ci` in the source tree where
`node_modules` state could mask portability bugs. This one starts
from zero, every run.

### `tarball` — validates `npm pack` contents

New `.github/scripts/tarball-check.mjs` — runs `npm pack`, inspects
the resulting tarball for forbidden patterns:

- `.env*`, `credentials.json`, `*.key`, `*.pem`, `id_rsa`, etc.
- `__tests__/`, `*.test.ts`, `*.spec.ts`
- `node_modules`, `.git`, `coverage`, sourcemaps, `.vscode`, `.idea`,
  `.DS_Store`
- Tarball size > 5MB (catches accidental large-file commits)
- `package.json` / `README` missing

Also prints a sorted top-10 largest files for the CI step summary.

## Real bug uncovered

The tarball checker's first run flagged **270+ test files were being
shipped in the npm tarball**. `package.json#files` listed `"src/"`
which swept in the entire test suite. Fixed by replacing the broad
`"src/"` entry with explicit subdirs:

  - "src/backend/", "src/core/", "src/frontend/", "src/plugins/",
    "src/storage/", "src/util/"
  - "src/bootstrap.ts", "src/cli.ts", "src/index.ts", "src/login.ts"

Tarball size dropped from including 270+ test files to a clean 99
source-only files (180KB → still 180KB but with no test scaffolding).
Future versions of `talon-agent` on npm won't ship test fixtures.

## Coverage config

`vitest.config.ts`: added an exclude list (test files, integration
scaffolding, entry points) and `lcov` reporter for tooling
compatibility (codecov, IDE plugins). Thresholds left at 60% for now —
ratcheting up is out of scope for this PR (would risk breaking CI on
unrelated paths).

## ci-ok gate

Updated to require all 10 jobs (was 6). Branch protection in
`.github/branch-protection.json` already gates merges on `CI Status`,
so adding a new required job means adding it to the `needs:` list +
the `check` calls in the gate script — no separate config change.

## Test plan

- [x] `tsc --noEmit` clean
- [x] `prettier --check` clean on touched files
- [x] `npm run test:integration` passes locally (4 tests)
- [x] `npm run tarball:check` passes locally — clean tarball
- [x] Lockfile portability passes (replicated tmpdir check, 7s npm ci)
- [x] YAML syntax valid
- [x] Existing test suite unaffected (1711 tests pass + 1 pre-existing main flake)
…n there yet

The MCP-functional tier passes on Ubuntu + macOS but fails on Windows:
the stub's SEA-bundled MCP client + StdioClientTransport spawn-and-fetch
chain to the recording handler doesn't complete under SEA-on-Windows.
Three of four cases see zero captures at the recording handler, even
though `PostToolBatch: terminating SDK loop on mcp__telegram-tools__end_turn`
fires (so the SDK loop sees the tool, but MCP dispatch never lands).

Likely culprits to investigate (out of scope for this PR):
- esbuild SEA bundle handling of the dynamic `await import("@modelcontextprotocol/sdk/...")`
- StdioClientTransport.spawn behavior under SEA-on-Windows
- Localhost loopback / fetch from spawned subprocess on Windows

The other Windows test paths still run — `Tests (windows-latest, Node 22)`
and `Functional (windows-latest)` cover the platform without MCP dispatch
(sdk-stub, talon-functional). Reinstating Windows for `integration` is
gated on root-causing the failure.
…ndows debugging

Two changes targeting the Windows integration failure:

1. fake-claude.mjs: convert dynamic `await import("@modelcontextprotocol/sdk/...")`
   to static top-level imports. Likely root cause of the Windows failure —
   esbuild's CJS bundling preserves `await import(literal)` as `Promise.resolve(require(literal))`,
   which works locally where node_modules exists, but the SEA binary on
   Windows ships only the bundled .cjs without node_modules. Static
   imports force esbuild to inline the SDK module bodies into the bundle.

   Verified: rebuilt SEA bundle locally, `grep "var StdioClientTransport"
   fake-claude.cjs` confirms the SDK is now inlined.

2. talon-mcp-functional.test.ts: add `withProtocolLogOnFailure()` helper.
   The MCP-functional tier has subprocess hops (SDK loop → MCP server
   spawn → bridge HTTP) that produce no Vitest output when something
   goes wrong upstream of the actual `expect(...)`. This wrapper dumps
   the last 60 lines of the stub's protocol log on assertion failure
   so future Windows regressions are diagnosable from the CI log
   directly. Wraps the canary first test only (the others' assertions
   are all downstream of the same MCP path).

Local: all 11 MCP-functional tests still pass on Linux. Windows
verification has to come from the CI run on this branch.
…sx, not npx tsx

Real root cause of the Windows integration failure.

`buildMcpServers` was spawning `npx tsx <path>` on Windows and
`node --import <tsx-loader> <path>` on POSIX. The Windows path goes
through the MCP launcher (which proxies stdio), so the launcher then
runs `child_process.spawn("npx", [...])` to start the actual MCP server.

Node 20.19+ refuses to execute `.cmd` shims via `child_process.spawn`
without `shell: true` (CVE-2024-27980 mitigation). `npx` on Windows IS
a .cmd shim. Result: the launcher's spawn fails immediately, the MCP
server never starts, the stub's MCP client tries to connect over a
dead pipe and gets `MCP error -32000: Connection closed`.

The earlier hypothesis (SEA bundle missing dynamic imports) was wrong
— the static-imports change in 9507333 is still the right fix
(removes a dynamic-import gotcha entirely), but the actual blocker
was this command shape.

Fix: use `node --import <tsx-loader> <path>` on every platform. tsx as
a Node loader is platform-identical, and we never spawn `npx.cmd`. The
launcher's spawn now hits a plain `node` binary, which works under
Node's CVE mitigation.

Verified locally: 11 / 11 MCP-functional tests still pass on Linux.
Windows verification: this branch's CI run.

Diagnostic from the previous failed Windows CI run that pinned this:

  MCP_CONFIG servers: ["telegram-tools"]
  MCP connect error for telegram-tools: MCP error -32000: Connection closed

The launcher's spawned child died before completing initialize.
Second Windows iteration. Same `MCP error -32000: Connection closed` after
the previous fix, even with `node --import <path>` everywhere. The remaining
issue: `--import` accepts URLs or paths, but on Windows a raw backslash
path like `D:\a\talon\talon\node_modules\tsx\dist\esm\index.mjs` is
ambiguous between path and URL. Node's loader-hook resolution silently
fails to register tsx, every subsequent `.ts` import in `mcp-server.ts`
throws, the MCP server crashes, the stub's MCP client gets "Connection
closed" on its initialize handshake.

Fix: convert via `pathToFileURL(resolve(...)).href` so the loader URL is
always a clean `file:///D:/.../index.mjs` regardless of platform. Linux
already worked because `/path/to/foo.mjs` is unambiguous as both a path
and a URL.

Local: 11 / 11 still pass on Linux. Windows: this commit's CI run is
the truth-or-not test.
@claudiusthebot
claudiusthebot merged commit 03968e2 into main May 10, 2026
22 checks passed
@claudiusthebot
claudiusthebot deleted the feat/ci-overhaul branch May 10, 2026 10:40
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