diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c9a02a..ccd808e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,15 @@ jobs: - run: bun run typecheck - run: bun test + # dashboard/ carries its own package.json and its own bun.lock, and the + # install above does not reach it. Without this step nothing in CI ever + # installs or builds that tree, so a drifted dashboard/bun.lock stayed + # invisible until `npm publish` — which now fails on it, because + # build:dashboard runs --frozen-lockfile. Exercising it on the pull request + # turns a late release failure into an ordinary red check on the PR that + # caused it. This is the same command the release workflow runs. + - run: bun run build:dashboard + # The macOS shell in Sources/ is 500+ lines of Swift the job above cannot see: # it runs on ubuntu and never invokes `swift`, so its green tick was green # regardless of what Sources/ contained — up to and including code that does not diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0791565..3a6f3ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,6 +139,49 @@ jobs: - name: Build run: bun run build + # dashboard/ is a SECOND dependency tree with its own lockfile, and no gate + # above touches it: ci.yml installs the root only, and `bun run build` does + # not enter dashboard/. Until this step existed, that tree's first and only + # install happened inside `npm publish` below, via prepublishOnly — after + # Typecheck, Test and Build had all gone green, so the reviewed thing and + # the published thing were separated by a dependency resolution that no + # gate could see. + # + # It ran unprotected in two distinct ways, both measured on bun 1.3.14: + # * unpinned — plain `bun install` on a drifted lockfile exits 0, silently + # re-resolves, and rewrites bun.lock ("Saved lockfile"). With + # --frozen-lockfile the same state exits 1. + # * unquarantined — the release-age quarantine on a workstation comes + # ENTIRELY from ~/.bunfig.toml, which does not exist on this runner. + # The identical install of a 5-day-old package exits 1 with a real HOME + # and exits 0 with an empty one. That is precisely why the root install + # above passes --minimum-release-age explicitly rather than relying on + # the environment, and the dashboard install needs the same treatment. + # + # Running it here, before the publish boundary, means the tree that ships is + # resolved inside the gated part of the workflow and is visible in these logs. + # prepublishOnly still rebuilds it, now under the same flags, so any other + # publish path is protected too. + # + # Note what each flag actually buys, because they are not interchangeable: + # --frozen-lockfile is load-bearing — it removes resolution from publish + # entirely. --minimum-release-age is defence in depth only; it is enforced + # at RESOLUTION time and does NOT re-validate versions already pinned in the + # lockfile (measured: a frozen install of a lock pinning a 5-day-old version + # exits 0). A too-new pin is caught by review of the lockfile diff, not here. + - name: Build dashboard with locked, quarantined dependencies + run: bun run build:dashboard + + # `files` in package.json ships dashboard/dist/. npm silently OMITS a listed + # path that does not exist rather than failing, so a dashboard that never + # built would publish as a tarball quietly missing its web UI — an artefact + # defect with no error anywhere upstream. Turn that silence into a failure. + - name: Require the dashboard build output + run: | + set -euo pipefail + test -f dashboard/dist/index.html + echo "dashboard/dist/index.html present" + # No NODE_AUTH_TOKEN, and no token of any kind. npm detects the Actions # OIDC environment and exchanges the id-token for a short-lived, # publish-scoped credential. --provenance is passed explicitly: npm diff --git a/package.json b/package.json index 9c2638b..ebe0164 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "scripts": { "clean": "rm -rf dist bin", "build": "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/serve-entry.ts --outfile ./bin/serve.js --target bun && bun build ./src/hooks/blocker-hook.ts --outfile ./bin/hook.js --target bun && bun build ./src/index.ts ./src/sdk/index.ts --outdir ./dist --target bun && (tsc --emitDeclarationOnly --declaration --outDir dist || true)", - "build:dashboard": "cd dashboard && bun install && bun run build", + "build:dashboard": "cd dashboard && bun install --frozen-lockfile --minimum-release-age 604800 && bun run build", "test": "bun test", "dev": "bun run ./src/cli/index.tsx", "serve": "bun run ./src/server/serve-entry.ts",