diff --git a/.github/workflows/approve-maintainer-pr-workflow-runs.yaml b/.github/workflows/approve-maintainer-pr-workflow-runs.yaml index defab07c13c..4550af557e1 100644 --- a/.github/workflows/approve-maintainer-pr-workflow-runs.yaml +++ b/.github/workflows/approve-maintainer-pr-workflow-runs.yaml @@ -3,9 +3,9 @@ name: Automation / Approve Maintainer PR Workflow Runs -# pull_request_target loads this workflow from the trusted base branch. The -# pinned action reads repository metadata and approves exact workflow-run IDs. -# Do not add a checkout or execute pull-request code in this workflow. +# pull_request_target loads this workflow from the base branch. The only +# checkout is the helper at the event's exact base SHA; PR-head code is never +# checked out or executed with actions: write. on: # Keep every target branch eligible so maintainer-owned stacked PRs receive # the same exact-head approval policy as PRs that target main. @@ -27,232 +27,77 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 2 steps: + - name: Validate trusted helper revision + env: + TRUSTED_HELPER_ROOT: ${{ github.workspace }}/.trusted-maintainer-approval + TRUSTED_HELPER_SHA: ${{ github.event.pull_request.base.sha }} + shell: bash + run: | + set -euo pipefail + [[ "$TRUSTED_HELPER_SHA" =~ ^[a-f0-9]{40}$ ]] || { + echo "::error::PR base SHA must be a lowercase 40-character commit SHA" + exit 1 + } + case "$TRUSTED_HELPER_ROOT" in + "$GITHUB_WORKSPACE"/*) ;; + *) echo "::error::Trusted helper root must stay inside the workflow workspace"; exit 1 ;; + esac + [[ ! -e "$TRUSTED_HELPER_ROOT" && ! -L "$TRUSTED_HELPER_ROOT" ]] || { + echo "::error::Trusted helper root already exists" + exit 1 + } + + - name: Check out trusted approval helper + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: NVIDIA/NemoClaw + ref: ${{ github.event.pull_request.base.sha }} + path: .trusted-maintainer-approval + sparse-checkout: | + tools/ci/approve-maintainer-pr-workflow-runs.mts + sparse-checkout-cone-mode: "false" + persist-credentials: "false" + fetch-depth: "1" + + - name: Verify trusted approval helper + env: + TRUSTED_HELPER_RELATIVE_PATH: tools/ci/approve-maintainer-pr-workflow-runs.mts + TRUSTED_HELPER_ROOT: ${{ github.workspace }}/.trusted-maintainer-approval + TRUSTED_HELPER_SHA: ${{ github.event.pull_request.base.sha }} + shell: bash + run: | + set -euo pipefail + [[ -d "$TRUSTED_HELPER_ROOT" && ! -L "$TRUSTED_HELPER_ROOT" ]] || { + echo "::error::Trusted helper checkout must be a non-symlink directory" + exit 1 + } + [[ "$(git -C "$TRUSTED_HELPER_ROOT" rev-parse --verify HEAD)" == "$TRUSTED_HELPER_SHA" ]] || { + echo "::error::Trusted helper checkout does not match the PR base SHA" + exit 1 + } + helper="$TRUSTED_HELPER_ROOT/$TRUSTED_HELPER_RELATIVE_PATH" + [[ -f "$helper" && ! -L "$helper" ]] || { + echo "::error::Trusted approval helper is missing or is not a regular file" + exit 1 + } + - name: Approve exact-head maintainer workflow runs uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + TRUSTED_HELPER_PATH: ${{ github.workspace }}/.trusted-maintainer-approval/tools/ci/approve-maintainer-pr-workflow-runs.mts + TRUSTED_HELPER_SHA: ${{ github.event.pull_request.base.sha }} with: github-token: ${{ github.token }} script: | - // Keeping this policy inline prevents a write-capable - // pull_request_target job from checking out repository files. - const SHA_PATTERN = /^[0-9a-f]{40}$/i; - const TRUSTED_BASE_PERMISSIONS = new Set(['write', 'admin']); - const POLL_ATTEMPTS = 12; - const POLL_INTERVAL_MS = 5000; - const { owner, repo } = context.repo; - - function validateEventPullRequest(value) { - if (!value || typeof value !== 'object') { - throw new Error('Invalid pull_request_target payload: pull_request is missing'); - } - if (!Number.isInteger(value.number) || value.number <= 0) { - throw new Error(`Invalid pull request number: ${value.number}`); - } - if (typeof value.head?.sha !== 'string' || !SHA_PATTERN.test(value.head.sha)) { - throw new Error(`Invalid event head SHA for PR #${value.number}`); - } - return { number: value.number, headSha: value.head.sha.toLowerCase() }; - } - - function liveHeadSha(pullRequest, prNumber) { - const headSha = pullRequest?.head?.sha; - if (typeof headSha !== 'string' || !SHA_PATTERN.test(headSha)) { - throw new Error(`Invalid live head SHA for PR #${prNumber}`); - } - return headSha.toLowerCase(); - } - - function repositoryName(value) { - return typeof value === 'string' ? value.toLowerCase() : ''; - } - - function isSameRepositoryHead(pullRequest) { - return ( - repositoryName(pullRequest?.head?.repo?.full_name) === - repositoryName(`${owner}/${repo}`) - ); - } - - async function loadLivePullRequest(prNumber) { - const response = await github.rest.pulls.get({ - owner, - repo, - pull_number: prNumber, - }); - const pullRequest = response.data; - if (pullRequest?.number !== prNumber || pullRequest?.state !== 'open') { - throw new Error(`PR #${prNumber} is not an open pull request`); - } - const baseRepository = pullRequest.base?.repo?.full_name; - if ( - typeof baseRepository !== 'string' || - baseRepository.toLowerCase() !== `${owner}/${repo}`.toLowerCase() - ) { - throw new Error(`PR #${prNumber} does not target ${owner}/${repo}`); - } - return pullRequest; - } - - async function loadAuthorPermission(author) { - try { - const response = await github.rest.repos.getCollaboratorPermissionLevel({ - owner, - repo, - username: author, - }); - const responseLogin = response.data.user?.login; - if ( - typeof responseLogin !== 'string' || - responseLogin.toLowerCase() !== author.toLowerCase() - ) { - throw new Error(`Permission response did not match PR author ${author}`); - } - return response.data; - } catch (error) { - if (error?.status === 404) return null; - throw error; - } - } - - function hasWritePermission(permission) { - const basePermission = String(permission?.permission ?? '').toLowerCase(); - // GitHub maps maintain to the write base permission. role_name can - // contain an arbitrary custom-role label, so it is not authority. - return TRUSTED_BASE_PERMISSIONS.has(basePermission); + const { pathToFileURL } = require('node:url'); + const helperPath = process.env.TRUSTED_HELPER_PATH; + const helperSha = process.env.TRUSTED_HELPER_SHA; + if (!helperPath || !helperSha) { + throw new Error('Trusted approval helper identity is missing'); } - - function belongsToExactPullRequest(run, prNumber, headSha) { - if ( - !Number.isInteger(run?.id) || - run.id <= 0 || - run.event !== 'pull_request' || - run.status !== 'completed' || - run.conclusion !== 'action_required' || - String(run.head_sha ?? '').toLowerCase() !== headSha || - repositoryName(run.head_repository?.full_name) !== - repositoryName(`${owner}/${repo}`) - ) { - return false; - } - return ( - Array.isArray(run.pull_requests) && - run.pull_requests.some( - (pullRequest) => - pullRequest?.number === prNumber && - String(pullRequest.head?.sha ?? '').toLowerCase() === headSha, - ) - ); - } - - const eventPullRequest = validateEventPullRequest(context.payload.pull_request); - const initialPullRequest = await loadLivePullRequest(eventPullRequest.number); - const expectedHeadSha = liveHeadSha(initialPullRequest, eventPullRequest.number); - if (expectedHeadSha !== eventPullRequest.headSha) { - core.info( - `PR #${eventPullRequest.number} moved from event head ${eventPullRequest.headSha} to ${expectedHeadSha}; no workflow runs approved`, - ); - return; - } - if (!isSameRepositoryHead(initialPullRequest)) { - core.info( - `PR #${eventPullRequest.number} head repository is not ${owner}/${repo}; workflow runs remain gated`, - ); - return; + const helperUrl = pathToFileURL(helperPath).href; + const helper = await import(`${helperUrl}?sha=${encodeURIComponent(helperSha)}`); + if (typeof helper.approveMaintainerPrWorkflowRuns !== 'function') { + throw new Error('Trusted approval helper does not export its entrypoint'); } - - const author = initialPullRequest.user?.login; - if (typeof author !== 'string' || author.length === 0) { - throw new Error(`PR #${eventPullRequest.number} has no live author`); - } - const permission = await loadAuthorPermission(author); - if (!permission || !hasWritePermission(permission)) { - core.info( - `PR #${eventPullRequest.number} author ${author} does not have write, maintain, or admin permission; workflow runs remain gated`, - ); - return; - } - - const approvedRunIds = new Set(); - for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) { - const currentPullRequest = await loadLivePullRequest(eventPullRequest.number); - if (liveHeadSha(currentPullRequest, eventPullRequest.number) !== expectedHeadSha) { - core.warning( - `PR #${eventPullRequest.number} head changed during workflow-run discovery; no further runs approved`, - ); - return; - } - - const runs = await github.paginate( - github.rest.actions.listWorkflowRunsForRepo, - { - owner, - repo, - event: 'pull_request', - head_sha: expectedHeadSha, - status: 'action_required', - per_page: 100, - }, - ); - - for (const run of runs) { - if ( - approvedRunIds.has(run?.id) || - !belongsToExactPullRequest( - run, - eventPullRequest.number, - expectedHeadSha, - ) - ) { - continue; - } - - const liveBeforeApproval = await loadLivePullRequest(eventPullRequest.number); - if ( - liveHeadSha(liveBeforeApproval, eventPullRequest.number) !== - expectedHeadSha - ) { - core.warning( - `PR #${eventPullRequest.number} head changed before workflow-run approval; no further runs approved`, - ); - return; - } - if (!isSameRepositoryHead(liveBeforeApproval)) { - core.warning( - `PR #${eventPullRequest.number} head repository changed; no further runs approved`, - ); - return; - } - if ( - String(liveBeforeApproval.user?.login ?? '').toLowerCase() !== - author.toLowerCase() - ) { - throw new Error( - `PR #${eventPullRequest.number} author changed during workflow-run discovery`, - ); - } - const livePermission = await loadAuthorPermission(author); - if (!livePermission || !hasWritePermission(livePermission)) { - core.warning( - `PR #${eventPullRequest.number} author ${author} no longer has write, maintain, or admin permission; no further runs approved`, - ); - return; - } - - await github.rest.actions.approveWorkflowRun({ - owner, - repo, - run_id: run.id, - }); - approvedRunIds.add(run.id); - core.info( - `Approved pull_request workflow run ${run.id} for PR #${eventPullRequest.number} at ${expectedHeadSha}`, - ); - } - - if (attempt + 1 < POLL_ATTEMPTS) { - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); - } - } - - core.info( - `Approved ${approvedRunIds.size} exact-head workflow run(s) for PR #${eventPullRequest.number}`, - ); + await helper.approveMaintainerPrWorkflowRuns({ github, context, core }); diff --git a/test/maintainer-pr-workflow-approval.test.ts b/test/maintainer-pr-workflow-approval.test.ts index 99f0046c81b..79d6827f2ee 100644 --- a/test/maintainer-pr-workflow-approval.test.ts +++ b/test/maintainer-pr-workflow-approval.test.ts @@ -1,14 +1,10 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { readYaml, type WorkflowJob } from "./helpers/e2e-workflow-contract"; -const AsyncFunction = Object.getPrototypeOf(async () => undefined).constructor as new ( - ...parameters: string[] -) => (...args: unknown[]) => Promise; - type ApprovalWorkflow = { concurrency?: { group?: string; "cancel-in-progress"?: boolean }; on?: { @@ -20,143 +16,23 @@ type ApprovalWorkflow = { jobs: Record; }; -type HarnessOptions = { - author?: string; - eventHead?: string; - headRepository?: string; - liveHeads?: string[]; - permission?: { - permission?: string; - role_name?: string; - user?: { login?: string }; - }; - permissionError?: { status: number }; - runsByPoll?: unknown[][]; -}; - const WORKFLOW_PATH = ".github/workflows/approve-maintainer-pr-workflow-runs.yaml"; -const HEAD_SHA = "a".repeat(40); -const MOVED_HEAD_SHA = "b".repeat(40); -const PR_NUMBER = 42; -const workflow = readYaml(WORKFLOW_PATH); -const job = workflow.jobs.approve; -const actionStep = job.steps?.find( - (step) => step.name === "Approve exact-head maintainer workflow runs", -); -const script = actionStep?.with?.script; - -function actionRequiredRun(id: number, overrides: Record = {}) { - return { - actor: { login: "github-actions[bot]" }, - conclusion: "action_required", - event: "pull_request", - head_repository: { full_name: "NVIDIA/NemoClaw" }, - head_sha: HEAD_SHA, - id, - pull_requests: [{ head: { sha: HEAD_SHA }, number: PR_NUMBER }], - status: "completed", - ...overrides, - }; -} - -function createHarness(options: HarnessOptions = {}) { - const author = options.author ?? "maintainer"; - const liveHeads = options.liveHeads ?? [HEAD_SHA]; - let pullRequestRead = 0; - let workflowRunPoll = 0; - - const getPullRequest = vi.fn(async () => { - const headSha = liveHeads[Math.min(pullRequestRead, liveHeads.length - 1)]; - pullRequestRead += 1; - return { - data: { - base: { repo: { full_name: "NVIDIA/NemoClaw" } }, - head: { - repo: { full_name: options.headRepository ?? "NVIDIA/NemoClaw" }, - sha: headSha, - }, - number: PR_NUMBER, - state: "open", - user: { login: author }, - }, - }; - }); - const permissionResponse = - (options.permission - ? { ...options.permission, user: options.permission.user ?? { login: author } } - : undefined) ?? - ({ - permission: "write", - role_name: "write", - user: { login: author }, - } as const); - const getCollaboratorPermissionLevel = vi.fn( - options.permissionError - ? async () => Promise.reject(options.permissionError) - : async () => ({ data: permissionResponse }), - ); - const listWorkflowRunsForRepo = vi.fn(async () => { - const runs = options.runsByPoll?.[workflowRunPoll] ?? []; - workflowRunPoll += 1; - return { data: { total_count: runs.length, workflow_runs: runs } }; - }); - const approveWorkflowRun = vi.fn().mockResolvedValue({ status: 201 }); - const paginate = vi.fn( - async ( - endpoint: () => Promise<{ data: { workflow_runs: unknown[] } }>, - _parameters: Record, - ) => (await endpoint()).data.workflow_runs, - ); - const info = vi.fn(); - const warning = vi.fn(); - const setTimeout = vi.fn((resolve: () => void, _delay: number) => { - resolve(); - return 0; - }); - - return { - approveWorkflowRun, - context: { - payload: { - pull_request: { - head: { sha: options.eventHead ?? HEAD_SHA }, - number: PR_NUMBER, - }, - }, - repo: { owner: "NVIDIA", repo: "NemoClaw" }, - }, - core: { info, warning }, - getCollaboratorPermissionLevel, - getPullRequest, - github: { - paginate, - rest: { - actions: { approveWorkflowRun, listWorkflowRunsForRepo }, - pulls: { get: getPullRequest }, - repos: { getCollaboratorPermissionLevel }, - }, - }, - info, - listWorkflowRunsForRepo, - paginate, - setTimeout, - warning, - }; -} - -async function runScript(harness: ReturnType): Promise { - expect(script).toEqual(expect.any(String)); - await new AsyncFunction("github", "context", "core", "setTimeout", script as string)( - harness.github, - harness.context, - harness.core, - harness.setTimeout, - ); +const HELPER_PATH = "tools/ci/approve-maintainer-pr-workflow-runs.mts"; +const TRUSTED_CHECKOUT_ACTION = "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"; +const GITHUB_SCRIPT_ACTION = "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3"; + +function requireStep(job: WorkflowJob, name: string) { + const step = job.steps?.find((candidate) => candidate.name === name); + expect(step, `workflow must contain ${name}`).toBeDefined(); + return step!; } describe("maintainer PR workflow-run approval", () => { - // source-shape-contract: security -- The write-capable pull_request_target workflow must keep its exact trigger, permission, action, and no-checkout execution boundary + // source-shape-contract: security -- The actions:write pull_request_target workflow must load only the directly tested helper from its exact trusted base SHA it("keeps workflow approval inside the trusted metadata boundary", () => { + const workflow = readYaml(WORKFLOW_PATH); + const job = workflow.jobs.approve!; + expect(workflow.on?.pull_request_target).toEqual({ types: ["opened", "synchronize", "reopened", "edited", "ready_for_review"], }); @@ -172,180 +48,62 @@ describe("maintainer PR workflow-run approval", () => { }); expect(job.if).toBe("${{ github.repository == 'NVIDIA/NemoClaw' }}"); expect(job["timeout-minutes"]).toBe(2); - expect(job.steps).toHaveLength(1); - expect(actionStep?.uses).toMatch(/^actions\/github-script@[0-9a-f]{40}$/u); - expect(job.steps?.some((step) => step.uses?.startsWith("actions/checkout@"))).toBe(false); - expect(job.steps?.some((step) => typeof step.run === "string")).toBe(false); - expect(script).not.toContain("github.rest.repos.getContent"); - expect(script).not.toContain("github.rest.git"); - expect(script).not.toContain("require("); - }); - - it.each([ - ["write", { permission: "write", role_name: "custom-write" }], - ["maintain", { permission: "write", role_name: "maintain" }], - ["admin", { permission: "admin", role_name: "admin" }], - ])("approves an exact same-repository bot-restack run for a PR author with %s permission", async (_name, role) => { - const harness = createHarness({ - permission: { ...role, user: { login: "MAINTAINER" } }, - runsByPoll: [[actionRequiredRun(101)]], - }); - - await runScript(harness); - - expect(harness.getCollaboratorPermissionLevel).toHaveBeenCalledWith({ - owner: "NVIDIA", - repo: "NemoClaw", - username: "maintainer", - }); - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.approveWorkflowRun).toHaveBeenCalledWith({ - owner: "NVIDIA", - repo: "NemoClaw", - run_id: 101, - }); - }); - - it("polls boundedly and approves exact-head runs that appear asynchronously", async () => { - const firstRun = actionRequiredRun(101); - const secondRun = actionRequiredRun(102); - const harness = createHarness({ - runsByPoll: [[], [firstRun], [firstRun], [firstRun, secondRun]], - }); - - await runScript(harness); - - expect(harness.listWorkflowRunsForRepo).toHaveBeenCalledTimes(12); - expect(harness.paginate).toHaveBeenCalledWith(harness.listWorkflowRunsForRepo, { - event: "pull_request", - head_sha: HEAD_SHA, - owner: "NVIDIA", - per_page: 100, - repo: "NemoClaw", - status: "action_required", - }); - expect(harness.setTimeout).toHaveBeenCalledTimes(11); - expect(harness.setTimeout).toHaveBeenCalledWith(expect.any(Function), 5000); - expect(harness.approveWorkflowRun.mock.calls.map(([input]) => input.run_id)).toEqual([ - 101, 102, + expect(job.steps?.map((step) => step.name)).toEqual([ + "Validate trusted helper revision", + "Check out trusted approval helper", + "Verify trusted approval helper", + "Approve exact-head maintainer workflow runs", ]); - }); - it.each([ - ["read permission", { permission: { permission: "read", role_name: "maintain" } }], - ["no collaborator record", { permissionError: { status: 404 } }], - ])("leaves an external author's runs gated for %s", async (_name, options) => { - const harness = createHarness({ - author: "external-contributor", - ...options, - runsByPoll: [[actionRequiredRun(101)]], + const validate = requireStep(job, "Validate trusted helper revision"); + expect((validate as { shell?: string }).shell).toBe("bash"); + expect(validate.env).toEqual({ + TRUSTED_HELPER_ROOT: "${{ github.workspace }}/.trusted-maintainer-approval", + TRUSTED_HELPER_SHA: "${{ github.event.pull_request.base.sha }}", }); - - await runScript(harness); - - expect(harness.listWorkflowRunsForRepo).not.toHaveBeenCalled(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - expect(harness.info).toHaveBeenCalledWith( - expect.stringContaining("workflow runs remain gated"), + expect(validate.run).toContain('[[ "$TRUSTED_HELPER_SHA" =~ ^[a-f0-9]{40}$ ]]'); + expect(validate.run).toContain( + '[[ ! -e "$TRUSTED_HELPER_ROOT" && ! -L "$TRUSTED_HELPER_ROOT" ]]', ); - }); - - it("does not approve from a stale pull_request_target event", async () => { - const harness = createHarness({ liveHeads: [MOVED_HEAD_SHA] }); - - await runScript(harness); - - expect(harness.getCollaboratorPermissionLevel).not.toHaveBeenCalled(); - expect(harness.listWorkflowRunsForRepo).not.toHaveBeenCalled(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - }); - it("leaves a write-author PR gated when an external repository controls its head", async () => { - const harness = createHarness({ - headRepository: "external-contributor/NemoClaw", - runsByPoll: [[actionRequiredRun(101)]], + const checkout = requireStep(job, "Check out trusted approval helper"); + expect(checkout.uses).toBe(TRUSTED_CHECKOUT_ACTION); + expect(checkout.with).toMatchObject({ + repository: "NVIDIA/NemoClaw", + ref: "${{ github.event.pull_request.base.sha }}", + path: ".trusted-maintainer-approval", + "sparse-checkout-cone-mode": "false", + "persist-credentials": "false", + "fetch-depth": "1", }); + expect(String(checkout.with?.["sparse-checkout"] ?? "").trim()).toBe(HELPER_PATH); + expect(job.steps?.filter((step) => step.uses?.startsWith("actions/checkout@"))).toEqual([ + checkout, + ]); - await runScript(harness); - - expect(harness.getCollaboratorPermissionLevel).not.toHaveBeenCalled(); - expect(harness.listWorkflowRunsForRepo).not.toHaveBeenCalled(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - expect(harness.info).toHaveBeenCalledWith( - expect.stringContaining("head repository is not NVIDIA/NemoClaw"), - ); - }); - - it("stops when the live PR head changes before approval", async () => { - const harness = createHarness({ - liveHeads: [HEAD_SHA, HEAD_SHA, MOVED_HEAD_SHA], - runsByPoll: [[actionRequiredRun(101)]], - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("head changed before workflow-run approval"), - ); - }); - - it("stops when the PR author loses write permission before approval", async () => { - const harness = createHarness({ runsByPoll: [[actionRequiredRun(101)]] }); - harness.getCollaboratorPermissionLevel - .mockResolvedValueOnce({ - data: { permission: "write", role_name: "write", user: { login: "maintainer" } }, - }) - .mockResolvedValueOnce({ - data: { permission: "read", role_name: "triage", user: { login: "maintainer" } }, - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("no longer has write, maintain, or admin permission"), - ); - }); - - it("ignores runs that do not bind the exact PR number and head SHA", async () => { - const harness = createHarness({ - runsByPoll: [ - [ - actionRequiredRun(101, { - pull_requests: [{ head: { sha: HEAD_SHA }, number: PR_NUMBER + 1 }], - }), - actionRequiredRun(102, { - pull_requests: [{ head: { sha: MOVED_HEAD_SHA }, number: PR_NUMBER }], - }), - actionRequiredRun(103, { head_sha: MOVED_HEAD_SHA }), - actionRequiredRun(104, { conclusion: "success" }), - actionRequiredRun(105, { event: "workflow_dispatch" }), - actionRequiredRun(106, { - head_repository: { full_name: "external-contributor/NemoClaw" }, - }), - ], - ], + const verify = requireStep(job, "Verify trusted approval helper"); + expect((verify as { shell?: string }).shell).toBe("bash"); + expect(verify.env).toEqual({ + TRUSTED_HELPER_RELATIVE_PATH: HELPER_PATH, + TRUSTED_HELPER_ROOT: "${{ github.workspace }}/.trusted-maintainer-approval", + TRUSTED_HELPER_SHA: "${{ github.event.pull_request.base.sha }}", }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - }); - - it("rejects a permission response for a different user", async () => { - const harness = createHarness({ - permission: { - permission: "admin", - role_name: "admin", - user: { login: "different-user" }, - }, + expect(verify.run).toContain('git -C "$TRUSTED_HELPER_ROOT" rev-parse --verify HEAD'); + expect(verify.run).toContain('[[ -f "$helper" && ! -L "$helper" ]]'); + + const execute = requireStep(job, "Approve exact-head maintainer workflow runs"); + expect(execute.uses).toBe(GITHUB_SCRIPT_ACTION); + expect(execute.env).toEqual({ + TRUSTED_HELPER_PATH: + "${{ github.workspace }}/.trusted-maintainer-approval/tools/ci/approve-maintainer-pr-workflow-runs.mts", + TRUSTED_HELPER_SHA: "${{ github.event.pull_request.base.sha }}", }); - - await expect(runScript(harness)).rejects.toThrow( - "Permission response did not match PR author maintainer", - ); - expect(harness.listWorkflowRunsForRepo).not.toHaveBeenCalled(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); + expect(execute.with?.["github-token"]).toBe("${{ github.token }}"); + const script = String(execute.with?.script ?? ""); + expect(script).toContain("pathToFileURL(helperPath)"); + expect(script).toContain("approveMaintainerPrWorkflowRuns"); + expect(script).toContain("{ github, context, core }"); + expect(script).not.toContain("github.rest."); + expect(script).not.toContain("pull_request.head.sha"); }); });