diff --git a/build/cli/index.js b/build/cli/index.js index f3be1315e..85d7e7d56 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -87046,12 +87046,13 @@ class PullRequestApprovalRepository { collectDiagnostic(diagnostics, cause); } } + const checkClient = this.checkOctokit ?? this.octokit; const [files, reviews, comments, headChecks, mergeChecks, headRuns, mergeRuns, rules, user] = await Promise.all([ this.allPages('files', this.octokit.rest.pulls.listFiles, { owner: target.owner, repo: target.repository, pull_number: target.pullNumber }), this.allPages('reviews', this.octokit.rest.pulls.listReviews, { owner: target.owner, repo: target.repository, pull_number: target.pullNumber }), this.allPages('comments', this.octokit.rest.issues.listComments, { owner: target.owner, repo: target.repository, issue_number: target.pullNumber }), - this.allPages('head-checks', this.octokit.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: headSha, filter: 'all' }), - testedMergeSha ? this.allPages('merge-checks', this.octokit.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: testedMergeSha, filter: 'all' }) : Promise.resolve([]), + this.allPages('head-checks', checkClient.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: headSha, filter: 'all' }, checkClient), + testedMergeSha ? this.allPages('merge-checks', checkClient.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: testedMergeSha, filter: 'all' }, checkClient) : Promise.resolve([]), this.allPages('head-runs', this.octokit.rest.actions.listWorkflowRunsForRepo, { owner: target.owner, repo: target.repository, head_sha: headSha }), testedMergeSha ? this.allPages('merge-runs', this.octokit.rest.actions.listWorkflowRunsForRepo, { owner: target.owner, repo: target.repository, head_sha: testedMergeSha }) : Promise.resolve([]), this.effectiveRules(target.owner, target.repository, pull.base.ref, diagnostics), @@ -87309,10 +87310,10 @@ class PullRequestApprovalRepository { return { readable: false, dismissesStaleReviews: false, requiredChecks: [] }; } } - async allPages(operation, method, params) { + async allPages(operation, method, params, client = this.octokit) { return (0, approval_evidence_read_1.readApprovalEvidence)(operation, async () => { const result = []; - for await (const response of this.octokit.paginate.iterator(method, { ...params, per_page: 100 })) { + for await (const response of client.paginate.iterator(method, { ...params, per_page: 100 })) { const page = Array.isArray(response.data) ? response.data : record(response.data)?.check_runs ?? record(response.data)?.workflow_runs ?? record(response.data)?.artifacts ?? record(response.data)?.jobs; diff --git a/build/github_action/index.js b/build/github_action/index.js index 1d4d40ac2..ec45abb9c 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -77558,12 +77558,13 @@ class PullRequestApprovalRepository { collectDiagnostic(diagnostics, cause); } } + const checkClient = this.checkOctokit ?? this.octokit; const [files, reviews, comments, headChecks, mergeChecks, headRuns, mergeRuns, rules, user] = await Promise.all([ this.allPages('files', this.octokit.rest.pulls.listFiles, { owner: target.owner, repo: target.repository, pull_number: target.pullNumber }), this.allPages('reviews', this.octokit.rest.pulls.listReviews, { owner: target.owner, repo: target.repository, pull_number: target.pullNumber }), this.allPages('comments', this.octokit.rest.issues.listComments, { owner: target.owner, repo: target.repository, issue_number: target.pullNumber }), - this.allPages('head-checks', this.octokit.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: headSha, filter: 'all' }), - testedMergeSha ? this.allPages('merge-checks', this.octokit.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: testedMergeSha, filter: 'all' }) : Promise.resolve([]), + this.allPages('head-checks', checkClient.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: headSha, filter: 'all' }, checkClient), + testedMergeSha ? this.allPages('merge-checks', checkClient.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: testedMergeSha, filter: 'all' }, checkClient) : Promise.resolve([]), this.allPages('head-runs', this.octokit.rest.actions.listWorkflowRunsForRepo, { owner: target.owner, repo: target.repository, head_sha: headSha }), testedMergeSha ? this.allPages('merge-runs', this.octokit.rest.actions.listWorkflowRunsForRepo, { owner: target.owner, repo: target.repository, head_sha: testedMergeSha }) : Promise.resolve([]), this.effectiveRules(target.owner, target.repository, pull.base.ref, diagnostics), @@ -77821,10 +77822,10 @@ class PullRequestApprovalRepository { return { readable: false, dismissesStaleReviews: false, requiredChecks: [] }; } } - async allPages(operation, method, params) { + async allPages(operation, method, params, client = this.octokit) { return (0, approval_evidence_read_1.readApprovalEvidence)(operation, async () => { const result = []; - for await (const response of this.octokit.paginate.iterator(method, { ...params, per_page: 100 })) { + for await (const response of client.paginate.iterator(method, { ...params, per_page: 100 })) { const page = Array.isArray(response.data) ? response.data : record(response.data)?.check_runs ?? record(response.data)?.workflow_runs ?? record(response.data)?.artifacts ?? record(response.data)?.jobs; diff --git a/docs/pull-requests/guarded-approval.mdx b/docs/pull-requests/guarded-approval.mdx index d66a4959a..22fd5c6c2 100644 --- a/docs/pull-requests/guarded-approval.mdx +++ b/docs/pull-requests/guarded-approval.mdx @@ -19,9 +19,11 @@ New `copilot setup` runs offer `recommend` by default: Copilot assesses evidence Use `copilot setup` interactively to select an exact CI check, its GitHub App ID, and the workflow that produces it. The coverage check must enforce your repository's coverage budget; a successful advisory upload does not prove a percentage. Setup displays the selected names, target branches, Bugbot prerequisites, and branch-rule readiness before confirmation. The generated `copilot_pull_request_approval.yml` must reach the repository default branch before its `workflow_run` wakeups operate. -With `copilot setup --web`, the setup PAT can request conditional `Actions: -read` and `Checks: read` grants so the assistant can offer candidates from -completed pull-request runs from the last 90 days. Discovery requests up to +With `copilot setup --web`, the assistant can offer candidates from completed +pull-request runs from the last 90 days when the setup credential can read the +Actions and Checks APIs. GitHub currently does not expose Checks API access for +fine-grained PATs; if private-repository discovery is denied, enter the inspected +producer tuples manually. There is no selectable PAT Checks permission to add. Discovery requests up to 1,000 latest workflow runs over at most ten pages, selects up to 20 PR runs, sorts them by creation time and inspects at most 15, with up to 100 checks per commit. Historical or malformed dates are excluded. The lookup stops once it collects 20 recent PR @@ -144,9 +146,13 @@ can also prevent the read. This state leaves existing PRs and reviews unchanged and publishes no assessment. `invalid-policy` instead means the value was read but failed validation: run doctor and correct the variable before retrying. -Setup includes the same evidence reads for both active modes: Actions, Contents, -Administration, Checks and Variables read, and Pull requests write to publish the -assessment card. An installation with only approval assessment does not need +Both active modes need evidence reads through Actions, Contents, Administration +and Variables, plus Pull requests write for the assessment card. Native observer +Checks reads use the job-local `GITHUB_TOKEN` supplied through +`COPILOT_APPROVAL_CHECK_TOKEN`, with the generated workflow's `checks: write` +permission; that credential also publishes the supplemental Check. The runtime +`PAT` retains bot identity, policy reads and native review writes. It does not +need an unavailable fine-grained PAT Checks grant. An installation with only approval assessment does not need Actions, Contents or Issues write. Existing tokens created before this permission fix need their grants checked when recommendation mode is enabled. The mode still determines whether a native approval may be submitted. @@ -180,6 +186,7 @@ check alone does not prove access to that endpoint. | Diagnostic | Next action | | --- | --- | +| `head-checks` / `merge-checks` denied | Restore the generated observer's `COPILOT_APPROVAL_CHECK_TOKEN: ${{ github.token }}` and job Checks permission. Do not broaden the bot PAT; fine-grained PATs cannot call the Checks API. | | `http-401` / `http-403` | Check the runtime credential and the named endpoint access; correct the specific denial before rerunning. | | `http-404` | Verify resource identity and visibility under that same credential. A private resource can be hidden by authorization. | | `http-429`, supported 5xx or `transport-unavailable` | Wait for provider recovery or rate-limit reset, then make a bounded new observation. | @@ -205,3 +212,13 @@ submission and when evidence is reread after a review POST. If GitHub might have accepted a review, `publication-unknown` retains its review identity and the exact failed evidence operation. Recheck after resolving that cause; the observer reads native review history before considering another POST. + + +The Checks client is selected before reading evidence. If its authenticated read +is denied or returns malformed or oversized pages, the observer keeps the named +`evidence-unavailable` failure and never retries that read with the bot PAT. The +existing 3,000-record bound and head/App/workflow provenance checks still apply. +Without a job-local token, the historical PAT-only path remains available for +credentials that support Checks; it cannot convert a denied read into approval. +GitHub documents the fine-grained PAT limitation in +[Managing personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens-limitations). diff --git a/package.json b/package.json index c4ff15a8c..fe1e08552 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vypdev/copilot", - "version": "3.10.0", + "version": "3.10.1", "packageManager": "pnpm@10.12.4", "pnpm": { "overrides": { diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 5526a0369..34e09de23 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -30,7 +30,7 @@ debt or convert unknown historic intent into a design decision. | `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 44 paths · 2026-10-19 | | `configurable-issue-workflows` | Implemented | Select one canonical set of issue workflows and enforce its forms, dependencies, branch policy, runtime admission, migration, and diagnosis | [Configurable issue workflows and fail-closed admission](./configurable-issue-workflows-and-admission.md) | 89 paths · 2026-10-13 | | `repository-agent-collaboration` | Implemented | Generate safe repository-local profiles, guidance, and a skill for agents contributing through configured issues, Action-managed branches, pull requests, and deployment boundaries | [Repository agent collaboration contract](./repository-agent-collaboration-contract.md) | 47 paths · 2026-10-17 | -| `guarded-pull-request-approval` | Proposed | Specify and locally implement revision-bound, evidence-gated native bot approvals for eligible human pull requests, with safe setup defaults, read-only doctor checks, and explainable recovery | [Guarded pull-request approval and setup readiness](./guarded-pull-request-approval.md) + 2 companion | 94 paths · 2026-10-20 | +| `guarded-pull-request-approval` | Proposed | Specify and locally implement revision-bound, evidence-gated native bot approvals for eligible human pull requests, with safe setup defaults, read-only doctor checks, and explainable recovery | [Guarded pull-request approval and setup readiness](./guarded-pull-request-approval.md) + 2 companion | 94 paths · 2026-10-22 | ## Evidence map @@ -257,7 +257,7 @@ debt or convert unknown historic intent into a design decision. ### `guarded-pull-request-approval` — Guarded pull-request approval and setup readiness - Owner: Copilot maintainers -- Last verified: 2026-10-20 +- Last verified: 2026-10-22 - Specifications: [`specs/guarded-pull-request-approval.md`](./guarded-pull-request-approval.md) · [`specs/guarded-pull-request-approval-setup-and-doctor.md`](./guarded-pull-request-approval-setup-and-doctor.md) · [`specs/guarded-pull-request-approval-status.md`](./guarded-pull-request-approval-status.md) - Workflows: [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) · [`.github/workflows/copilot_pull_request_approval.yml`](../.github/workflows/copilot_pull_request_approval.yml) · [`setup/workflows/copilot_pull_request.yml`](../setup/workflows/copilot_pull_request.yml) · [`setup/workflows/copilot_pull_request_review_state.yml`](../setup/workflows/copilot_pull_request_review_state.yml) · [`setup/workflows/copilot_pull_request_approval.yml`](../setup/workflows/copilot_pull_request_approval.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/pull_request_approval_action.ts`](../src/actions/pull_request_approval_action.ts) · [`src/cli/commands/setup.ts`](../src/cli/commands/setup.ts) · [`src/cli/commands/doctor.ts`](../src/cli/commands/doctor.ts) · [`src/infrastructure/composition/pull_request_status_composition_root.ts`](../src/infrastructure/composition/pull_request_status_composition_root.ts) diff --git a/specs/catalog.json b/specs/catalog.json index ca41fbc29..e7dc656b3 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -2579,7 +2579,7 @@ "status": "proposed", "scope": "Specify and locally implement revision-bound, evidence-gated native bot approvals for eligible human pull requests, with safe setup defaults, read-only doctor checks, and explainable recovery", "owner": "Copilot maintainers", - "lastVerified": "2026-10-20", + "lastVerified": "2026-10-22", "specs": [ "specs/guarded-pull-request-approval.md", "specs/guarded-pull-request-approval-setup-and-doctor.md", diff --git a/specs/guarded-pull-request-approval.md b/specs/guarded-pull-request-approval.md index bd1aae95f..0d8d3b5d2 100644 --- a/specs/guarded-pull-request-approval.md +++ b/specs/guarded-pull-request-approval.md @@ -485,3 +485,69 @@ MUST retain their causal diagnostic and any accepted review ID while keeping promoted to a verified approval. This preserves read-first recovery and never retries the review POST blindly. The three additional application cases above cover these final-boundary risks independently of initial-read failures. + + +### Native Checks credential isolation — #670 (2026-10-22) + +Observed 3.10.0 consumer evidence: private Platform PR #204 passed application CI +and Bugbot, but observer runs `39785896854` and `39799838760` failed with +`head-checks` / `merge-checks` HTTP 403. The adapter read Checks with the bot PAT +even though the trusted workflow supplied `COPILOT_APPROVAL_CHECK_TOKEN`. GitHub +[documents that fine-grained PATs cannot call the Checks API](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens-limitations). +The operator cannot add the suggested PAT permission. + +The adapter MUST use the existing job-local Checks client for both head and +verified-merge Check Run pagination. Policy, bot identity, PR data, native review +POSTs and assessment comments MUST retain the bot PAT client. The native token +also retains its existing supplemental Check publication responsibility. Client +selection happens before reads; an authenticated native denial MUST NOT retry +with the PAT. Existing PAT-only callers retain their prior client selection, +bounds and non-authorizing failure behavior. No new input, permission, policy, +durable state, retry, workflow or installer behavior is introduced. + +```text +trusted observer -> job-local token -> bounded head/merge Checks evidence + -> bot PAT -> policy, identity, PR/rules/coverage evidence + -> unchanged pure decision -> bot PAT native review + card + -> job-local token supplemental Check +``` + +Text equivalent: only Check Run inventory changes credential. The adapter still +terminates provider DTOs and sanitized errors; application/domain/presentation +contracts and all current-head, producer, coverage and native approval fences +remain unchanged. Existing architecture/workflow contracts enforce the trusted +entrypoint and credential boundaries. + +Allocate **five additional distinct SDK cases**: one successful two-credential +read plus retained bot review identity; two independent head/merge denials with +no PAT fallback or review write; one malformed native page; and one native +inventory above 3,000 records. Existing PAT-only fixtures prove compatibility. +The changed adapter retains the repository's coverage gates and MUST meet at +least 95% lines/statements and 90% branches/functions. Deterministic SDK clients +replace network and waits. No new policy or UI state needs new domain cases. + +Operator recovery for a Checks denial is to restore the generated native token +binding and job Checks permission, then rerun the observer. Representative +technical state remains `merge-checks (http-403)`, `evidence-unavailable`, no +authorized review, and retained prior PR/reviews. Pending, successful and partial +review states keep the existing card/summary content; there is no extra comment +or localization surface. The approval guide MUST distinguish this job credential +from the fine-grained PAT and explain manual setup producer entry when private +Checks discovery is unavailable. + +Acceptance: native read fixtures must return the exact configured App/workflow +check for the verified merge; denials, malformed pages and overflow must retain +the named causal failure and perform no native review. Existing producer/head +and final reread cases must remain green. DoD includes these five cases, changed +adapter coverage, types/lint, architecture, workflow/docs/specification gates, +generated bundles/catalog and Graphify. Deploying the corrected Action and +rechecking the private consumer is required for live acceptance; local fixtures +do not establish production recovery. Rollback restores the previous client +selection without changing native reviews. + +Local implementation evidence: five new cases first failed against the old +client selection and then passed; the complete repository adapter suite passes +137 cases. Full coverage passes 710 suites and 11,890 tests, with 35 existing +skips; global lines/statements/functions/branches are 97.81/96.94/97.69/94.30%. +The changed adapter reaches 98.90% lines, 97.42% statements, 100% functions and +90.82% branches. These local facts do not claim native consumer recovery. diff --git a/src/data/repository/pull_request/__tests__/pull_request_approval_repository.test.ts b/src/data/repository/pull_request/__tests__/pull_request_approval_repository.test.ts index b5ef4fa42..de1d3fc13 100644 --- a/src/data/repository/pull_request/__tests__/pull_request_approval_repository.test.ts +++ b/src/data/repository/pull_request/__tests__/pull_request_approval_repository.test.ts @@ -121,6 +121,26 @@ function evidenceFixture() { return { pat, pages, repository: new PullRequestApprovalRepository('pat', settings) }; } +function nativeCheckFixture() { + const pat = evidenceOctokit(); + const original = pat.paginate.iterator.getMockImplementation()!; + const listForRef = jest.fn(); + const checks = { + rest: { checks: { listForRef } }, + paginate: { iterator: jest.fn(async function* (method: unknown, params: { ref?: string; head_sha?: string }) { + if (method !== listForRef) throw new Error('Unexpected native check operation.'); + yield* original(pat.rest.checks.listForRef, params); + }) }, + }; + pat.paginate.iterator.mockImplementation(async function* (method, params) { + if (method === pat.rest.checks.listForRef) throw { status: 403 }; + yield* original(method, params); + }); + (github.getOctokit as jest.Mock).mockReturnValueOnce(pat).mockReturnValueOnce(checks); + const repository = new PullRequestApprovalRepository('pat', { ...settings, githubToken: 'job-token' }); + return { pat, checks, repository }; +} + function numericFixture() { const f = evidenceFixture(); f.pat.rest.actions.getRepoVariable.mockResolvedValue({ data: { value: serializePullRequestApprovalPolicy({ @@ -144,6 +164,59 @@ function bugbotCard(statusHead = head, pr = 42, evidenceHead = statusHead, diges describe('PullRequestApprovalRepository', () => { beforeEach(() => jest.clearAllMocks()); + it('uses the native check credential for both revisions and retains bot review identity', async () => { + const f = nativeCheckFixture(); + const evidence = await f.repository.loadEvidence(target); + expect(evidence.checks).toEqual([expect.objectContaining({ + name: 'CI Check', sourceAppId: 15368, workflowName: 'CI Check', headSha: testedMerge, + })]); + for (const ref of [head, testedMerge]) { + expect(f.checks.paginate.iterator).toHaveBeenCalledWith(f.checks.rest.checks.listForRef, + expect.objectContaining({ owner: 'owner', repo: 'repo', ref, filter: 'all', per_page: 100 })); + } + await f.repository.submitApproval(target, head, 'Verified current evidence'); + expect(f.pat.rest.users.getAuthenticated).toHaveBeenCalled(); + expect(f.pat.rest.actions.getRepoVariable).toHaveBeenCalled(); + expect(f.pat.rest.pulls.createReview).toHaveBeenCalledWith(expect.objectContaining({ + commit_id: head, event: 'APPROVE', + })); + expect(f.pat.paginate.iterator.mock.calls.some(([method]) => method === f.pat.rest.checks.listForRef)).toBe(false); + }); + + it.each([[head, 'head-checks'], [testedMerge, 'merge-checks']])( + 'fails closed after the native check credential cannot read %s without PAT fallback', async (ref, operation) => { + const f = nativeCheckFixture(); + const original = f.checks.paginate.iterator.getMockImplementation()!; + f.checks.paginate.iterator.mockImplementation(async function* (method, params) { + if (params.ref === ref) throw { status: 403, message: 'PRIVATE-CREDENTIAL' }; + yield* original(method, params); + }); + await expect(f.repository.loadEvidence(target)).rejects.toMatchObject({ + code: 'authorization.denied', recovery: { variables: { operation, reason: 'http-403' } }, + }); + expect(f.pat.paginate.iterator.mock.calls.some(([method]) => method === f.pat.rest.checks.listForRef)).toBe(false); + expect(f.pat.rest.pulls.createReview).not.toHaveBeenCalled(); + expect(JSON.stringify((logInfo as jest.Mock).mock.calls)).not.toContain('PRIVATE-CREDENTIAL'); + }); + + it('rejects malformed pages from the native check credential', async () => { + const f = nativeCheckFixture(); + f.checks.paginate.iterator.mockImplementation(async function* () { yield { data: { check_runs: null } }; }); + await expect(f.repository.loadEvidence(target)).rejects.toMatchObject({ + recovery: { variables: { operation: 'head-checks', reason: 'invalid-page' } }, + }); + expect(f.pat.rest.pulls.createReview).not.toHaveBeenCalled(); + }); + + it('retains the bounded inventory with the native check credential', async () => { + const f = nativeCheckFixture(); + f.checks.paginate.iterator.mockImplementation(async function* () { yield { data: { check_runs: Array(3001).fill({}) } }; }); + await expect(f.repository.loadEvidence(target)).rejects.toMatchObject({ + recovery: { variables: { operation: 'head-checks', reason: 'page-limit' } }, + }); + expect(f.pat.rest.pulls.createReview).not.toHaveBeenCalled(); + }); + it('prefers a repository policy Variable over organization scope', async () => { const pat = patOctokit(); (github.getOctokit as jest.Mock).mockReturnValue(pat); diff --git a/src/data/repository/pull_request/pull_request_approval_repository.ts b/src/data/repository/pull_request/pull_request_approval_repository.ts index b86b90047..48673553c 100644 --- a/src/data/repository/pull_request/pull_request_approval_repository.ts +++ b/src/data/repository/pull_request/pull_request_approval_repository.ts @@ -96,12 +96,13 @@ export class PullRequestApprovalRepository implements PullRequestApprovalPort { && merge.data.parents.length === 2) testedMergeSha = pull.merge_commit_sha!; } catch (cause) { collectDiagnostic(diagnostics, cause); } } + const checkClient = this.checkOctokit ?? this.octokit; const [files, reviews, comments, headChecks, mergeChecks, headRuns, mergeRuns, rules, user] = await Promise.all([ this.allPages('files', this.octokit.rest.pulls.listFiles, { owner: target.owner, repo: target.repository, pull_number: target.pullNumber }), this.allPages('reviews', this.octokit.rest.pulls.listReviews, { owner: target.owner, repo: target.repository, pull_number: target.pullNumber }), this.allPages('comments', this.octokit.rest.issues.listComments, { owner: target.owner, repo: target.repository, issue_number: target.pullNumber }), - this.allPages('head-checks', this.octokit.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: headSha, filter: 'all' as const }), - testedMergeSha ? this.allPages('merge-checks', this.octokit.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: testedMergeSha, filter: 'all' as const }) : Promise.resolve([]), + this.allPages('head-checks', checkClient.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: headSha, filter: 'all' as const }, checkClient), + testedMergeSha ? this.allPages('merge-checks', checkClient.rest.checks.listForRef, { owner: target.owner, repo: target.repository, ref: testedMergeSha, filter: 'all' as const }, checkClient) : Promise.resolve([]), this.allPages('head-runs', this.octokit.rest.actions.listWorkflowRunsForRepo, { owner: target.owner, repo: target.repository, head_sha: headSha }), testedMergeSha ? this.allPages('merge-runs', this.octokit.rest.actions.listWorkflowRunsForRepo, { owner: target.owner, repo: target.repository, head_sha: testedMergeSha }) : Promise.resolve([]), this.effectiveRules(target.owner, target.repository, pull.base.ref, diagnostics), @@ -352,10 +353,10 @@ export class PullRequestApprovalRepository implements PullRequestApprovalPort { } } - private async allPages(operation: ApprovalEvidenceOperation, method: unknown, params: Record): Promise { + private async allPages(operation: ApprovalEvidenceOperation, method: unknown, params: Record, client = this.octokit): Promise { return readApprovalEvidence(operation, async () => { const result: T[] = []; - for await (const response of this.octokit.paginate.iterator(method as never, { ...params, per_page: 100 } as never)) { + for await (const response of client.paginate.iterator(method as never, { ...params, per_page: 100 } as never)) { const page = Array.isArray(response.data) ? response.data : record(response.data)?.check_runs ?? record(response.data)?.workflow_runs ?? record(response.data)?.artifacts ?? record(response.data)?.jobs;