diff --git a/tests/boundary/filesystem/ciCachePolicy.test.ts b/tests/boundary/filesystem/ciCachePolicy.test.ts index 058c47c94b..d13ef20b2b 100644 --- a/tests/boundary/filesystem/ciCachePolicy.test.ts +++ b/tests/boundary/filesystem/ciCachePolicy.test.ts @@ -10,6 +10,7 @@ import { writeFile, } from "node:fs/promises"; import { delimiter, join } from "node:path"; +import { runInNewContext } from "node:vm"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; @@ -17,6 +18,69 @@ import { z } from "zod"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +// These policy expressions use only JS-compatible boolean operators, property +// access, literals and format; exercise the parsed workflow rather than copies. +const evaluatePolicy = ( + expression: string, + context: Record, +): unknown => + runInNewContext( + expression.replace(/^\$\{\{\s*|\s*\}\}$/gu, ""), + { + ...context, + format: (template: string, value: string) => + template.replace("{0}", value), + }, + { timeout: 100 }, + ); + +const assertRetentionPolicy = (expression: string): void => { + for (const conclusion of [ + "success", + "failure", + "timed_out", + "cancelled", + "skipped", + "action_required", + ]) { + for (const event of [ + "push", + "schedule", + "workflow_dispatch", + "pull_request", + ]) { + for (const [repositoryName, branch] of [ + ["owner/repo", "main"], + ["untrusted/fork", "main"], + ["owner/repo", "feature"], + ]) { + const permitted = + repositoryName === "owner/repo" && + branch === "main" && + event !== "pull_request" && + ["success", "failure", "timed_out", "cancelled"].includes(conclusion); + expect( + evaluatePolicy(expression, { + github: { + repository: "owner/repo", + event: { + repository: { default_branch: "main" }, + workflow_run: { + conclusion, + event, + head_repository: { full_name: repositoryName }, + head_branch: branch, + }, + }, + }, + }), + `${repositoryName}:${branch}:${event}:${conclusion}`, + ).toBe(permitted); + } + } + } +}; + const repository = process.cwd(); const actionPath = join(repository, ".github/actions/npm-cache/action.yml"); const readAction = async () => @@ -512,30 +576,7 @@ it("deletes superseded caches only from the trusted follow-up workflow", async ( actions: "write", }); const job = workflow.jobs.retain; - expect(job?.if).toContain( - "github.event.workflow_run.conclusion == 'success'", - ); - expect(job?.if).toContain( - "github.event.workflow_run.conclusion == 'failure'", - ); - expect(job?.if).toContain( - "github.event.workflow_run.conclusion == 'timed_out'", - ); - expect(job?.if).toContain( - "github.event.workflow_run.conclusion == 'cancelled'", - ); - expect(job?.if).toContain( - "github.event.workflow_run.head_repository.full_name == github.repository", - ); - expect(job?.if).toContain( - "github.event.workflow_run.head_branch == github.event.repository.default_branch", - ); - expect(job?.if).toContain("github.event.workflow_run.event == 'push'"); - expect(job?.if).toContain("github.event.workflow_run.event == 'schedule'"); - expect(job?.if).toContain( - "github.event.workflow_run.event == 'workflow_dispatch'", - ); - expect(job?.if).not.toContain("pull_request"); + assertRetentionPolicy(z.string().parse(job?.if)); const steps = job?.steps ?? []; expect(steps.some((step) => step.run === "npm ci")).toBe(false); const checkouts = steps.filter( @@ -729,6 +770,43 @@ it("exports turbo remote-cache credentials wherever a workflow runs turbo", asyn expect(cache, `${file}:${name}`).not.toContain("local:rw,remote:r'"); } else expect(cache, `${file}:${name}`).toContain("local:rw,remote:r'"); expect(cache, `${file}:${name}`).toContain("local:rw,remote:rw"); + for (const event of [ + "push", + "schedule", + "workflow_dispatch", + "pull_request", + "workflow_call", + ]) { + for (const ref of ["refs/heads/main", "refs/heads/feature"]) { + for (const grant of [false, true]) { + const permitted = + (grantsReleaseWrite && grant) || + (ref === "refs/heads/main" && + (["push", "schedule", "workflow_dispatch"].includes(event) || + (grantsCalledEvent && event === "workflow_call"))); + const context = { + github: { + ref, + event_name: event, + event: { repository: { default_branch: "main" } }, + }, + inputs: { remote_cache_write: grant }, + secrets: { TURBO_TOKEN: "recording-token" }, + }; + const label = `${file}:${name}:${event}:${ref}:${grant}`; + expect(evaluatePolicy(token, context), label).toBe( + permitted ? "recording-token" : "", + ); + expect(evaluatePolicy(cache, context), label).toBe( + permitted + ? "local:rw,remote:rw" + : grantsReleaseWrite + ? "local:rw" + : "local:rw,remote:r", + ); + } + } + } expect(env.TURBO_TEAM, `${file}:${name}`).toBe("${{ vars.TURBO_TEAM }}"); } } diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index c3b560ec47..a0a07cffd5 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -501,6 +501,7 @@ it("selects only a trusted successful ancestor from recent CI push runs", async }) => { skip(process.platform === "win32", "the fake gh is a POSIX shell script"); const { directory, base } = await fixture(); + const foreignBase = await change(directory, ["README.md"]); const head = await change(directory, ["src/evm/client.ts"]); const bin = join(directory, "fake-bin"); await mkdir(bin); @@ -517,7 +518,7 @@ it("selects only a trusted successful ancestor from recent CI push runs", async conclusion: "success", head_branch: "main", head_repository: { full_name: "untrusted/fork" }, - head_sha: base, + head_sha: foreignBase, }, { event: "push",