From ceb2dfed15a0e10c39a5ef1e501d878b06b6a00e Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 2 Jul 2026 05:10:47 -0700 Subject: [PATCH] feat(ci): add a cf-typegen staleness check for worker-configuration.d.ts worker-configuration.d.ts is generated from wrangler.jsonc bindings via npm run cf-typegen, but unlike its sibling generated artifacts (openapi.json, migrations) there was no CI guard against it drifting. Two PRs that each independently add a wrangler binding can both pass CI in isolation, then merge sequentially and leave a stale committed worker-configuration.d.ts with zero prior gate signal. Added cf-typegen:check (wrangler types --check, which wrangler already supports natively -- no custom diffing script needed) and wired it into test:ci right after the migrations check. wrangler.jsonc and worker-configuration.d.ts are now backend-filter paths in ci.yml so the validate-code job's new drift-check step actually runs when either changes. Documented the new check in the contributing skill's reference table alongside the other generated-artifact guards. Closes #2557 --- .../contributing-to-gittensory/reference.md | 1 + .github/workflows/ci.yml | 8 +++ package.json | 3 +- test/unit/ci-cf-typegen-check.test.ts | 70 +++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 test/unit/ci-cf-typegen-check.test.ts diff --git a/.claude/skills/contributing-to-gittensory/reference.md b/.claude/skills/contributing-to-gittensory/reference.md index 3faa86e77b..f9d2cd89ae 100644 --- a/.claude/skills/contributing-to-gittensory/reference.md +++ b/.claude/skills/contributing-to-gittensory/reference.md @@ -26,6 +26,7 @@ path filter matched; on push to `main`, everything runs. | changes | `git diff --check` + path filter | `git diff --check` | trailing whitespace / conflict markers | | lint → actionlint | workflow lint | `npm run actionlint` | any `.github/workflows/*.yml` violation | | lint → migrations | migration guard | `npm run db:migrations:check` | duplicate/gap/misnamed migration number | +| lint → cf-typegen | worker types drift | `npm run cf-typegen:check` | committed `worker-configuration.d.ts` is stale (run `npm run cf-typegen`) | | lint → typecheck | `tsc --noEmit` | `npm run typecheck` | any backend type error | | test (1/2) | sharded vitest + coverage | `npm run test:coverage` (unsharded) | any failing `test/**/*.test.ts` (excl. `test/workers/**`) | | workers | workers-pool vitest | `npm run test:workers` | any failing `test/workers/**` | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd48c6cebc..28259d39a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,8 @@ jobs: - 'scripts/**' - 'migrations/**' - '.github/workflows/**' + - 'wrangler.jsonc' + - 'worker-configuration.d.ts' # The UI's own app/extension code -- triggers the FULL toolchain (lint/typecheck/test/build). # A dependency bump (package.json/package-lock.json) stays here too since it can break the UI # build or types in ways only that full toolchain would catch. @@ -190,6 +192,12 @@ jobs: - name: Check migrations if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} run: npm run db:migrations:check + # Guards against the same staleness-drift class as db:migrations:check/ui:openapi:check: two PRs that + # each independently add a wrangler.jsonc binding can both pass CI in isolation, then merge sequentially + # and leave a stale committed worker-configuration.d.ts with zero prior gate signal (#2557). + - name: cf-typegen drift check + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} + run: npm run cf-typegen:check - name: Typecheck if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} run: npm run typecheck diff --git a/package.json b/package.json index 079d6afea3..3d0775e85d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "deploy:api": "wrangler d1 migrations apply gittensory --remote && wrangler deploy", "selfhost:postgres:migrate": "tsx scripts/migrate-selfhost-sqlite-to-postgres.ts", "cf-typegen": "wrangler types && perl -pi -e 's/[[:blank:]]+$//' worker-configuration.d.ts", + "cf-typegen:check": "wrangler types --check", "db:migrate:local": "wrangler d1 migrations apply gittensory --local", "db:migrate:remote": "wrangler d1 migrations apply gittensory --remote", "drizzle:generate": "drizzle-kit generate", @@ -62,7 +63,7 @@ "test:smoke:observability": "node scripts/smoke-observability-traces.mjs", "test:smoke:browser:install": "playwright install chromium", "test:smoke:browser": "node scripts/smoke-ui-browser.mjs", - "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:version-audit && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", + "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:version-audit && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", "test:release": "npm run test:ci && npm run changelog:check", "test:release:mcp": "npm run test:ci && npm run changelog:check:mcp", "test:watch": "vitest", diff --git a/test/unit/ci-cf-typegen-check.test.ts b/test/unit/ci-cf-typegen-check.test.ts new file mode 100644 index 0000000000..7d22c85189 --- /dev/null +++ b/test/unit/ci-cf-typegen-check.test.ts @@ -0,0 +1,70 @@ +import { readFileSync } from "node:fs"; +import { parse } from "yaml"; +import { describe, expect, it } from "vitest"; + +function readYaml(path: string): Record { + return record(parse(readFileSync(path, "utf8")), path); +} + +function record(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } + return value as Record; +} + +function recordArray(value: unknown, label: string): Array> { + if (!Array.isArray(value)) throw new Error(`${label} must be an array`); + return value.map((entry, index) => record(entry, `${label}[${index}]`)); +} + +function nestedRecord(source: Record, path: string[]): Record { + return path.reduce((current, key) => record(current[key], path.join(".")), source); +} + +// #2557: worker-configuration.d.ts is generated from wrangler.jsonc via `npm run cf-typegen`, but unlike its +// sibling generated artifacts (openapi.json, migrations) it had no CI drift guard -- two independently-valid +// binding additions could merge sequentially and leave the committed types silently stale. +describe("cf-typegen staleness guard (#2557)", () => { + it("package.json defines cf-typegen:check and wires it into test:ci", () => { + const pkg = record(JSON.parse(readFileSync("package.json", "utf8")), "package.json"); + const scripts = record(pkg.scripts, "package.json.scripts"); + + expect(scripts["cf-typegen:check"]).toBe("wrangler types --check"); + expect(String(scripts["test:ci"])).toContain("npm run cf-typegen:check"); + // Must run before typecheck, mirroring db:migrations:check's position -- a drift-check failure should + // surface before the more expensive type/test/build steps run. + const ciChain = String(scripts["test:ci"]); + expect(ciChain.indexOf("cf-typegen:check")).toBeLessThan(ciChain.indexOf("npm run typecheck")); + }); + + it("ci.yml's changes job treats wrangler.jsonc and worker-configuration.d.ts as backend paths", () => { + const workflow = readYaml(".github/workflows/ci.yml"); + const changesJob = nestedRecord(workflow, ["jobs", "changes"]); + const steps = recordArray(changesJob.steps, "jobs.changes.steps"); + const filterStep = steps.find((step) => step.id === "filter"); + expect(filterStep).toBeDefined(); + const withBlock = record(filterStep!.with, "filter.with"); + const filters = String(withBlock.filters); + const backendBlock = filters.slice(filters.indexOf("backend:"), filters.indexOf("ui:")); + + expect(backendBlock).toContain("wrangler.jsonc"); + expect(backendBlock).toContain("worker-configuration.d.ts"); + }); + + it("ci.yml's validate-code job runs the drift check gated the same as the migrations check", () => { + const workflow = readYaml(".github/workflows/ci.yml"); + const validateCode = nestedRecord(workflow, ["jobs", "validate-code"]); + const steps = recordArray(validateCode.steps, "jobs.validate-code.steps"); + + const migrationsIndex = steps.findIndex((step) => step.name === "Check migrations"); + const cfTypegenIndex = steps.findIndex((step) => step.name === "cf-typegen drift check"); + expect(migrationsIndex).toBeGreaterThan(-1); + expect(cfTypegenIndex).toBeGreaterThan(migrationsIndex); + + const migrationsStep = steps[migrationsIndex]!; + const cfTypegenStep = steps[cfTypegenIndex]!; + expect(String(cfTypegenStep.if)).toBe(String(migrationsStep.if)); + expect(String(cfTypegenStep.run)).toBe("npm run cf-typegen:check"); + }); +});