From 99ec767a4209f4144641a8d7036caa2a824ac009 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:19:11 -0500 Subject: [PATCH] Fix forced CI execution and align OpenCode shell fixture (#18230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use one explicit Turbo cache mode for forced CI execution and preserve cache trust boundaries. Match OpenCode’s ignored stdin in the relocated Linux shell-hook fixture while retaining strict OOM and parent-isolation assertions. Co-Authored-By: Paperclip --- doc/DEVELOPING.md | 3 +- .../src/server/runtime-config.test.ts | 4 +- scripts/ci/cache.integration.test.mjs | 54 ++++++++++++++++++- scripts/ci/run-task.mjs | 4 +- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 313bd129fdc..9568e183e33 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -184,7 +184,8 @@ changed cache definitions. The repository variable `CI_INCREMENTAL_MODE` accepts `enabled` (the default), `shadow` (report selections but execute everything), or `full`. Add the `full-ci` label to force all checks on one PR. Authorized `profile-ci` runs also execute -everything. Forced and shadow runs bypass task-result caches. Every routing run +everything. Forced and shadow runs disable local and remote task-result cache +reads and writes. Every routing run with a valid plan publishes affected and scheduled job counts with input reasons in its job summary. Required aggregates reject failed, cancelled, and unexpectedly skipped selected suites; the verify aggregate includes serialized tests and the diff --git a/packages/adapters/opencode-local/src/server/runtime-config.test.ts b/packages/adapters/opencode-local/src/server/runtime-config.test.ts index e5ed0fb4c6a..e6ada9b8851 100644 --- a/packages/adapters/opencode-local/src/server/runtime-config.test.ts +++ b/packages/adapters/opencode-local/src/server/runtime-config.test.ts @@ -58,14 +58,14 @@ describe("prepareOpenCodeRuntimeConfig", () => { expect(hookEnv.parent).toBeNull(); expect(hookEnv.child.BASH_ENV).toContain(relocated); const command = spawnSync("bash", ["-c", 'printf "%s" "$1"; exit 7', "bash", "literal ' $() value"], { - env: { ...process.env, ...hookEnv.child }, encoding: "utf8", + env: { ...process.env, ...hookEnv.child }, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); expect(command.stdout).toBe("literal ' $() value"); expect(command.status).toBe(7); if (process.platform === "linux") { const parentScore = await fs.readFile("/proc/self/oom_score_adj", "utf8"); expect(execFileSync("bash", ["-c", "cat /proc/self/oom_score_adj"], { - env: { ...process.env, ...hookEnv.child }, encoding: "utf8", + env: { ...process.env, ...hookEnv.child }, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }).trim()).toBe("1000"); expect(await fs.readFile("/proc/self/oom_score_adj", "utf8")).toBe(parentScore); } diff --git a/scripts/ci/cache.integration.test.mjs b/scripts/ci/cache.integration.test.mjs index ec9b31a285b..c7fddcad772 100644 --- a/scripts/ci/cache.integration.test.mjs +++ b/scripts/ci/cache.integration.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, copyFileSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; @@ -73,3 +73,55 @@ test("real Turbo executes existing suite scripts and invalidates source, argumen assert.equal(calls(), 7, "a failed task must execute on every attempt"); } finally { rmSync(directory, { recursive: true, force: true }); } }); + +test("the task launcher bypasses cache reads and writes when execution is forced", () => { + const directory = mkdtempSync(path.join(tmpdir(), "ci-turbo-forced-")); + const write = (name, value) => writeFileSync(path.join(directory, name), value); + const json = (name, value) => write(name, JSON.stringify(value)); + try { + execFileSync("git", ["init", "-q"], { cwd: directory }); + mkdirSync(path.join(directory, "scripts/ci"), { recursive: true }); + mkdirSync(path.join(directory, "bin")); + mkdirSync(path.join(directory, "packages/fixture"), { recursive: true }); + for (const name of ["run-task.mjs", "execution-plan.mjs"]) { + copyFileSync(new URL(`scripts/ci/${name}`, repositoryRoot), path.join(directory, "scripts/ci", name)); + } + write(".gitignore", ".turbo/\ncalls.txt\nresult.txt\nbin/\n"); + json("packages/fixture/package.json", { name: "fixture", private: true }); + json("package.json", { name: "forced-cache-proof", private: true, packageManager: "npm@10.0.0", + workspaces: ["packages/*"], scripts: { "test:run:general": "node check.cjs" } }); + json("package-lock.json", { name: "forced-cache-proof", lockfileVersion: 3, packages: { "": { name: "forced-cache-proof" } } }); + json("turbo.json", { globalDependencies: [], globalEnv: ["CI_TASK_CACHE_SCOPE"], tasks: { + "//#ci:server": { inputs: ["input.txt", "check.cjs"], outputs: ["result.txt"], cache: true }, + } }); + write("input.txt", "result"); + write("check.cjs", "const fs=require('node:fs');fs.appendFileSync('calls.txt','call\\n');fs.writeFileSync('result.txt',fs.readFileSync('input.txt'));"); + write("bin/pnpm", `#!${process.execPath}\nconst {spawnSync}=require('node:child_process');const args=process.argv.slice(2);if(args[0]==='--version'){console.log('9.15.4');}else{if(args[0]!=='exec'||args[1]!=='turbo')process.exit(2);const result=spawnSync(${JSON.stringify(turbo)},args.slice(2),{stdio:'inherit'});process.exit(result.status??1);}`); + chmodSync(path.join(directory, "bin/pnpm"), 0o755); + const run = force => { + const result = spawnSync(process.execPath, [path.join(directory, "scripts/ci/run-task.mjs")], { + cwd: directory, encoding: "utf8", env: { ...process.env, PATH: `${path.join(directory, "bin")}${path.delimiter}${process.env.PATH}`, + CI_TASK: "ci:server", CI_TASK_ARGS: "[]", CI_TASK_DRY_RUN: "false", CI_FORCE_TASKS: String(force), + TURBO_TOKEN: "", TURBO_TEAM: "", TURBO_TELEMETRY_DISABLED: "1" }, + }); + assert.equal(result.status, 0, result.stdout + result.stderr); + }; + const calls = () => readFileSync(path.join(directory, "calls.txt"), "utf8").trim().split("\n").length; + const cacheFiles = () => existsSync(path.join(directory, ".turbo/cache")) + ? readdirSync(path.join(directory, ".turbo/cache"), { recursive: true }).sort() : []; + run(true); run(true); + assert.equal(calls(), 2, "forced runs must execute even when a previous result exists"); + assert.deepEqual(cacheFiles(), [], "forced runs must not populate the task cache"); + run(false); run(false); + assert.equal(calls(), 3, "ordinary runs still reuse local results"); + const retainedCache = cacheFiles(); + assert.ok(retainedCache.length > 0); + run(true); + assert.equal(calls(), 4, "forced runs must bypass an existing cache hit"); + assert.deepEqual(cacheFiles(), retainedCache); + symlinkSync("input.txt", path.join(directory, "unclassified-link")); + run(false); + assert.equal(calls(), 5, "unclassified non-regular input must force execution"); + assert.deepEqual(cacheFiles(), retainedCache); + } finally { rmSync(directory, { recursive: true, force: true }); } +}); diff --git a/scripts/ci/run-task.mjs b/scripts/ci/run-task.mjs index da05e876bf2..73e8a23985a 100644 --- a/scripts/ci/run-task.mjs +++ b/scripts/ci/run-task.mjs @@ -41,9 +41,9 @@ const scope = { }; const force = process.env.CI_FORCE_TASKS === "true" || uncacheable; const result = spawnSync("pnpm", ["exec", "turbo", "run", execution.target, - "--root-turbo-json", runtimeConfig, "--summarize", "--cache=local:rw", "--cache-dir=.turbo/cache", + "--root-turbo-json", runtimeConfig, "--summarize", force ? "--cache=" : "--cache=local:rw", "--cache-dir=.turbo/cache", ...(process.env.CI_TASK_DRY_RUN === "true" ? ["--dry=json"] : []), - ...(force ? ["--force"] : []), "--", ...execution.args], { + "--", ...execution.args], { stdio: "inherit", env: { ...process.env, TURBO_TELEMETRY_DISABLED: "1", CI_TASK_CACHE_SCOPE: createHash("sha256").update(JSON.stringify(scope)).digest("hex") },