From 12992fa2fade9dd54eb87dae04bfb8c817d5594b Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Fri, 23 Oct 2026 20:08:36 -0400 Subject: [PATCH 1/3] ci: pin Bun 1.4.4 / Node 24.22.1 for CVE-2026-111497 (Brotli) --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b5b05b..608fa36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies run: bun install --frozen-lockfile @@ -48,7 +48,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies run: bun install --frozen-lockfile @@ -67,7 +67,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies run: bun install --frozen-lockfile - name: Verify native Windows provenance diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e7ab61..d283c57 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,13 +31,13 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "24.11.0" + node-version: "24.22.1" registry-url: "https://registry.npmjs.org" - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies run: bun install --frozen-lockfile @@ -129,7 +129,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies run: bun install --frozen-lockfile @@ -275,7 +275,7 @@ jobs: if: steps.tap-token.outputs.enabled == 'true' uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies if: steps.tap-token.outputs.enabled == 'true' @@ -366,13 +366,13 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "24.11.0" + node-version: "24.22.1" registry-url: "https://registry.npmjs.org" - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: "1.3.9" + bun-version: "1.4.4" - name: Install dependencies run: bun install --frozen-lockfile From 555e19f3daf499e8c9d7192192ccdbbd2de57361 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Fri, 23 Oct 2026 20:25:01 -0400 Subject: [PATCH 2/3] fix: restore absent test environment variables on Bun 1.4.4 --- scripts/release-workflow.test.ts | 37 +++++++ src/audit/agent.test.ts | 176 +++++++++++++++++++++---------- src/cli-context.test.ts | 5 +- src/reconciliation.test.ts | 5 +- src/snippets-cli.test.ts | 5 +- test/environment.ts | 11 ++ 6 files changed, 178 insertions(+), 61 deletions(-) create mode 100644 test/environment.ts diff --git a/scripts/release-workflow.test.ts b/scripts/release-workflow.test.ts index 33cf708..a0990c4 100644 --- a/scripts/release-workflow.test.ts +++ b/scripts/release-workflow.test.ts @@ -74,3 +74,40 @@ it("publishes npm through OIDC without a stored publish token", async () => { ) ).toBe(true); }); + +it("keeps CI and release runtime pins consistent", async () => { + const versions: Record = { bun: [], node: [] }; + for (const name of ["ci", "release"]) { + const workflow: unknown = parseYaml( + await Bun.file( + new URL(`../.github/workflows/${name}.yml`, import.meta.url) + ).text() + ); + if (!(isPlainObject(workflow) && isPlainObject(workflow.jobs))) { + throw new Error("Workflow jobs are missing."); + } + for (const job of Object.values(workflow.jobs)) { + if (!(isPlainObject(job) && Array.isArray(job.steps))) { + continue; + } + for (const step of job.steps.filter(isPlainObject)) { + if (!isPlainObject(step.with)) { + continue; + } + for (const runtime of ["bun", "node"]) { + const version = step.with[`${runtime}-version`]; + if (typeof version === "string") { + versions[runtime]!.push(version); + } + } + } + } + } + for (const [runtime, expected] of [ + ["bun", "1.4.4"], + ["node", "24.22.1"], + ]) { + expect(versions[runtime!]!.length).toBeGreaterThan(0); + expect([...new Set(versions[runtime!]!)]).toEqual([expected]); + } +}); diff --git a/src/audit/agent.test.ts b/src/audit/agent.test.ts index 6088b77..bacdeeb 100644 --- a/src/audit/agent.test.ts +++ b/src/audit/agent.test.ts @@ -13,6 +13,7 @@ import { } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, delimiter, dirname, join } from "node:path"; +import { restoreEnvironmentVariable } from "../../test/environment"; import { evaluateAgentAudit, makeAgentRuntimeDir, @@ -758,12 +759,18 @@ for (const tool of ["claude", "codex"] as const) { SERVICE_TOKEN: process.env.SERVICE_TOKEN, }; process.env.PATH = `${bin}${delimiter}${previousPath ?? ""}`; - process.env.ANTHROPIC_API_KEY = - tool === "claude" ? "fixture-environment-auth" : undefined; - process.env.CLAUDE_CODE_OAUTH_TOKEN = - tool === "claude" ? "fixture-environment-auth" : undefined; - process.env.OPENAI_API_KEY = - tool === "codex" ? "fixture-environment-auth" : undefined; + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + tool === "claude" ? "fixture-environment-auth" : undefined + ); + restoreEnvironmentVariable( + "CLAUDE_CODE_OAUTH_TOKEN", + tool === "claude" ? "fixture-environment-auth" : undefined + ); + restoreEnvironmentVariable( + "OPENAI_API_KEY", + tool === "codex" ? "fixture-environment-auth" : undefined + ); process.env.ALL_PROXY = "http://fixture.invalid"; process.env.AWS_SECRET_ACCESS_KEY = "fixture-unrelated-secret"; process.env.BASH_ENV = join(dir, "fixture-hook"); @@ -849,10 +856,10 @@ for (const tool of ["claude", "codex"] as const) { const profileBackup = `${authMarkers[tool]}.disabled`; await rename(authMarkers[tool], profileBackup); if (tool === "claude") { - process.env.ANTHROPIC_API_KEY = undefined; - process.env.CLAUDE_CODE_OAUTH_TOKEN = undefined; + restoreEnvironmentVariable("ANTHROPIC_API_KEY", undefined); + restoreEnvironmentVariable("CLAUDE_CODE_OAUTH_TOKEN", undefined); } else { - process.env.OPENAI_API_KEY = undefined; + restoreEnvironmentVariable("OPENAI_API_KEY", undefined); } const nativeReport = await runAgentAudit({ argv: ["ok-skill", "--with", tool, "--max-items", "1"], @@ -875,10 +882,10 @@ for (const tool of ["claude", "codex"] as const) { if (phase === "profile-precondition") { if (tool === "claude") { - process.env.ANTHROPIC_API_KEY = undefined; - process.env.CLAUDE_CODE_OAUTH_TOKEN = undefined; + restoreEnvironmentVariable("ANTHROPIC_API_KEY", undefined); + restoreEnvironmentVariable("CLAUDE_CODE_OAUTH_TOKEN", undefined); } else { - process.env.OPENAI_API_KEY = undefined; + restoreEnvironmentVariable("OPENAI_API_KEY", undefined); } await expect( runAgentAudit({ @@ -958,22 +965,51 @@ for (const tool of ["claude", "codex"] as const) { expect(await readdir(scratch)).toEqual([]); } } finally { - process.env.PATH = previousPath; - process.env.ANTHROPIC_API_KEY = previousAnthropicApiKey; - process.env.CLAUDE_CODE_OAUTH_TOKEN = previousClaudeOauthToken; - process.env.OPENAI_API_KEY = previousOpenAiApiKey; - process.env.ALL_PROXY = unrelatedEnvironment.ALL_PROXY; - process.env.AWS_SECRET_ACCESS_KEY = - unrelatedEnvironment.AWS_SECRET_ACCESS_KEY; - process.env.BASH_ENV = unrelatedEnvironment.BASH_ENV; - process.env.DATABASE_URL = unrelatedEnvironment.DATABASE_URL; - process.env.GH_TOKEN = unrelatedEnvironment.GH_TOKEN; - process.env.GIT_CONFIG_COUNT = unrelatedEnvironment.GIT_CONFIG_COUNT; - process.env.GIT_SSH_COMMAND = unrelatedEnvironment.GIT_SSH_COMMAND; - process.env.HTTP_PROXY = unrelatedEnvironment.HTTP_PROXY; - process.env.HTTPS_PROXY = unrelatedEnvironment.HTTPS_PROXY; - process.env.NODE_OPTIONS = unrelatedEnvironment.NODE_OPTIONS; - process.env.SERVICE_TOKEN = unrelatedEnvironment.SERVICE_TOKEN; + restoreEnvironmentVariable("PATH", previousPath); + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + previousAnthropicApiKey + ); + restoreEnvironmentVariable( + "CLAUDE_CODE_OAUTH_TOKEN", + previousClaudeOauthToken + ); + restoreEnvironmentVariable("OPENAI_API_KEY", previousOpenAiApiKey); + restoreEnvironmentVariable("ALL_PROXY", unrelatedEnvironment.ALL_PROXY); + restoreEnvironmentVariable( + "AWS_SECRET_ACCESS_KEY", + unrelatedEnvironment.AWS_SECRET_ACCESS_KEY + ); + restoreEnvironmentVariable("BASH_ENV", unrelatedEnvironment.BASH_ENV); + restoreEnvironmentVariable( + "DATABASE_URL", + unrelatedEnvironment.DATABASE_URL + ); + restoreEnvironmentVariable("GH_TOKEN", unrelatedEnvironment.GH_TOKEN); + restoreEnvironmentVariable( + "GIT_CONFIG_COUNT", + unrelatedEnvironment.GIT_CONFIG_COUNT + ); + restoreEnvironmentVariable( + "GIT_SSH_COMMAND", + unrelatedEnvironment.GIT_SSH_COMMAND + ); + restoreEnvironmentVariable( + "HTTP_PROXY", + unrelatedEnvironment.HTTP_PROXY + ); + restoreEnvironmentVariable( + "HTTPS_PROXY", + unrelatedEnvironment.HTTPS_PROXY + ); + restoreEnvironmentVariable( + "NODE_OPTIONS", + unrelatedEnvironment.NODE_OPTIONS + ); + restoreEnvironmentVariable( + "SERVICE_TOKEN", + unrelatedEnvironment.SERVICE_TOKEN + ); } }); } @@ -1186,8 +1222,8 @@ beforeAll(async () => { let evaluation: Awaited> | undefined; let commandError: unknown; process.env.PATH = `${bin}${delimiter}${previousEnvironment.PATH ?? ""}`; - process.env.ANTHROPIC_API_KEY = undefined; - process.env.CLAUDE_CODE_OAUTH_TOKEN = undefined; + restoreEnvironmentVariable("ANTHROPIC_API_KEY", undefined); + restoreEnvironmentVariable("CLAUDE_CODE_OAUTH_TOKEN", undefined); process.env.OPENAI_API_KEY = "fixture-codex-cold-start-auth"; try { evaluation = await evaluateAgentAudit({ @@ -1202,11 +1238,19 @@ beforeAll(async () => { commandError = error; } finally { clearTimeout(deadline); - process.env.PATH = previousEnvironment.PATH; - process.env.ANTHROPIC_API_KEY = previousEnvironment.ANTHROPIC_API_KEY; - process.env.CLAUDE_CODE_OAUTH_TOKEN = - previousEnvironment.CLAUDE_CODE_OAUTH_TOKEN; - process.env.OPENAI_API_KEY = previousEnvironment.OPENAI_API_KEY; + restoreEnvironmentVariable("PATH", previousEnvironment.PATH); + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + previousEnvironment.ANTHROPIC_API_KEY + ); + restoreEnvironmentVariable( + "CLAUDE_CODE_OAUTH_TOKEN", + previousEnvironment.CLAUDE_CODE_OAUTH_TOKEN + ); + restoreEnvironmentVariable( + "OPENAI_API_KEY", + previousEnvironment.OPENAI_API_KEY + ); } if (commandError) { throw commandError; @@ -1336,22 +1380,34 @@ for (const tool of ["claude", "codex"] as const) { | undefined; try { process.env.PATH = `${bin}${delimiter}${previousEnvironment.PATH ?? ""}`; - process.env.ANTHROPIC_API_KEY = - tool === "claude" ? selectedSecret : undefined; - process.env.CLAUDE_CODE_OAUTH_TOKEN = undefined; - process.env.OPENAI_API_KEY = - tool === "codex" ? selectedSecret : undefined; + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + tool === "claude" ? selectedSecret : undefined + ); + restoreEnvironmentVariable("CLAUDE_CODE_OAUTH_TOKEN", undefined); + restoreEnvironmentVariable( + "OPENAI_API_KEY", + tool === "codex" ? selectedSecret : undefined + ); await Bun.write(modePath, mode); evaluation = await evaluateMode(commandController.signal); } catch (error) { commandError = error; } finally { clearTimeout(commandDeadline); - process.env.PATH = previousEnvironment.PATH; - process.env.ANTHROPIC_API_KEY = previousEnvironment.ANTHROPIC_API_KEY; - process.env.CLAUDE_CODE_OAUTH_TOKEN = - previousEnvironment.CLAUDE_CODE_OAUTH_TOKEN; - process.env.OPENAI_API_KEY = previousEnvironment.OPENAI_API_KEY; + restoreEnvironmentVariable("PATH", previousEnvironment.PATH); + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + previousEnvironment.ANTHROPIC_API_KEY + ); + restoreEnvironmentVariable( + "CLAUDE_CODE_OAUTH_TOKEN", + previousEnvironment.CLAUDE_CODE_OAUTH_TOKEN + ); + restoreEnvironmentVariable( + "OPENAI_API_KEY", + previousEnvironment.OPENAI_API_KEY + ); } expect({ @@ -1449,11 +1505,15 @@ for (const tool of ["claude", "codex"] as const) { const previousOpenAiApiKey = process.env.OPENAI_API_KEY; const previousServiceToken = process.env.SERVICE_TOKEN; process.env.PATH = `${bin}${delimiter}${previousPath ?? ""}`; - process.env.ANTHROPIC_API_KEY = - tool === "claude" ? selectedSecret : undefined; - process.env.CLAUDE_CODE_OAUTH_TOKEN = undefined; - process.env.OPENAI_API_KEY = - tool === "codex" ? selectedSecret : undefined; + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + tool === "claude" ? selectedSecret : undefined + ); + restoreEnvironmentVariable("CLAUDE_CODE_OAUTH_TOKEN", undefined); + restoreEnvironmentVariable( + "OPENAI_API_KEY", + tool === "codex" ? selectedSecret : undefined + ); process.env.SERVICE_TOKEN = unrelatedSecret; const assertNoHostileValue = (value: unknown) => { @@ -1549,11 +1609,17 @@ for (const tool of ["claude", "codex"] as const) { } expect(await readdir(scratch)).toEqual([]); } finally { - process.env.PATH = previousPath; - process.env.ANTHROPIC_API_KEY = previousAnthropicApiKey; - process.env.CLAUDE_CODE_OAUTH_TOKEN = previousClaudeOauthToken; - process.env.OPENAI_API_KEY = previousOpenAiApiKey; - process.env.SERVICE_TOKEN = previousServiceToken; + restoreEnvironmentVariable("PATH", previousPath); + restoreEnvironmentVariable( + "ANTHROPIC_API_KEY", + previousAnthropicApiKey + ); + restoreEnvironmentVariable( + "CLAUDE_CODE_OAUTH_TOKEN", + previousClaudeOauthToken + ); + restoreEnvironmentVariable("OPENAI_API_KEY", previousOpenAiApiKey); + restoreEnvironmentVariable("SERVICE_TOKEN", previousServiceToken); } }, 15_000); } diff --git a/src/cli-context.test.ts b/src/cli-context.test.ts index 8a9732b..83d99d0 100644 --- a/src/cli-context.test.ts +++ b/src/cli-context.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test"; import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { restoreEnvironmentVariable } from "../test/environment"; import { parseCliContextArgs, resolveCliContextRoot, @@ -21,8 +22,8 @@ afterEach(async () => { await rm(tempRoot, { recursive: true, force: true }); } tempRoot = null; - process.env.FACULT_ROOT_DIR = ORIGINAL_ROOT; - process.env.FACULT_ROOT_SCOPE = ORIGINAL_ROOT_SCOPE; + restoreEnvironmentVariable("FACULT_ROOT_DIR", ORIGINAL_ROOT); + restoreEnvironmentVariable("FACULT_ROOT_SCOPE", ORIGINAL_ROOT_SCOPE); }); describe("parseCliContextArgs", () => { diff --git a/src/reconciliation.test.ts b/src/reconciliation.test.ts index 3ba4eef..82372c5 100644 --- a/src/reconciliation.test.ts +++ b/src/reconciliation.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test"; import { chmod, mkdir, readdir, readFile, rm, utimes } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, dirname, join } from "node:path"; +import { restoreEnvironmentVariable } from "../test/environment"; import { facultAiReconciliationLockPath, facultAiReconciliationReviewDir, @@ -171,8 +172,8 @@ async function fixtureGitOutput(args: { } afterEach(async () => { - process.env.FACULT_ROOT_DIR = originalRootDir; - process.env.FACULT_ROOT_SCOPE = originalRootScope; + restoreEnvironmentVariable("FACULT_ROOT_DIR", originalRootDir); + restoreEnvironmentVariable("FACULT_ROOT_SCOPE", originalRootScope); if (tempRoot) { await rm(tempRoot, { recursive: true, force: true }); } diff --git a/src/snippets-cli.test.ts b/src/snippets-cli.test.ts index 6735922..a8028b6 100644 --- a/src/snippets-cli.test.ts +++ b/src/snippets-cli.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test"; import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { restoreEnvironmentVariable } from "../test/environment"; import { snippetsCommand } from "./snippets-cli"; let tempDir: string | null = null; @@ -41,12 +42,12 @@ async function captureConsole(fn: () => Promise) { afterEach(async () => { process.chdir(originalCwd); if (originalRootDir === undefined) { - process.env.FACULT_ROOT_DIR = undefined; + restoreEnvironmentVariable("FACULT_ROOT_DIR", undefined); } else { process.env.FACULT_ROOT_DIR = originalRootDir; } if (originalEditor === undefined) { - process.env.EDITOR = undefined; + restoreEnvironmentVariable("EDITOR", undefined); } else { process.env.EDITOR = originalEditor; } diff --git a/test/environment.ts b/test/environment.ts new file mode 100644 index 0000000..2fc6d58 --- /dev/null +++ b/test/environment.ts @@ -0,0 +1,11 @@ +/** Restore absence explicitly: assigning undefined stores the string "undefined". */ +export function restoreEnvironmentVariable( + name: string, + value: string | undefined +): void { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} From 09710f6e34f8fee8577225ca84404fb4cc98f530 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Fri, 23 Oct 2026 20:31:06 -0400 Subject: [PATCH 3/3] fix: type runtime pin expectations as tuples --- scripts/release-workflow.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/release-workflow.test.ts b/scripts/release-workflow.test.ts index a0990c4..cb1d86d 100644 --- a/scripts/release-workflow.test.ts +++ b/scripts/release-workflow.test.ts @@ -106,8 +106,8 @@ it("keeps CI and release runtime pins consistent", async () => { for (const [runtime, expected] of [ ["bun", "1.4.4"], ["node", "24.22.1"], - ]) { - expect(versions[runtime!]!.length).toBeGreaterThan(0); - expect([...new Set(versions[runtime!]!)]).toEqual([expected]); + ] as const) { + expect(versions[runtime]!.length).toBeGreaterThan(0); + expect([...new Set(versions[runtime]!)]).toEqual([expected]); } });