From ca92f66a3dd439e43e8461b95b459c547e702d26 Mon Sep 17 00:00:00 2001 From: PoterPan Date: Tue, 9 Jun 2026 00:38:13 +0800 Subject: [PATCH 1/2] test: scrub external secrets from the test baseline so local == CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A local .dev.vars (gitignored) injects a REAL DISCORD_BOT_TOKEN; CI has none. Code gated on the token (e.g. billing/initiate -> sendBillingOpened) therefore hit Discord for real on a dev machine while CI took the silent no-send branch — tests behaved differently per machine, and the suite made an unintended external POST locally. Delete DISCORD_BOT_TOKEN in the test setup so the baseline is identical everywhere; tests that exercise outbound calls already set their own dummy token + stub fetch. Add a smoke assertion that the token does not leak (reproduces the divergence locally: red before the scrub, green after). --- packages/worker/test/apply-migrations.ts | 6 ++++++ packages/worker/test/smoke.test.ts | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/worker/test/apply-migrations.ts b/packages/worker/test/apply-migrations.ts index 9a9aedd..d092040 100644 --- a/packages/worker/test/apply-migrations.ts +++ b/packages/worker/test/apply-migrations.ts @@ -1,3 +1,9 @@ import { applyD1Migrations, env } from "cloudflare:test"; +// Scrub external secrets that a local `.dev.vars` would otherwise inject (CI has none), so the +// test baseline is identical on every machine. Tests that exercise outbound calls set their own +// dummy token + stub fetch; leaving a real token here would make those paths hit Discord for real +// locally while CI takes the no-send branch. Keep in sync with outbound-gated secrets. +delete (env as { DISCORD_BOT_TOKEN?: string }).DISCORD_BOT_TOKEN; + await applyD1Migrations(env.DB, env.TEST_MIGRATIONS); diff --git a/packages/worker/test/smoke.test.ts b/packages/worker/test/smoke.test.ts index 701369e..02f6cc0 100644 --- a/packages/worker/test/smoke.test.ts +++ b/packages/worker/test/smoke.test.ts @@ -11,4 +11,13 @@ describe("test harness", () => { expect(env.DB).toBeDefined(); expect(env.BUCKET).toBeDefined(); }); + + // A local `.dev.vars` (gitignored) supplies a REAL DISCORD_BOT_TOKEN, but CI has none. + // If that token leaked into the test baseline, any code path gated on it (e.g. + // billing/initiate → sendBillingOpened) would make a REAL Discord fetch locally while + // CI silently takes the no-send branch — tests would behave differently per machine. + // The setup file scrubs it; tests that exercise sending set their own dummy token. + it("does not leak external secrets from .dev.vars into the test baseline", () => { + expect(env.DISCORD_BOT_TOKEN).toBeUndefined(); + }); }); From baada7ef01855e9a85b1695a09a9a5d7036d4ce6 Mon Sep 17 00:00:00 2001 From: PoterPan Date: Tue, 9 Jun 2026 00:38:13 +0800 Subject: [PATCH 2/2] build(web): fail the build when VITE_API_BASE is unset (don't ship a white-screen bundle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VITE_API_BASE is statically inlined at build time and points the upload page at the fork's own worker. When unset, `vite build` still succeeded and emitted a bundle whose top-level throw fires at load (white screen) — invisible to CI's `pnpm -r build`. Move the gate to build time in vite.config.ts (dev exempt). CI: pass a throwaway VITE_API_BASE to the build step (it only checks the build compiles) and add a regression guard asserting the web build refuses to build without it. --- .github/workflows/ci.yml | 14 ++++++++++++++ packages/web/vite.config.ts | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cc0254..436766b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,4 +34,18 @@ jobs: # so it passes here without a .dev.vars (which is gitignored and never in CI). - run: pnpm -r test + # The web build hard-fails without VITE_API_BASE (see packages/web/vite.config.ts). + # Supply a throwaway value — CI only verifies the build compiles; admin/worker ignore it. - run: pnpm -r build + env: + VITE_API_BASE: https://example.invalid + + # Regression guard: the web build MUST refuse to build without VITE_API_BASE, so a fork + # can never ship an upload page that white-screens at load (no VITE_API_BASE in this step). + - name: web build must fail without VITE_API_BASE + run: | + if pnpm --filter @chippot/web build; then + echo "::error::web build succeeded without VITE_API_BASE — the fail-fast guard is broken" + exit 1 + fi + echo "OK: web build correctly refused to build without VITE_API_BASE" diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 081c8d9..ea916f8 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -1,6 +1,16 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; -export default defineConfig({ - plugins: [react()], +// VITE_API_BASE is statically inlined at build time and points the upload page at the user's +// own worker. If it's missing, `vite build` still succeeds and emits a bundle that throws at +// load (white screen) — invisible to CI's `pnpm -r build`. Fail the BUILD instead so a fork +// finds out immediately. (dev is exempt: `vite dev` can run against a proxy/placeholder.) +export default defineConfig(({ command, mode }) => { + if (command === "build" && !loadEnv(mode, process.cwd(), "VITE_").VITE_API_BASE) { + throw new Error( + "VITE_API_BASE is required for the web build. Point it at your worker, e.g.\n" + + " VITE_API_BASE=https://chippot..workers.dev pnpm --filter @chippot/web build" + ); + } + return { plugins: [react()] }; });