From b1bee7e7e4bcb53a2579b93dabf21f5b96e55476 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 31 Oct 2026 01:29:45 -0700 Subject: [PATCH] ci: fail the checks on any dependency not pinned to an exact version --- .github/workflows/ci.yml | 3 +- package.json | 3 +- scripts/verify-pins.ts | 82 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 scripts/verify-pins.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d19cd2f6..bbb1ddf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,8 @@ jobs: strategy: fail-fast: false matrix: - script: [format:check, lint, typecheck, test, vendor:verify] + script: + [format:check, lint, typecheck, test, vendor:verify, pins:verify] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/package.json b/package.json index c6a06145..cc054002 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "docker:sidecar": "docker build -f deploy/Dockerfile --target sidecar -t corbits-builder-sidecar:local .", "auth:bootstrap": "bun --env-file=.env.hub --conditions=intx-src apps/hub/src/setup.ts", "auth:emulate": "bun --env-file=.env.hub scripts/google-emulate.ts", - "check": "bun run lint && bun run format:check && bun run typecheck && bun run test", + "check": "bun run lint && bun run format:check && bun run typecheck && bun run test && bun run pins:verify", "lint": "bun --bun run oxlint --deny-warnings", "format": "bun fmt --no-script", "format:check": "bun fmt --no-script --check", @@ -36,6 +36,7 @@ "test": "bun --conditions=intx-src test ./apps/hub ./apps/web/src ./packages ./workflows ./tests/integration", "test:e2e": "bun --env-file=.env.hub --conditions=intx-src test ./tests/e2e", "vendor:verify": "bun scripts/verify-vendor.ts", + "pins:verify": "bun scripts/verify-pins.ts", "skills:react-ui": "bun scripts/react-ui-api.ts" }, "dependencies": { diff --git a/scripts/verify-pins.ts b/scripts/verify-pins.ts new file mode 100644 index 00000000..1664e3e2 --- /dev/null +++ b/scripts/verify-pins.ts @@ -0,0 +1,82 @@ +import { $ } from "bun"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +type Specs = { [name: string]: string | Specs }; + +const dependencyFields = [ + "dependencies", + "devDependencies", + "peerDependencies", + "optionalDependencies", + "catalog", + "catalogs", +]; + +const exactVersion = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/; +const npmAlias = /^npm:(@[\w.-]+\/)?[\w.-]+@\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/; +const workspaceReference = /^workspace:/; +const catalogReference = /^catalog:[\w.-]*$/; +const gitPin = + /^(git\+\w+:\/\/\S*|git:\/\/\S*|git@\S*|github:\S*|https?:\/\/\S*\.git|[\w.-]+\/[\w.-]+)#[0-9a-f]{40}$/; +const overrideReference = /^\$[\w@/.-]+$/; + +function isPinned(spec: string) { + return ( + exactVersion.test(spec) || + npmAlias.test(spec) || + workspaceReference.test(spec) || + catalogReference.test(spec) || + gitPin.test(spec) + ); +} + +function isOverridePinned(spec: string) { + return overrideReference.test(spec) || isPinned(spec); +} + +function offenders( + specs: Specs | undefined, + allowed: (spec: string) => boolean, + parent?: string, +): string[] { + if (specs === undefined) return []; + return Object.entries(specs).flatMap(([name, spec]) => { + const label = parent ? `${parent}>${name}` : name; + if (typeof spec !== "string") return offenders(spec, allowed, label); + return allowed(spec) ? [] : [`${label}@${spec}`]; + }); +} + +export async function verifyPins(root = resolve(import.meta.dir, "..")) { + const listed = + await $`git ls-files -- package.json '**/package.json' ':!vendor'` + .cwd(root) + .text(); + const files = listed.split("\n").filter((file) => file !== ""); + if (files.length === 0) throw new Error("No package.json files are tracked"); + const errors: string[] = []; + for (const file of files) { + const manifest = JSON.parse(await readFile(resolve(root, file), "utf8")); + const found = [ + ...dependencyFields.flatMap((field) => + offenders(manifest[field], isPinned), + ), + ...offenders(manifest.workspaces?.catalog, isPinned), + ...offenders(manifest.workspaces?.catalogs, isPinned), + ...offenders(manifest.overrides, isOverridePinned), + ...offenders(manifest.resolutions, isOverridePinned), + ]; + errors.push(...found.map((offender) => `${file}: ${offender}`)); + } + return { files, errors }; +} + +if (import.meta.main) { + const { files, errors } = await verifyPins(); + if (errors.length) { + console.error(errors.join("\n")); + process.exit(1); + } + console.log(`Verified exact pins in ${files.length} package.json files`); +}