From 35285aefd5171ac53b2dc20356bef468bd6c318e Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 21:54:13 +0000 Subject: [PATCH 1/8] ci: speed up Windows node checks --- .github/workflows/node-ci.yml | 2 +- sdk/typescript/scripts/check-package.mjs | 36 +++++++---- .../scripts/run-windows-ci-tests.mjs | 63 +++++++++++++++++++ sdk/typescript/tests-ts/skeleton.test.ts | 5 +- 4 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 sdk/typescript/scripts/run-windows-ci-tests.mjs diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index c7a1e08e..8df7c84e 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -90,7 +90,7 @@ jobs: TMP: ${{ steps.windows-temp.outputs.path || runner.temp }} TMPDIR: ${{ steps.windows-temp.outputs.path || runner.temp }} CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: ${{ runner.environment == 'github-hosted' && runner.os == 'Windows' && 'true' || 'false' }} - run: pnpm --dir sdk/typescript run test + run: ${{ runner.os == 'Windows' && 'node sdk/typescript/scripts/run-windows-ci-tests.mjs' || 'pnpm --dir sdk/typescript run test' }} - name: Check formatting run: pnpm --dir sdk/typescript run format diff --git a/sdk/typescript/scripts/check-package.mjs b/sdk/typescript/scripts/check-package.mjs index c4a04a57..731a289f 100644 --- a/sdk/typescript/scripts/check-package.mjs +++ b/sdk/typescript/scripts/check-package.mjs @@ -44,6 +44,7 @@ function tar(args, encoding = "buffer") { } let offset = 0; +const archiveFiles = new Map(); for (; offset + 512 <= archiveBytes.byteLength; ) { const header = archiveBytes.subarray(offset, offset + 512); if (header.every((byte) => byte === 0)) { @@ -65,12 +66,31 @@ for (; offset + 512 <= archiveBytes.byteLength; ) { throw new Error("npm tarball contains an invalid tar entry."); } const size = Number.parseInt(sizeField || "0", 8); - offset += 512 + Math.ceil(size / 512) * 512; + const contentsStart = offset + 512; + const nextOffset = contentsStart + Math.ceil(size / 512) * 512; + if (nextOffset > archiveBytes.byteLength) { + throw new Error("npm tarball contains an invalid tar entry."); + } + if (header[156] === 0 || header[156] === 0x30) { + archiveFiles.set( + path, + archiveBytes.subarray(contentsStart, contentsStart + size), + ); + } + offset = nextOffset; } if (archiveBytes.subarray(offset).some((byte) => byte !== 0)) { throw new Error("npm tarball contains trailing tar data."); } +function archiveFile(path) { + const contents = archiveFiles.get(path); + if (contents === undefined) { + throw new Error("npm tarball contains an invalid tar entry: " + path + "."); + } + return contents; +} + const entries = tar(["-tzf", archive], "utf8").split(/\r?\n/u).filter(Boolean); const files = new Set(entries); if (files.size !== entries.length) { @@ -211,7 +231,7 @@ if ([3, 6, 9].some((index) => launcherPermissions[index] !== "x")) { throw new Error("npm package CLI launcher is not executable."); } const packageJson = JSON.parse( - tar(["-xOf", archive, "package/package.json"]).toString("utf8"), + archiveFile("package/package.json").toString("utf8"), ); if ( packageJson.name !== "@openai/codex-security" || @@ -251,22 +271,16 @@ function brotliPayload(bytes, file) { } for (const file of compressedFiles) { - payloads.push( - brotliPayload(tar(["-xOf", archive, file]), file).toString("utf8"), - ); + payloads.push(brotliPayload(archiveFile(file), file).toString("utf8")); } for (const parts of compressedParts.values()) { parts.sort((left, right) => left.part - right.part); - const bytes = Buffer.concat( - parts.map(({ file }) => tar(["-xOf", archive, file])), - ); + const bytes = Buffer.concat(parts.map(({ file }) => archiveFile(file))); payloads.push(brotliPayload(bytes, parts[0].file).toString("utf8")); } for (const file of files) { if (/\.png$/iu.test(file)) { - const digest = createHash("sha256") - .update(tar(["-xOf", archive, file])) - .digest("hex"); + const digest = createHash("sha256").update(archiveFile(file)).digest("hex"); if (digest !== PUBLIC_LOGO_SHA256) { throw new Error(`npm tarball contains an unexpected PNG asset: ${file}.`); } diff --git a/sdk/typescript/scripts/run-windows-ci-tests.mjs b/sdk/typescript/scripts/run-windows-ci-tests.mjs new file mode 100644 index 00000000..b4145949 --- /dev/null +++ b/sdk/typescript/scripts/run-windows-ci-tests.mjs @@ -0,0 +1,63 @@ +import { spawn } from "node:child_process"; +import { readdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const testsDirectory = new URL("../tests-ts/", import.meta.url); +const packageDirectory = fileURLToPath(new URL("../", import.meta.url)); +const tests = (await readdir(testsDirectory)) + .filter((file) => file.endsWith(".test.ts")) + .sort(); +const shardSeeds = [ + ["api.test.ts"], + ["runtime.test.ts"], + ["cli-authentication.test.ts", "scan-recovery.test.ts"], + [], +]; +const assigned = new Set(shardSeeds.flat()); +for (const file of assigned) { + if (!tests.includes(file)) { + throw new Error("Windows CI test shard references a missing file: " + file); + } +} +for (const file of tests) { + if (!assigned.has(file)) shardSeeds[3].push(file); +} + +const shards = shardSeeds.map((files) => + files.map((file) => "./tests-ts/" + file), +); +if ( + shards.flat().length !== tests.length || + new Set(shards.flat()).size !== tests.length +) { + throw new Error("Windows CI test shards must run every test file once."); +} + +const results = await Promise.all( + shards.map( + (files, index) => + new Promise((resolve, reject) => { + console.log( + "Windows CI test shard " + + (index + 1) + + "/" + + shards.length + + ": " + + files.join(" "), + ); + const child = spawn("bun", ["test", "--timeout", "30000", ...files], { + cwd: packageDirectory, + stdio: "inherit", + windowsHide: true, + }); + child.once("error", reject); + child.once("close", (code) => { + resolve(code ?? 1); + }); + }), + ), +); + +if (results.some((code) => code !== 0)) { + process.exitCode = 1; +} diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index 128ec67b..0eac83d6 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -82,7 +82,10 @@ describe("TypeScript package skeleton", () => { expect(packageJson.scripts.test).toBe( "bun test --timeout 30000 ./tests-ts", ); - expect(ciWorkflow).toContain("run: pnpm --dir sdk/typescript run test\n"); + expect(ciWorkflow).toContain( + "runner.os == 'Windows' && 'node sdk/typescript/scripts/run-windows-ci-tests.mjs'", + ); + expect(ciWorkflow).toContain("|| 'pnpm --dir sdk/typescript run test'"); expect(ciWorkflow).not.toContain("--timeout 60000"); }); From 56104da7c025085fc7620aa6f28035a5f08095c0 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:02:57 +0000 Subject: [PATCH 2/8] ci: isolate Windows test shards --- .github/workflows/node-ci.yml | 150 +++++++++++++++++- .../scripts/run-windows-ci-tests.mjs | 21 ++- sdk/typescript/tests-ts/skeleton.test.ts | 7 +- 3 files changed, 170 insertions(+), 8 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 8df7c84e..a9d7eb7c 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -20,11 +20,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] node: ["22.13.0"] include: - - os: windows-latest - node: "24" - os: ubuntu-latest node: "24.0.0" - os: ubuntu-latest @@ -90,7 +88,7 @@ jobs: TMP: ${{ steps.windows-temp.outputs.path || runner.temp }} TMPDIR: ${{ steps.windows-temp.outputs.path || runner.temp }} CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: ${{ runner.environment == 'github-hosted' && runner.os == 'Windows' && 'true' || 'false' }} - run: ${{ runner.os == 'Windows' && 'node sdk/typescript/scripts/run-windows-ci-tests.mjs' || 'pnpm --dir sdk/typescript run test' }} + run: pnpm --dir sdk/typescript run test - name: Check formatting run: pnpm --dir sdk/typescript run format @@ -121,3 +119,147 @@ jobs: ' node bin/codex-security.mjs --version node bin/codex-security.mjs --help + + windows-test: + name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }} / tests-${{ matrix.shard }} + runs-on: windows-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + node: ["22.13.0", "24"] + shard: [1, 2, 3, 4] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + package_json_file: sdk/typescript/package.json + cache: true + cache_dependency_path: sdk/typescript/pnpm-lock.yaml + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: sdk/typescript/pnpm-lock.yaml + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.14" + + - name: Install dependencies + run: pnpm --dir sdk/typescript install --frozen-lockfile + + - name: Prepare private Windows test root + id: windows-temp + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $path = Join-Path $env:USERPROFILE '.codex-security-ci-temp' + New-Item -ItemType Directory -Path $path -Force | Out-Null + $sid = (whoami /user /fo csv /nh | ConvertFrom-Csv -Header Name, Sid).Sid + & icacls $path /inheritance:r /grant:r "*${sid}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Could not secure the Windows test root' } + "path=$path" >> $env:GITHUB_OUTPUT + + - name: Test shard ${{ matrix.shard }} + timeout-minutes: 10 + env: + TEMP: ${{ steps.windows-temp.outputs.path }} + TMP: ${{ steps.windows-temp.outputs.path }} + TMPDIR: ${{ steps.windows-temp.outputs.path }} + CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: ${{ runner.environment == 'github-hosted' && 'true' || 'false' }} + run: node sdk/typescript/scripts/run-windows-ci-tests.mjs ${{ matrix.shard }} + + windows-verify: + name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }} / verify + runs-on: windows-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + node: ["22.13.0", "24"] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + package_json_file: sdk/typescript/package.json + cache: true + cache_dependency_path: sdk/typescript/pnpm-lock.yaml + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: sdk/typescript/pnpm-lock.yaml + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.14" + + - name: Install dependencies + run: pnpm --dir sdk/typescript install --frozen-lockfile + + - name: Typecheck + run: pnpm --dir sdk/typescript run types + + - name: Check formatting + run: pnpm --dir sdk/typescript run format + + - name: Build + run: pnpm --dir sdk/typescript run build + + - name: Pack + working-directory: sdk/typescript + run: pnpm pack --pack-destination ../../dist + + - name: Inspect package + working-directory: sdk/typescript + shell: bash + run: pnpm run check:package ../../dist/*.tgz + + - name: Smoke-test Node.js runtime + working-directory: sdk/typescript + shell: bash + run: | + set -euo pipefail + node --input-type=module --eval ' + import { CodexSecurity } from "@openai/codex-security"; + + if (typeof CodexSecurity !== "function") { + throw new Error("The SDK does not export CodexSecurity."); + } + ' + node bin/codex-security.mjs --version + node bin/codex-security.mjs --help + + windows: + name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }} + runs-on: ubuntu-latest + if: always() + needs: [windows-test, windows-verify] + strategy: + fail-fast: false + matrix: + node: ["22.13.0", "24"] + + steps: + - name: Require every Windows coverage job + if: needs.windows-test.result != 'success' || needs.windows-verify.result != 'success' + run: exit 1 diff --git a/sdk/typescript/scripts/run-windows-ci-tests.mjs b/sdk/typescript/scripts/run-windows-ci-tests.mjs index b4145949..eac4e53e 100644 --- a/sdk/typescript/scripts/run-windows-ci-tests.mjs +++ b/sdk/typescript/scripts/run-windows-ci-tests.mjs @@ -33,9 +33,26 @@ if ( throw new Error("Windows CI test shards must run every test file once."); } +const requestedShard = + process.argv[2] === undefined + ? undefined + : Number.parseInt(process.argv[2], 10); +if ( + requestedShard !== undefined && + (!Number.isSafeInteger(requestedShard) || + requestedShard < 1 || + requestedShard > shards.length) +) { + throw new Error("Usage: node scripts/run-windows-ci-tests.mjs [1-4]"); +} +const selectedShards = + requestedShard === undefined + ? shards.map((files, index) => ({ files, index })) + : [{ files: shards[requestedShard - 1], index: requestedShard - 1 }]; + const results = await Promise.all( - shards.map( - (files, index) => + selectedShards.map( + ({ files, index }) => new Promise((resolve, reject) => { console.log( "Windows CI test shard " + diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index 0eac83d6..1226a2b0 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -83,9 +83,12 @@ describe("TypeScript package skeleton", () => { "bun test --timeout 30000 ./tests-ts", ); expect(ciWorkflow).toContain( - "runner.os == 'Windows' && 'node sdk/typescript/scripts/run-windows-ci-tests.mjs'", + "run: node sdk/typescript/scripts/run-windows-ci-tests.mjs ${{ matrix.shard }}", ); - expect(ciWorkflow).toContain("|| 'pnpm --dir sdk/typescript run test'"); + expect(ciWorkflow).toContain( + "name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }}", + ); + expect(ciWorkflow).toContain("run: pnpm --dir sdk/typescript run test"); expect(ciWorkflow).not.toContain("--timeout 60000"); }); From eba4ce6aed5de0b48f54e774a418726240d3994d Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:07:31 +0000 Subject: [PATCH 3/8] ci: skip unused Windows shard caches --- .github/workflows/node-ci.yml | 6 ------ sdk/typescript/scripts/smoke-package.mjs | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index a9d7eb7c..342dacd6 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -140,15 +140,11 @@ jobs: uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: package_json_file: sdk/typescript/package.json - cache: true - cache_dependency_path: sdk/typescript/pnpm-lock.yaml - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ matrix.node }} - cache: npm - cache-dependency-path: sdk/typescript/pnpm-lock.yaml - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 @@ -198,8 +194,6 @@ jobs: uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: package_json_file: sdk/typescript/package.json - cache: true - cache_dependency_path: sdk/typescript/pnpm-lock.yaml - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 diff --git a/sdk/typescript/scripts/smoke-package.mjs b/sdk/typescript/scripts/smoke-package.mjs index 0cd6b116..3ccb900a 100644 --- a/sdk/typescript/scripts/smoke-package.mjs +++ b/sdk/typescript/scripts/smoke-package.mjs @@ -314,6 +314,7 @@ try { "--prefer-offline", "--include=optional", "--ignore-scripts", + "--package-lock=false", "--no-audit", "--no-fund", archive, From 254bec12a597e0eec22f71b02d0069cc57daac6e Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:14:51 +0000 Subject: [PATCH 4/8] ci: reduce Windows package-manager setup --- .github/workflows/node-ci.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 342dacd6..d4b9f37d 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -136,16 +136,15 @@ jobs: with: persist-credentials: false - - name: Set up pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - package_json_file: sdk/typescript/package.json - - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ matrix.node }} + - name: Set up pnpm + working-directory: sdk/typescript + run: corepack enable && corepack install + - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: @@ -190,11 +189,6 @@ jobs: with: persist-credentials: false - - name: Set up pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - package_json_file: sdk/typescript/package.json - - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: @@ -202,6 +196,10 @@ jobs: cache: npm cache-dependency-path: sdk/typescript/pnpm-lock.yaml + - name: Set up pnpm + working-directory: sdk/typescript + run: corepack enable && corepack install + - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: From c738c1a000ed359feefc9d7de2195b0cbbed064b Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:18:13 +0000 Subject: [PATCH 5/8] ci: run Windows pnpm from package root --- .github/workflows/node-ci.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index d4b9f37d..365da294 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -151,7 +151,8 @@ jobs: bun-version: "1.3.14" - name: Install dependencies - run: pnpm --dir sdk/typescript install --frozen-lockfile + working-directory: sdk/typescript + run: pnpm install --frozen-lockfile - name: Prepare private Windows test root id: windows-temp @@ -206,16 +207,20 @@ jobs: bun-version: "1.3.14" - name: Install dependencies - run: pnpm --dir sdk/typescript install --frozen-lockfile + working-directory: sdk/typescript + run: pnpm install --frozen-lockfile - name: Typecheck - run: pnpm --dir sdk/typescript run types + working-directory: sdk/typescript + run: pnpm run types - name: Check formatting - run: pnpm --dir sdk/typescript run format + working-directory: sdk/typescript + run: pnpm run format - name: Build - run: pnpm --dir sdk/typescript run build + working-directory: sdk/typescript + run: pnpm run build - name: Pack working-directory: sdk/typescript From a0b36d9c5cd131496b51e27fbe5aa862169ebda1 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:20:10 +0000 Subject: [PATCH 6/8] ci: avoid stale Windows Corepack shims --- .github/workflows/node-ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 365da294..7c30de5d 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -142,8 +142,7 @@ jobs: node-version: ${{ matrix.node }} - name: Set up pnpm - working-directory: sdk/typescript - run: corepack enable && corepack install + run: npm install --global pnpm@11.9.0 --no-audit --no-fund - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 @@ -198,8 +197,7 @@ jobs: cache-dependency-path: sdk/typescript/pnpm-lock.yaml - name: Set up pnpm - working-directory: sdk/typescript - run: corepack enable && corepack install + run: npm install --global pnpm@11.9.0 --no-audit --no-fund - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 From 472c38bd0f66e05c1801b817ad68b99181bebaa6 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:25:21 +0000 Subject: [PATCH 7/8] ci: balance slow Windows test shards --- .github/workflows/node-ci.yml | 2 +- .../scripts/run-windows-ci-tests.mjs | 75 +++++++++++++------ 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 7c30de5d..d1302c7c 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -128,7 +128,7 @@ jobs: fail-fast: false matrix: node: ["22.13.0", "24"] - shard: [1, 2, 3, 4] + shard: [1, 2, 3, 4, 5, 6, 7] steps: - name: Checkout repository diff --git a/sdk/typescript/scripts/run-windows-ci-tests.mjs b/sdk/typescript/scripts/run-windows-ci-tests.mjs index eac4e53e..80896ece 100644 --- a/sdk/typescript/scripts/run-windows-ci-tests.mjs +++ b/sdk/typescript/scripts/run-windows-ci-tests.mjs @@ -7,30 +7,50 @@ const packageDirectory = fileURLToPath(new URL("../", import.meta.url)); const tests = (await readdir(testsDirectory)) .filter((file) => file.endsWith(".test.ts")) .sort(); +const slowApiTests = [ + "keeps a private preflight snapshot isolated from persistent credentials", + "reuses keyring-compatible credentials across separate scan clients", + "serializes parallel scans sharing a managed credential home", + "recreates isolated and managed runtimes when scan authentication changes", + "does not reimport ambient credentials after an explicit logout", +].join("|"); const shardSeeds = [ - ["api.test.ts"], - ["runtime.test.ts"], - ["cli-authentication.test.ts", "scan-recovery.test.ts"], - [], + { + files: ["api.test.ts"], + testNamePattern: slowApiTests, + }, + { + files: ["api.test.ts"], + testNamePattern: `^(?!.*(?:${slowApiTests})).*$`, + }, + { files: ["runtime.test.ts"] }, + { files: ["cli-authentication.test.ts"] }, + { files: ["scan-recovery.test.ts"] }, + { files: [] }, + { files: [] }, ]; -const assigned = new Set(shardSeeds.flat()); +const assigned = new Set(shardSeeds.flatMap(({ files }) => files)); for (const file of assigned) { if (!tests.includes(file)) { throw new Error("Windows CI test shard references a missing file: " + file); } } -for (const file of tests) { - if (!assigned.has(file)) shardSeeds[3].push(file); +const unassigned = tests.filter((file) => !assigned.has(file)); +for (const [index, file] of unassigned.entries()) { + shardSeeds[5 + (index % 2)].files.push(file); } -const shards = shardSeeds.map((files) => - files.map((file) => "./tests-ts/" + file), -); -if ( - shards.flat().length !== tests.length || - new Set(shards.flat()).size !== tests.length -) { - throw new Error("Windows CI test shards must run every test file once."); +const assignments = new Map(); +for (const { files } of shardSeeds) { + for (const file of files) { + assignments.set(file, (assignments.get(file) ?? 0) + 1); + } +} +for (const file of tests) { + const expectedAssignments = file === "api.test.ts" ? 2 : 1; + if (assignments.get(file) !== expectedAssignments) { + throw new Error("Windows CI test shards must run every test file."); + } } const requestedShard = @@ -41,28 +61,37 @@ if ( requestedShard !== undefined && (!Number.isSafeInteger(requestedShard) || requestedShard < 1 || - requestedShard > shards.length) + requestedShard > shardSeeds.length) ) { - throw new Error("Usage: node scripts/run-windows-ci-tests.mjs [1-4]"); + throw new Error("Usage: node scripts/run-windows-ci-tests.mjs [1-7]"); } const selectedShards = requestedShard === undefined - ? shards.map((files, index) => ({ files, index })) - : [{ files: shards[requestedShard - 1], index: requestedShard - 1 }]; + ? shardSeeds.map((shard, index) => ({ ...shard, index })) + : [{ ...shardSeeds[requestedShard - 1], index: requestedShard - 1 }]; const results = await Promise.all( selectedShards.map( - ({ files, index }) => + ({ files, index, testNamePattern }) => new Promise((resolve, reject) => { + const paths = files.map((file) => "./tests-ts/" + file); console.log( "Windows CI test shard " + (index + 1) + "/" + - shards.length + + shardSeeds.length + ": " + - files.join(" "), + paths.join(" ") + + (testNamePattern === undefined + ? "" + : " --test-name-pattern " + testNamePattern), ); - const child = spawn("bun", ["test", "--timeout", "30000", ...files], { + const args = ["test", "--timeout", "30000"]; + if (testNamePattern !== undefined) { + args.push("--test-name-pattern", testNamePattern); + } + args.push(...paths); + const child = spawn("bun", args, { cwd: packageDirectory, stdio: "inherit", windowsHide: true, From 49b942bfb5f4e9c9d3f3e63a6770f4ac92a108d8 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 7 Aug 2026 22:30:18 +0000 Subject: [PATCH 8/8] ci: keep Windows verification off critical path --- .github/workflows/node-ci.yml | 18 ++++++++++-------- .../scripts/run-windows-ci-tests.mjs | 9 ++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index d1302c7c..60892c1e 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -174,6 +174,16 @@ jobs: CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: ${{ runner.environment == 'github-hosted' && 'true' || 'false' }} run: node sdk/typescript/scripts/run-windows-ci-tests.mjs ${{ matrix.shard }} + - name: Typecheck + if: matrix.shard == 7 + working-directory: sdk/typescript + run: pnpm run types + + - name: Check formatting + if: matrix.shard == 7 + working-directory: sdk/typescript + run: pnpm run format + windows-verify: name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }} / verify runs-on: windows-latest @@ -208,14 +218,6 @@ jobs: working-directory: sdk/typescript run: pnpm install --frozen-lockfile - - name: Typecheck - working-directory: sdk/typescript - run: pnpm run types - - - name: Check formatting - working-directory: sdk/typescript - run: pnpm run format - - name: Build working-directory: sdk/typescript run: pnpm run build diff --git a/sdk/typescript/scripts/run-windows-ci-tests.mjs b/sdk/typescript/scripts/run-windows-ci-tests.mjs index 80896ece..f0fa31ad 100644 --- a/sdk/typescript/scripts/run-windows-ci-tests.mjs +++ b/sdk/typescript/scripts/run-windows-ci-tests.mjs @@ -36,8 +36,15 @@ for (const file of assigned) { } } const unassigned = tests.filter((file) => !assigned.has(file)); +const slowRemainderFiles = new Set([ + "deep-scan-workbench.test.ts", + "release-automation.test.ts", + "scan-comparison.test.ts", +]); for (const [index, file] of unassigned.entries()) { - shardSeeds[5 + (index % 2)].files.push(file); + shardSeeds[slowRemainderFiles.has(file) ? 6 : 5 + (index % 2)].files.push( + file, + ); } const assignments = new Map();