diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index ca07aa3dbd..e0ac9a8c0c 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -56,9 +56,9 @@ runs: # e.g. ./patches/ for standard-openapi # https://github.com/oven-sh/bun/issues/28147 if [ "$RUNNER_OS" = "Windows" ]; then - bun install --linker hoisted ${{ inputs.install-flags }} + bun install --frozen-lockfile --linker hoisted ${{ inputs.install-flags }} else - bun install ${{ inputs.install-flags }} + bun install --frozen-lockfile ${{ inputs.install-flags }} fi shell: bash diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml index 03f58b43de..c6ad8e31f5 100644 --- a/.github/workflows/release-fork.yml +++ b/.github/workflows/release-fork.yml @@ -116,7 +116,7 @@ jobs: save-cache: false - name: Install Runtime Dependencies - run: bun install + run: bun install --frozen-lockfile - name: Validate and Package Templates (fail closed) working-directory: packages/opencode diff --git a/bun.lock b/bun.lock index 2a7e67fa31..2c045542f4 100644 --- a/bun.lock +++ b/bun.lock @@ -59,7 +59,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#main", + "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", @@ -3832,7 +3832,7 @@ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#513463a", {}, "anomalyco-ghostty-web-513463a", "sha512-GZR8LSmgGzViWnBJrqRI8MpAZRCJxhcr1Hi9Tyeh7YRooHZQjK9J97FQRD3tbBaM2wjq05gzGY2UEsG+JtZeBw=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], diff --git a/packages/app/package.json b/packages/app/package.json index 53c01ea64e..a4773e4da2 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -72,7 +72,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#main", + "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", diff --git a/packages/opencode/test/policy/repo-dependencies.test.ts b/packages/opencode/test/policy/repo-dependencies.test.ts new file mode 100644 index 0000000000..2d26e38235 --- /dev/null +++ b/packages/opencode/test/policy/repo-dependencies.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "bun:test" +import path from "node:path" + +// CI-LOCK-02 gate — the lockfile is authoritative. This test STATICALLY validates repository +// configuration only; it performs NO install (no second install flow). It fails closed when: +// 1. any git/github dependency is pinned to a mutable ref (branch/tag) instead of a full 40-char +// commit SHA; or +// 2. any `bun install` in CI (`.github/**/*.yml`) is not frozen. +// Mutations this gate must catch (turn Red): change a full SHA back to `#main`; drop +// `--frozen-lockfile` from any CI install command. + +async function findRepoRoot(): Promise { + let dir = import.meta.dir + for (let i = 0; i < 10; i++) { + if (await Bun.file(path.join(dir, ".github", "actions", "setup-bun", "action.yml")).exists()) return dir + dir = path.dirname(dir) + } + throw new Error("repo-policy gate: could not locate repo root (setup-bun action.yml not found walking up)") +} + +const GIT_SPEC = /^(github:|git\+|git:|https:\/\/[^\s"]+\.git)/i +const FULL_SHA = /^[0-9a-f]{40}$/i +const DEP_FIELDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const + +describe("repository dependency + install policy (CI-LOCK-02)", () => { + it("every git dependency is pinned to a full immutable 40-char commit SHA", async () => { + const root = await findRepoRoot() + const manifests = await Array.fromAsync( + new Bun.Glob("**/package.json").scan({ cwd: root, onlyFiles: true, dot: false }), + ).then((files) => files.filter((rel) => !rel.includes("node_modules") && !rel.includes(".turbo") && !rel.includes("dist"))) + const offenders: string[] = [] + for (const rel of manifests) { + const pkg = (await Bun.file(path.join(root, rel)).json().catch(() => null)) as + | Record + | null + if (!pkg || typeof pkg !== "object") continue + for (const field of DEP_FIELDS) { + const deps = pkg[field] as Record | undefined + if (!deps || typeof deps !== "object") continue + for (const [name, raw] of Object.entries(deps)) { + if (typeof raw !== "string" || !GIT_SPEC.test(raw)) continue + const ref = raw.split("#")[1] + if (!ref || !FULL_SHA.test(ref)) + offenders.push(`${rel} :: ${field}.${name} = "${raw}" — git deps must use #, not a branch/tag`) + } + } + } + expect(offenders, offenders.join("\n")).toEqual([]) + }) + + it("every bun install in CI (.github/**/*.yml) is frozen", async () => { + const root = await findRepoRoot() + const ymls = await Array.fromAsync( + // `.github` is dot-prefixed, so `dot: true` is required to descend into it. + new Bun.Glob(".github/**/*.yml").scan({ cwd: root, onlyFiles: true, dot: true }), + ) + expect(ymls.length, "expected .github yml files").toBeGreaterThan(0) + let installCount = 0 + const offenders: string[] = [] + for (const rel of ymls) { + const text = await Bun.file(path.join(root, rel)).text() + for (const raw of text.split("\n")) { + const trimmed = raw.trim() + if (trimmed.startsWith("#")) continue // comment line, not an invocation + // Strip a leading `run:` key so both inline (`run: bun install ...`) and block + // (`run: |` + bare `bun install` on the next line) forms reduce to the command. + const cmd = trimmed.replace(/^run:\s*/, "").trim() + if (!/^bun\s+install\b/.test(cmd)) continue + installCount++ + if (!/--frozen-lockfile/.test(cmd)) offenders.push(`${rel}: "${cmd}"`) + } + } + expect(installCount, "expected at least one `bun install` under .github").toBeGreaterThan(0) + expect(offenders, `unfrozen CI installs:\n${offenders.join("\n")}`).toEqual([]) + }) +})