diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml new file mode 100644 index 00000000..ddf2a7dc --- /dev/null +++ b/.github/workflows/ci-checks.yml @@ -0,0 +1,45 @@ +name: CI Checks + +# Lightweight, portable PR gate: a TypeScript typecheck via the repo's own +# `typecheck` turbo tasks (tsc --noEmit). +# +# It intentionally does NOT use `vp check` / `vp test`: those load +# vite.config.ts, which imports the `vite-plus` package. `vite-plus` is +# provisioned by the local `vp` toolchain but is not a declared dependency and +# is not resolvable in a clean CI checkout, so any vite-config-loading command +# fails there. This gate is also separate from the heavy Nix `om ci` workflow. +# +# `@stackpanel/sdk` is excluded because its typecheck has a pre-existing failure +# (its tsconfig does not pull in node types) unrelated to this gate. Remove the +# `--filter` exclusion once that's fixed so the gate covers every package that +# defines a `typecheck` script. +on: + pull_request: + push: + branches: [main] + +# Least-privilege GITHUB_TOKEN (CodeQL: workflows should set explicit permissions). +permissions: + contents: read + +concurrency: + group: ci-checks-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + typecheck: + name: typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.2" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck (tsc --noEmit) + run: bunx turbo run typecheck --filter='!@stackpanel/sdk' diff --git a/nix/stackpanel/lib/containers.nix b/nix/stackpanel/lib/containers.nix index d1c4ffc1..83ecb19a 100644 --- a/nix/stackpanel/lib/containers.nix +++ b/nix/stackpanel/lib/containers.nix @@ -94,7 +94,13 @@ let else let spec = defaultBaseImageSpecs.${type} or defaultBaseImageSpecs.alpine; + # Refuse to build from an unpinned base image: the unpinned specs carry + # lib.fakeSha256, which would otherwise fail later as a cryptic hash + # mismatch. Pin with `nix-prefetch-docker` (set imageDigest + sha256). + isPinned = (spec.sha256 or lib.fakeSha256) != lib.fakeSha256; in + assert lib.assertMsg isPinned + "stackpanel: base image '${spec.imageName}' is not pinned (still lib.fakeSha256); pin it with nix-prefetch-docker before building a container from it."; nix2containerLib.pullImage spec; # --------------------------------------------------------------------------- diff --git a/packages/auth/package.json b/packages/auth/package.json index 28659f6c..6708ee00 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -9,7 +9,9 @@ } }, "type": "module", - "scripts": {}, + "scripts": { + "typecheck": "tsc --noEmit" + }, "devDependencies": { "@stackpanel/config": "workspace:*" }, diff --git a/packages/auth/src/lib/polar-products.ts b/packages/auth/src/lib/polar-products.ts index 72936c12..5f4119d4 100644 --- a/packages/auth/src/lib/polar-products.ts +++ b/packages/auth/src/lib/polar-products.ts @@ -26,13 +26,25 @@ const SANDBOX_FREE = "6abf1427-cf83-4251-90d7-8852e7e5ca21"; */ export function polarProducts( env: DeployEnv = resolveDeployEnv(), + { strict = true }: { strict?: boolean } = {}, ): Record { switch (env) { - case "production": - return { - pro: process.env.POLAR_PRO_PRODUCT_ID_PRODUCTION ?? SANDBOX_PRO, - free: process.env.POLAR_FREE_PRODUCT_ID_PRODUCTION ?? SANDBOX_FREE, - }; + case "production": { + const pro = process.env.POLAR_PRO_PRODUCT_ID_PRODUCTION; + const free = process.env.POLAR_FREE_PRODUCT_ID_PRODUCTION; + if (pro && free) return { pro, free }; + if (strict) { + throw new Error( + "Polar production product IDs are not configured. Set " + + "POLAR_PRO_PRODUCT_ID_PRODUCTION and POLAR_FREE_PRODUCT_ID_PRODUCTION. " + + "Refusing to silently fall back to sandbox products in production — " + + "that would serve sandbox checkouts to real users and never charge real cards.", + ); + } + // Non-strict callers (e.g. the webhook inverse-lookup, which may run + // outside a production runtime) only need id->plan resolution. + return { pro: pro ?? SANDBOX_PRO, free: free ?? SANDBOX_FREE }; + } case "preview": case "dev": default: @@ -65,7 +77,7 @@ export function planForProduct(productId: string | null | undefined): PlanId | " if (!productId) return "unknown"; const envs: DeployEnv[] = ["production", "preview", "dev"]; for (const env of envs) { - const products = polarProducts(env); + const products = polarProducts(env, { strict: false }); for (const [plan, id] of Object.entries(products) as [PlanId, string][]) { if (id === productId) return plan; }