diff --git a/.github/workflows/approve-maintainer-pr-workflow-runs.yaml b/.github/workflows/approve-maintainer-pr-workflow-runs.yaml deleted file mode 100644 index 4550af557e1..00000000000 --- a/.github/workflows/approve-maintainer-pr-workflow-runs.yaml +++ /dev/null @@ -1,103 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: Automation / Approve Maintainer PR Workflow Runs - -# 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. - pull_request_target: - types: [opened, synchronize, reopened, edited, ready_for_review] - -permissions: - actions: write - contents: read - pull-requests: read - -concurrency: - group: approve-maintainer-pr-workflow-runs-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }} - cancel-in-progress: false - -jobs: - approve: - if: ${{ github.repository == 'NVIDIA/NemoClaw' }} - 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: | - 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'); - } - 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'); - } - await helper.approveMaintainerPrWorkflowRuns({ github, context, core }); diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index c3471faee7b..207bc5adf87 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -326,11 +326,6 @@ "test": "keeps fork-safe labeling inside the trusted metadata boundary", "category": "security" }, - { - "file": "test/maintainer-pr-workflow-approval.test.ts", - "test": "keeps workflow approval inside the trusted metadata boundary", - "category": "security" - }, { "file": "test/macos-e2e-workflow-boundary.test.ts", "test": "keeps secret-bearing live E2E on trusted main-branch code", diff --git a/test/helpers/vitest-watch-triggers.ts b/test/helpers/vitest-watch-triggers.ts index 15f541961d7..183030b5e75 100644 --- a/test/helpers/vitest-watch-triggers.ts +++ b/test/helpers/vitest-watch-triggers.ts @@ -128,10 +128,6 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [ pattern: /(?:^|\/)\.github\/workflows\/code-scanning\.yaml$/, testsToRun: runTests("test/code-scanning-workflow.test.ts"), }, - { - pattern: /(?:^|\/)\.github\/workflows\/approve-maintainer-pr-workflow-runs\.yaml$/, - testsToRun: runTests("test/maintainer-pr-workflow-approval.test.ts"), - }, { pattern: /(?:^|\/)\.github\/workflows\/pr-merge-conflict-fixer\.yaml$/, testsToRun: runTests("test/pr-merge-conflict-fixer-workflow-boundary.test.ts"), diff --git a/test/maintainer-pr-workflow-approval-helper.test.ts b/test/maintainer-pr-workflow-approval-helper.test.ts deleted file mode 100644 index 5f494161e5d..00000000000 --- a/test/maintainer-pr-workflow-approval-helper.test.ts +++ /dev/null @@ -1,661 +0,0 @@ -// 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 { approveMaintainerPrWorkflowRuns } from "../tools/ci/approve-maintainer-pr-workflow-runs.mts"; - -type ApiFailure = { - code?: string; - status?: number; -}; - -type ApiRequestInput = { - request?: { signal?: AbortSignal }; -}; - -type WorkflowRunListInput = ApiRequestInput & { - event: string; - head_sha: string; - owner: string; - page: number; - per_page: number; - repo: string; - status: string; -}; - -type HarnessOptions = { - abortSignalsImmediately?: boolean; - approvalErrors?: ApiFailure[]; - author?: string; - dateNowValues?: number[]; - eventHead?: string; - headRepository?: string; - hungPullRequestAttempts?: number; - liveHeads?: string[]; - permission?: { - permission?: string; - role_name?: string; - user?: { login?: string }; - }; - permissionError?: { status: number }; - pullRequestErrors?: ApiFailure[]; - runsByPoll?: unknown[][]; - workflowRunGetErrors?: ApiFailure[]; - workflowRunsByGet?: unknown[]; - workflowRunErrors?: ApiFailure[]; -}; - -const HEAD_SHA = "a".repeat(40); -const MOVED_HEAD_SHA = "b".repeat(40); -const PR_NUMBER = 42; - -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]; - const approvalErrors = [...(options.approvalErrors ?? [])]; - const pullRequestErrors = [...(options.pullRequestErrors ?? [])]; - const workflowRunGetErrors = [...(options.workflowRunGetErrors ?? [])]; - const workflowRunErrors = [...(options.workflowRunErrors ?? [])]; - const dateNowValues = options.dateNowValues ?? [0]; - let dateNowRead = 0; - let hungPullRequestAttempts = options.hungPullRequestAttempts ?? 0; - let pullRequestRead = 0; - let workflowRunRead = 0; - let workflowRunPoll = 0; - - const dateNow = vi.fn( - () => dateNowValues[Math.min(dateNowRead++, dateNowValues.length - 1)] ?? 0, - ); - const abortSignalTimeout = vi.fn((_delayMs: number) => { - const controller = new AbortController(); - options.abortSignalsImmediately ? queueMicrotask(() => controller.abort()) : undefined; - return controller.signal; - }); - const abortSignal = { timeout: abortSignalTimeout }; - - const getPullRequest = vi.fn(async (input: ApiRequestInput) => { - const hungRequest = - hungPullRequestAttempts > 0 - ? new Promise((_resolve, reject) => { - const signal = input.request?.signal; - const rejectRequest = () => - reject( - Object.assign(new Error("request aborted"), { code: "ETIMEDOUT", status: 500 }), - ); - signal?.addEventListener("abort", rejectRequest, { once: true }); - signal?.aborted ? rejectRequest() : undefined; - }) - : Promise.resolve(); - hungPullRequestAttempts = Math.max(0, hungPullRequestAttempts - 1); - await hungRequest; - const failure = pullRequestErrors.shift(); - await (failure ? Promise.reject(failure) : Promise.resolve()); - 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 (_input: WorkflowRunListInput) => { - const failure = workflowRunErrors.shift(); - await (failure ? Promise.reject(failure) : Promise.resolve()); - const runs = options.runsByPoll?.[workflowRunPoll] ?? []; - workflowRunPoll += 1; - return { data: { total_count: runs.length, workflow_runs: runs } }; - }); - const approveWorkflowRun = vi.fn( - async (_input: ApiRequestInput & { owner: string; repo: string; run_id: number }) => { - const failure = approvalErrors.shift(); - await (failure ? Promise.reject(failure) : Promise.resolve()); - return { status: 201 }; - }, - ); - const getWorkflowRun = vi.fn(async ({ run_id: runId }: { run_id: number }) => { - const failure = workflowRunGetErrors.shift(); - await (failure ? Promise.reject(failure) : Promise.resolve()); - const configuredRuns = options.workflowRunsByGet ?? []; - const run = - configuredRuns[Math.min(workflowRunRead, configuredRuns.length - 1)] ?? - actionRequiredRun(runId); - workflowRunRead += 1; - return { data: run }; - }); - const info = vi.fn(); - const warning = vi.fn(); - const setTimeout = vi.fn((resolve: () => void, _delay: number) => { - resolve(); - return 0; - }); - - return { - abortSignal, - abortSignalTimeout, - approveWorkflowRun, - context: { - payload: { - pull_request: { - head: { sha: options.eventHead ?? HEAD_SHA }, - number: PR_NUMBER, - }, - }, - repo: { owner: "NVIDIA", repo: "NemoClaw" }, - }, - core: { info, warning }, - dateNow, - getCollaboratorPermissionLevel, - getPullRequest, - getWorkflowRun, - github: { - rest: { - actions: { approveWorkflowRun, getWorkflowRun, listWorkflowRunsForRepo }, - pulls: { get: getPullRequest }, - repos: { getCollaboratorPermissionLevel }, - }, - }, - info, - listWorkflowRunsForRepo, - setTimeout, - warning, - }; -} - -async function runScript(harness: ReturnType): Promise { - await approveMaintainerPrWorkflowRuns({ - github: harness.github, - context: harness.context, - core: harness.core, - now: harness.dateNow, - createTimeoutSignal: harness.abortSignal.timeout, - sleep: (delayMs) => - new Promise((resolve) => { - harness.setTimeout(resolve, delayMs); - }), - }); -} - -describe("maintainer PR workflow-run approval", () => { - it.each([ - ["write", { permission: "write", role_name: "custom-write" }], - ["maintain", { permission: "write", role_name: "maintain" }], - ["admin", { permission: "admin", role_name: "admin" }], - ])("approves an exact-head run from the target repository 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", - request: { signal: expect.anything() }, - username: "maintainer", - }); - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.approveWorkflowRun).toHaveBeenCalledWith({ - owner: "NVIDIA", - repo: "NemoClaw", - request: { signal: expect.anything() }, - run_id: 101, - }); - }); - - it("stops polling after the attempt limit and approves exact-head runs that appear later", 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.listWorkflowRunsForRepo).toHaveBeenCalledWith({ - event: "pull_request", - head_sha: HEAD_SHA, - owner: "NVIDIA", - page: 1, - per_page: 100, - request: { signal: expect.anything() }, - 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, - ]); - }); - - it("uses a fresh timeout signal for each workflow-run page request", async () => { - const firstPage = Array.from({ length: 100 }, (_value, index) => - actionRequiredRun(1_000 + index, { head_sha: MOVED_HEAD_SHA }), - ); - const harness = createHarness({ - runsByPoll: [firstPage, [actionRequiredRun(101)]], - }); - - await runScript(harness); - - const firstRequest = harness.listWorkflowRunsForRepo.mock.calls[0]?.[0]; - const secondRequest = harness.listWorkflowRunsForRepo.mock.calls[1]?.[0]; - expect(firstRequest).toMatchObject({ page: 1, request: { signal: expect.anything() } }); - expect(secondRequest).toMatchObject({ page: 2, request: { signal: expect.anything() } }); - expect(firstRequest?.request?.signal).not.toBe(secondRequest?.request?.signal); - expect(harness.approveWorkflowRun).toHaveBeenCalledWith({ - owner: "NVIDIA", - repo: "NemoClaw", - request: { signal: expect.anything() }, - run_id: 101, - }); - }); - - it("reports completed approvals when the polling budget ends", async () => { - const harness = createHarness({ - dateNowValues: [0, 0, 0, 0, 0, 101_000], - runsByPoll: [[actionRequiredRun(101)]], - }); - - await runScript(harness); - - expect(harness.listWorkflowRunsForRepo).toHaveBeenCalledOnce(); - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.setTimeout).not.toHaveBeenCalled(); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("Workflow-run polling stopped after 1/12 attempts"), - ); - expect(harness.info).toHaveBeenCalledWith( - "Exact-head workflow runs that no longer require approval for PR #42: 1", - ); - }); - - it("retries pulls.get after a transient failure before trusting live PR metadata", async () => { - const harness = createHarness({ - pullRequestErrors: [{ status: 504 }], - runsByPoll: [[actionRequiredRun(101)]], - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("Load live PR #42 failed transiently with HTTP 504"), - ); - expect(harness.setTimeout).toHaveBeenCalledWith(expect.any(Function), 250); - }); - - it("aborts and retries a hung API request with a fresh bounded signal", async () => { - const harness = createHarness({ - abortSignalsImmediately: true, - hungPullRequestAttempts: 3, - }); - - await expect(runScript(harness)).rejects.toMatchObject({ - code: "ETIMEDOUT", - status: 500, - }); - - expect(harness.getPullRequest).toHaveBeenCalledTimes(3); - expect(harness.abortSignalTimeout).toHaveBeenCalledTimes(3); - expect(harness.abortSignalTimeout).toHaveBeenNthCalledWith(1, 10_000); - expect(harness.abortSignalTimeout).toHaveBeenNthCalledWith(2, 10_000); - expect(harness.abortSignalTimeout).toHaveBeenNthCalledWith(3, 10_000); - const requestSignals = harness.getPullRequest.mock.calls.map( - ([input]) => input.request?.signal, - ); - expect(new Set(requestSignals).size).toBe(3); - }); - - it("bounds an aborting request by the remaining script budget", async () => { - const harness = createHarness({ - abortSignalsImmediately: true, - dateNowValues: [0, 104_500, 105_000], - hungPullRequestAttempts: 1, - }); - - await expect(runScript(harness)).rejects.toThrow( - "Load live PR #42 exceeded the bounded 105000ms script budget", - ); - - expect(harness.getPullRequest).toHaveBeenCalledOnce(); - expect(harness.abortSignalTimeout).toHaveBeenCalledOnce(); - expect(harness.abortSignalTimeout).toHaveBeenCalledWith(500); - }); - - it("retries transient workflow-run listing failures with bounded backoff", async () => { - const harness = createHarness({ - runsByPoll: [[actionRequiredRun(101)]], - workflowRunErrors: [{ status: 502 }, { status: 504 }], - }); - - await runScript(harness); - - expect(harness.listWorkflowRunsForRepo).toHaveBeenCalledTimes(14); - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.setTimeout).toHaveBeenCalledWith(expect.any(Function), 250); - expect(harness.setTimeout).toHaveBeenCalledWith(expect.any(Function), 500); - }); - - it("revalidates exact-head authority before retrying a transient approval failure", async () => { - const harness = createHarness({ - approvalErrors: [{ status: 504 }], - runsByPoll: [[actionRequiredRun(101)]], - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).toHaveBeenCalledTimes(2); - expect(harness.getCollaboratorPermissionLevel).toHaveBeenCalledTimes(3); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("Approve workflow run 101 returned HTTP 504"), - ); - expect(harness.setTimeout).toHaveBeenCalledWith(expect.any(Function), 250); - }); - - it("retries after HTTP 504 and records success when the second recheck no longer requires approval", async () => { - const harness = createHarness({ - approvalErrors: [{ status: 504 }, { status: 403 }], - runsByPoll: [[actionRequiredRun(101)]], - workflowRunsByGet: [ - actionRequiredRun(101), - actionRequiredRun(101, { conclusion: null, status: "queued" }), - ], - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).toHaveBeenCalledTimes(2); - expect(harness.getWorkflowRun).toHaveBeenCalledTimes(2); - expect(harness.getWorkflowRun).toHaveBeenLastCalledWith({ - owner: "NVIDIA", - repo: "NemoClaw", - request: { signal: expect.anything() }, - run_id: 101, - }); - expect(harness.info).toHaveBeenCalledWith( - expect.stringContaining("no longer requires approval after an ambiguous approval response"), - ); - expect(harness.info).not.toHaveBeenCalledWith( - expect.stringContaining("Approved pull_request workflow run 101"), - ); - expect(harness.info).toHaveBeenCalledWith( - "Exact-head workflow runs that no longer require approval for PR #42: 1", - ); - }); - - it.each([ - 403, 404, - ])("records success without another approval request after HTTP %i when the exact run no longer requires approval", async (status) => { - const harness = createHarness({ - approvalErrors: [{ status }], - runsByPoll: [[actionRequiredRun(101)]], - workflowRunsByGet: [actionRequiredRun(101, { conclusion: null, status: "queued" })], - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.getWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.info).toHaveBeenCalledWith( - expect.stringContaining("no longer requires approval after an ambiguous approval response"), - ); - expect(harness.info).toHaveBeenCalledWith( - "Exact-head workflow runs that no longer require approval for PR #42: 1", - ); - }); - - it("fails closed after exhausting ambiguous approval retries", async () => { - const finalFailure = { status: 504 }; - const harness = createHarness({ - approvalErrors: [{ status: 504 }, { status: 504 }, finalFailure], - runsByPoll: [[actionRequiredRun(101)]], - }); - - await expect(runScript(harness)).rejects.toBe(finalFailure); - - expect(harness.approveWorkflowRun).toHaveBeenCalledTimes(3); - expect(harness.getWorkflowRun).toHaveBeenCalledTimes(3); - expect(harness.getCollaboratorPermissionLevel).toHaveBeenCalledTimes(4); - }); - - it("fails closed when exact-run reconciliation cannot be read", async () => { - const recheckFailure = { status: 403 }; - const harness = createHarness({ - approvalErrors: [{ status: 504 }], - runsByPoll: [[actionRequiredRun(101)]], - workflowRunGetErrors: [recheckFailure], - }); - - await expect(runScript(harness)).rejects.toBe(recheckFailure); - - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.getWorkflowRun).toHaveBeenCalledOnce(); - }); - - it("fails closed when an ambiguous approval recheck returns another run identity", async () => { - const harness = createHarness({ - approvalErrors: [{ status: 504 }], - runsByPoll: [[actionRequiredRun(101)]], - workflowRunsByGet: [actionRequiredRun(101, { head_sha: MOVED_HEAD_SHA })], - }); - - await expect(runScript(harness)).rejects.toThrow( - `Workflow run 101 no longer matches exact PR #${PR_NUMBER} at ${HEAD_SHA}`, - ); - - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.getWorkflowRun).toHaveBeenCalledOnce(); - }); - - it("abandons an approval retry when the PR head changes during backoff", async () => { - const harness = createHarness({ - approvalErrors: [{ status: 504 }], - liveHeads: [HEAD_SHA, HEAD_SHA, HEAD_SHA, MOVED_HEAD_SHA], - runsByPoll: [[actionRequiredRun(101)]], - }); - - await runScript(harness); - - expect(harness.approveWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("head changed before workflow-run approval"), - ); - expect(harness.info).not.toHaveBeenCalledWith( - expect.stringContaining("Approved pull_request workflow run 101"), - ); - }); - - it("abandons an approval retry when the author loses permission", async () => { - const harness = createHarness({ - approvalErrors: [{ status: 504 }], - runsByPoll: [[actionRequiredRun(101)]], - }); - harness.getCollaboratorPermissionLevel - .mockResolvedValueOnce({ - data: { permission: "write", role_name: "write", user: { login: "maintainer" } }, - }) - .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).toHaveBeenCalledOnce(); - expect(harness.getWorkflowRun).toHaveBeenCalledOnce(); - expect(harness.warning).toHaveBeenCalledWith( - expect.stringContaining("no longer has write, maintain, or admin permission"), - ); - }); - - it("does not retry a non-transient GitHub API failure", async () => { - const failure = { status: 403 }; - const harness = createHarness({ workflowRunErrors: [failure] }); - - await expect(runScript(harness)).rejects.toBe(failure); - - expect(harness.listWorkflowRunsForRepo).toHaveBeenCalledOnce(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - expect(harness.setTimeout).not.toHaveBeenCalled(); - expect(harness.warning).not.toHaveBeenCalledWith(expect.stringContaining("failed transiently")); - }); - - 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)]], - }); - - await runScript(harness); - - expect(harness.listWorkflowRunsForRepo).not.toHaveBeenCalled(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - expect(harness.info).toHaveBeenCalledWith( - expect.stringContaining("workflow runs remain gated"), - ); - }); - - 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)]], - }); - - 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" }, - }), - ], - ], - }); - - 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" }, - }, - }); - - await expect(runScript(harness)).rejects.toThrow( - "Permission response did not match PR author maintainer", - ); - expect(harness.listWorkflowRunsForRepo).not.toHaveBeenCalled(); - expect(harness.approveWorkflowRun).not.toHaveBeenCalled(); - }); -}); diff --git a/test/maintainer-pr-workflow-approval.test.ts b/test/maintainer-pr-workflow-approval.test.ts deleted file mode 100644 index 79d6827f2ee..00000000000 --- a/test/maintainer-pr-workflow-approval.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { describe, expect, it } from "vitest"; - -import { readYaml, type WorkflowJob } from "./helpers/e2e-workflow-contract"; - -type ApprovalWorkflow = { - concurrency?: { group?: string; "cancel-in-progress"?: boolean }; - on?: { - pull_request_target?: { - types?: string[]; - }; - }; - permissions?: Record; - jobs: Record; -}; - -const WORKFLOW_PATH = ".github/workflows/approve-maintainer-pr-workflow-runs.yaml"; -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 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"], - }); - expect(workflow.permissions).toEqual({ - actions: "write", - contents: "read", - "pull-requests": "read", - }); - expect(workflow.concurrency).toEqual({ - group: - "approve-maintainer-pr-workflow-runs-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}", - "cancel-in-progress": false, - }); - expect(job.if).toBe("${{ github.repository == 'NVIDIA/NemoClaw' }}"); - expect(job["timeout-minutes"]).toBe(2); - 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", - ]); - - 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 }}", - }); - expect(validate.run).toContain('[[ "$TRUSTED_HELPER_SHA" =~ ^[a-f0-9]{40}$ ]]'); - expect(validate.run).toContain( - '[[ ! -e "$TRUSTED_HELPER_ROOT" && ! -L "$TRUSTED_HELPER_ROOT" ]]', - ); - - 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, - ]); - - 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 }}", - }); - 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 }}", - }); - 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"); - }); -}); diff --git a/test/vitest-watch-triggers.test.ts b/test/vitest-watch-triggers.test.ts index e2cf4fba25a..bc50f45205b 100644 --- a/test/vitest-watch-triggers.test.ts +++ b/test/vitest-watch-triggers.test.ts @@ -69,7 +69,6 @@ const OPAQUE_INPUTS = [ "test/e2e/docs/parity-inventory.generated.json", ".github/workflows/e2e.yaml", ".github/workflows/code-scanning.yaml", - ".github/workflows/approve-maintainer-pr-workflow-runs.yaml", ".github/workflows/pr-review-advisor.yaml", "tools/pr-review-advisor/openshell-policy.yaml", ".github/workflows/hosted-runner-recovery.yaml", @@ -154,9 +153,6 @@ describe("Vitest opaque-input watch triggers", () => { expect(triggeredBy(".github/workflows/code-scanning.yaml")).toEqual([ "test/code-scanning-workflow.test.ts", ]); - expect(triggeredBy(".github/workflows/approve-maintainer-pr-workflow-runs.yaml")).toEqual([ - "test/maintainer-pr-workflow-approval.test.ts", - ]); expect(triggeredBy(".github/workflows/pr-review-advisor.yaml")).toEqual([ "test/pr-review-advisor-workflow-boundary.test.ts", "test/pr-review-advisor-openshell-workflow-boundary.test.ts", diff --git a/tools/ci/approve-maintainer-pr-workflow-runs.mts b/tools/ci/approve-maintainer-pr-workflow-runs.mts deleted file mode 100644 index 4fef68733b3..00000000000 --- a/tools/ci/approve-maintainer-pr-workflow-runs.mts +++ /dev/null @@ -1,528 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export type MaintainerApprovalRuntime = { - github: unknown; - context: unknown; - core: unknown; - now?: () => number; - sleep?: (delayMs: number) => Promise; - createTimeoutSignal?: (delayMs: number) => AbortSignal; -}; - -type RequestInput = { request?: { signal?: AbortSignal } }; -type ApiError = { code?: unknown; status?: unknown; response?: { status?: unknown } }; -type EventPullRequest = { number?: unknown; head?: { sha?: unknown } }; -type ApprovalResult = { approved: boolean; noOp?: boolean; stop: boolean }; - -type PullRequest = { - number?: number; - state?: string; - base?: { repo?: { full_name?: string } }; - head?: { repo?: { full_name?: string }; sha?: string }; - user?: { login?: string }; -}; -type Permission = { - permission?: string; - role_name?: string; - user?: { login?: string }; -}; -type WorkflowRun = { - id?: number; - event?: string; - head_sha?: string; - head_repository?: { full_name?: string }; - pull_requests?: Array<{ number?: number; head?: { sha?: string } }>; - status?: string; - conclusion?: string | null; -}; -type GitHubClient = { - rest: { - pulls: { - get( - input: RequestInput & { owner: string; repo: string; pull_number: number }, - ): Promise<{ data: PullRequest }>; - }; - repos: { - getCollaboratorPermissionLevel( - input: RequestInput & { owner: string; repo: string; username: string }, - ): Promise<{ data: Permission }>; - }; - actions: { - approveWorkflowRun( - input: RequestInput & { owner: string; repo: string; run_id: number }, - ): Promise; - getWorkflowRun( - input: RequestInput & { owner: string; repo: string; run_id: number }, - ): Promise<{ data: WorkflowRun }>; - listWorkflowRunsForRepo( - input: RequestInput & { - owner: string; - repo: string; - event: string; - head_sha: string; - status: string; - page: number; - per_page: number; - }, - ): Promise<{ data: { workflow_runs?: WorkflowRun[] } }>; - }; - }; -}; -type WorkflowContext = { - repo: { owner: string; repo: string }; - payload: { pull_request?: unknown }; -}; -type WorkflowCore = { - info(message: string): void; - warning(message: string): void; -}; - -export async function approveMaintainerPrWorkflowRuns( - runtime: MaintainerApprovalRuntime, -): Promise { - const github = runtime.github as GitHubClient; - const context = runtime.context as WorkflowContext; - const core = runtime.core as WorkflowCore; - const now = runtime.now ?? Date.now; - const sleep = - runtime.sleep ?? - ((delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs))); - const createTimeoutSignal = - runtime.createTimeoutSignal ?? ((delayMs: number) => AbortSignal.timeout(delayMs)); - - // A workflow with `actions: write` must load this helper from the PR base SHA or another trusted commit SHA. - // Loading it from the PR head SHA would execute untrusted code with GitHub Actions write access. - 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 API_RETRY_ATTEMPTS = 3; - const API_RETRY_BASE_DELAY_MS = 250; - const API_RETRY_MAX_DELAY_MS = 1000; - const API_REQUEST_TIMEOUT_MS = 10000; - const SCRIPT_BUDGET_MS = 105000; - const RETRYABLE_HTTP_STATUSES = new Set([408, 429, 500, 502, 503, 504]); - // Treat 403, 404, 409, and 422 as ambiguous approval responses. - // Record success only when an exact-run GET shows that approval is no longer required. - const APPROVAL_POST_STATE_HTTP_STATUSES = new Set([403, 404, 409, 422]); - const RETRYABLE_NETWORK_CODES = new Set(["EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"]); - const { owner, repo } = context.repo; - const scriptDeadlineMs = now() + SCRIPT_BUDGET_MS; - - function remainingScriptBudgetMs(): number { - return Math.max(0, scriptDeadlineMs - now()); - } - - function requestSignalFor(label: string): AbortSignal { - const remainingMs = remainingScriptBudgetMs(); - if (remainingMs <= 0) { - throw new Error(`${label} exceeded the bounded ${SCRIPT_BUDGET_MS}ms script budget`); - } - // @octokit/request 10.x consumes request.signal, not the legacy - // request.timeout option. A fresh signal is required per retry. - return createTimeoutSignal(Math.min(API_REQUEST_TIMEOUT_MS, remainingMs)); - } - - async function waitWithinScriptBudget(label: string, delayMs: number): Promise { - if (remainingScriptBudgetMs() <= delayMs) { - throw new Error(`${label} exceeded the bounded ${SCRIPT_BUDGET_MS}ms script budget`); - } - await sleep(delayMs); - } - - function apiErrorStatus(error: unknown): number | null { - const apiError = error as ApiError; - const status = Number(apiError.status ?? apiError.response?.status); - return Number.isInteger(status) ? status : null; - } - - function apiErrorCode(error: unknown): string { - const code = (error as ApiError).code; - return typeof code === "string" ? code.toUpperCase() : ""; - } - - function isRetryableApiError(error: unknown): boolean { - const status = apiErrorStatus(error); - return ( - (status !== null && RETRYABLE_HTTP_STATUSES.has(status)) || - RETRYABLE_NETWORK_CODES.has(apiErrorCode(error)) - ); - } - - async function withTransientApiRetry(label: string, operation: () => Promise): Promise { - for (let attempt = 1; attempt <= API_RETRY_ATTEMPTS; attempt += 1) { - try { - return await operation(); - } catch (error) { - if (!isRetryableApiError(error) || attempt === API_RETRY_ATTEMPTS) { - throw error; - } - const delayMs = Math.min( - API_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), - API_RETRY_MAX_DELAY_MS, - ); - const status = apiErrorStatus(error); - const identity = status === null ? apiErrorCode(error) : `HTTP ${status}`; - core.warning( - `${label} failed transiently with ${identity}; retrying attempt ${attempt + 1}/${API_RETRY_ATTEMPTS} in ${delayMs}ms`, - ); - await waitWithinScriptBudget(label, delayMs); - } - } - throw new Error(`${label} exhausted its bounded retry loop`); - } - - function validateEventPullRequest(value: unknown): { number: number; headSha: string } { - if (!value || typeof value !== "object") { - throw new Error("Invalid pull_request_target payload: pull_request is missing"); - } - const pullRequest = value as EventPullRequest; - if ( - typeof pullRequest.number !== "number" || - !Number.isInteger(pullRequest.number) || - pullRequest.number <= 0 - ) { - throw new Error(`Invalid pull request number: ${pullRequest.number}`); - } - if (typeof pullRequest.head?.sha !== "string" || !SHA_PATTERN.test(pullRequest.head.sha)) { - throw new Error(`Invalid event head SHA for PR #${pullRequest.number}`); - } - return { number: pullRequest.number, headSha: pullRequest.head.sha.toLowerCase() }; - } - - function liveHeadSha(pullRequest: PullRequest, prNumber: number): string { - 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: unknown): string { - return typeof value === "string" ? value.toLowerCase() : ""; - } - - function isSameRepositoryHead(pullRequest: PullRequest): boolean { - return ( - repositoryName(pullRequest?.head?.repo?.full_name) === repositoryName(`${owner}/${repo}`) - ); - } - - async function loadLivePullRequest(prNumber: number): Promise { - const response = await withTransientApiRetry(`Load live PR #${prNumber}`, () => - github.rest.pulls.get({ - owner, - repo, - pull_number: prNumber, - request: { signal: requestSignalFor(`Load live PR #${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: string): Promise { - try { - const response = await withTransientApiRetry( - `Load collaborator permission for ${author}`, - () => - github.rest.repos.getCollaboratorPermissionLevel({ - owner, - repo, - username: author, - request: { - signal: requestSignalFor(`Load collaborator permission for ${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 (apiErrorStatus(error) === 404) return null; - throw error; - } - } - - function hasWritePermission(permission: Permission | null): boolean { - const basePermission = String(permission?.permission ?? "").toLowerCase(); - // GitHub maps maintain to the write base permission. - // Do not use role_name as authorization evidence because it can contain a custom-role label. - return TRUSTED_BASE_PERMISSIONS.has(basePermission); - } - - function hasExactWorkflowRunIdentity( - run: WorkflowRun, - prNumber: number, - headSha: string, - runId: number, - ): boolean { - const exactRunId = run.id; - - if ( - typeof exactRunId !== "number" || - !Number.isInteger(exactRunId) || - exactRunId <= 0 || - exactRunId !== runId || - run.event !== "pull_request" || - 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, - ) - ); - } - - function belongsToExactPullRequest(run: WorkflowRun, prNumber: number, headSha: string): boolean { - return ( - typeof run.id === "number" && - hasExactWorkflowRunIdentity(run, prNumber, headSha, run.id) && - run.status === "completed" && - run.conclusion === "action_required" - ); - } - - async function exactWorkflowRunStillRequiresApproval( - prNumber: number, - headSha: string, - runId: number, - ): Promise { - const label = `Recheck exact workflow run ${runId}`; - const response = await withTransientApiRetry(label, () => - github.rest.actions.getWorkflowRun({ - owner, - repo, - run_id: runId, - request: { signal: requestSignalFor(label) }, - }), - ); - const run = response.data; - if (!hasExactWorkflowRunIdentity(run, prNumber, headSha, runId)) { - throw new Error( - `Workflow run ${runId} no longer matches exact PR #${prNumber} at ${headSha}`, - ); - } - if (typeof run.status !== "string" || run.status.length === 0) { - throw new Error(`Workflow run ${runId} returned an invalid status`); - } - return run.status === "completed" && run.conclusion === "action_required"; - } - - async function approveExactWorkflowRun( - prNumber: number, - headSha: string, - author: string, - runId: number, - ): Promise { - const label = `Approve workflow run ${runId}`; - let approvalMayHaveSucceeded = false; - for (let attempt = 1; attempt <= API_RETRY_ATTEMPTS; attempt += 1) { - // The PR head, head repository, author, and author permission can change between attempts. - // Revalidate them before each approval request. - const liveBeforeApproval = await loadLivePullRequest(prNumber); - if (liveHeadSha(liveBeforeApproval, prNumber) !== headSha) { - core.warning( - `PR #${prNumber} head changed before workflow-run approval; no further runs approved`, - ); - return { approved: false, stop: true }; - } - if (!isSameRepositoryHead(liveBeforeApproval)) { - core.warning(`PR #${prNumber} head repository changed; no further runs approved`); - return { approved: false, stop: true }; - } - if (String(liveBeforeApproval.user?.login ?? "").toLowerCase() !== author.toLowerCase()) { - throw new Error(`PR #${prNumber} author changed during workflow-run discovery`); - } - const livePermission = await loadAuthorPermission(author); - if (!livePermission || !hasWritePermission(livePermission)) { - core.warning( - `PR #${prNumber} author ${author} no longer has write, maintain, or admin permission; no further runs approved`, - ); - return { approved: false, stop: true }; - } - - try { - await github.rest.actions.approveWorkflowRun({ - owner, - repo, - run_id: runId, - request: { signal: requestSignalFor(label) }, - }); - return { approved: true, noOp: false, stop: false }; - } catch (error) { - const status = apiErrorStatus(error); - const transient = isRetryableApiError(error); - const postStateResponse = status !== null && APPROVAL_POST_STATE_HTTP_STATUSES.has(status); - if (!transient && !postStateResponse) throw error; - - if (transient) { - approvalMayHaveSucceeded = true; - } - - const delayMs = Math.min( - API_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), - API_RETRY_MAX_DELAY_MS, - ); - const identity = status === null ? apiErrorCode(error) : `HTTP ${status}`; - const ambiguity = approvalMayHaveSucceeded - ? "an approval request may have succeeded" - : "another approver may have completed the request"; - core.warning( - `${label} returned ${identity}; ${ambiguity}, rechecking exact run state in ${delayMs}ms`, - ); - await waitWithinScriptBudget(label, delayMs); - - const stillRequiresApproval = await exactWorkflowRunStillRequiresApproval( - prNumber, - headSha, - runId, - ); - if (!stillRequiresApproval) { - core.info( - `Workflow run ${runId} no longer requires approval after an ambiguous approval response; recording success without another approval request`, - ); - return { approved: true, noOp: true, stop: false }; - } - if (!transient || attempt === API_RETRY_ATTEMPTS) throw error; - } - } - throw new Error(`${label} exhausted its bounded retry loop`); - } - - async function listExactHeadWorkflowRunsRequiringApproval( - prNumber: number, - headSha: string, - ): Promise { - const runs: WorkflowRun[] = []; - const perPage = 100; - for (let page = 1; ; page += 1) { - const label = `List exact-head workflow runs for PR #${prNumber}, page ${page}`; - const response = await withTransientApiRetry(label, () => - github.rest.actions.listWorkflowRunsForRepo({ - owner, - repo, - event: "pull_request", - head_sha: headSha, - status: "action_required", - page, - per_page: perPage, - request: { signal: requestSignalFor(label) }, - }), - ); - const pageRuns = response.data?.workflow_runs; - if (!Array.isArray(pageRuns)) { - throw new Error(`${label} returned an invalid workflow_runs value`); - } - runs.push(...pageRuns); - if (pageRuns.length < perPage) return runs; - } - } - - 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 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 resolvedRunIds = 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 listExactHeadWorkflowRunsRequiringApproval( - eventPullRequest.number, - expectedHeadSha, - ); - - for (const run of runs) { - const runId = run.id; - - if ( - typeof runId !== "number" || - resolvedRunIds.has(runId) || - !belongsToExactPullRequest(run, eventPullRequest.number, expectedHeadSha) - ) { - continue; - } - - const approval = await approveExactWorkflowRun( - eventPullRequest.number, - expectedHeadSha, - author, - runId, - ); - if (approval.stop) return; - if (!approval.approved) continue; - resolvedRunIds.add(runId); - if (!approval.noOp) { - core.info( - `Approved pull_request workflow run ${runId} for PR #${eventPullRequest.number} at ${expectedHeadSha}`, - ); - } - } - - if (attempt + 1 < POLL_ATTEMPTS) { - if (remainingScriptBudgetMs() <= POLL_INTERVAL_MS) { - core.warning( - `Workflow-run polling stopped after ${attempt + 1}/${POLL_ATTEMPTS} attempts because the bounded ${SCRIPT_BUDGET_MS}ms script budget ended`, - ); - break; - } - await waitWithinScriptBudget("Workflow-run polling", POLL_INTERVAL_MS); - } - } - - core.info( - `Exact-head workflow runs that no longer require approval for PR #${eventPullRequest.number}: ${resolvedRunIds.size}`, - ); -}