From a30e4dd9cae8853d74751c6e6b1e0492d368eb89 Mon Sep 17 00:00:00 2001 From: Sergio Velderrain Date: Mon, 9 Mar 2026 19:25:51 -0700 Subject: [PATCH] Update for standing priority #978 (#985) * #978 Skip Copilot gate on throughput fork repos * #978 Fix fork standing-priority workflow context * #978 Fix Copilot gate signal preflight --------- Co-authored-by: GitHub Copilot --- .github/workflows/issue-snapshot.yml | 1 + .github/workflows/validate.yml | 1 + .../__tests__/copilot-review-gate.test.mjs | 107 ++++++++++++++++++ ...anding-priority-workflow-contract.test.mjs | 47 ++++++++ tools/priority/copilot-review-gate.mjs | 27 ++++- 5 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 tools/priority/__tests__/standing-priority-workflow-contract.test.mjs diff --git a/.github/workflows/issue-snapshot.yml b/.github/workflows/issue-snapshot.yml index d9612b809..9fdbef8f5 100644 --- a/.github/workflows/issue-snapshot.yml +++ b/.github/workflows/issue-snapshot.yml @@ -16,6 +16,7 @@ jobs: node-version: '20' - name: Sync standing-priority snapshot env: + AGENT_PRIORITY_UPSTREAM_REPOSITORY: LabVIEW-Community-CI-CD/compare-vi-cli-action GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index df6c8173e..45cb74d36 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -713,6 +713,7 @@ jobs: node-version: '20' - name: Sync standing-priority snapshot env: + AGENT_PRIORITY_UPSTREAM_REPOSITORY: LabVIEW-Community-CI-CD/compare-vi-cli-action GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/tools/priority/__tests__/copilot-review-gate.test.mjs b/tools/priority/__tests__/copilot-review-gate.test.mjs index 29a2cca2f..d6ca0431a 100644 --- a/tools/priority/__tests__/copilot-review-gate.test.mjs +++ b/tools/priority/__tests__/copilot-review-gate.test.mjs @@ -105,6 +105,47 @@ test('copilot-review-gate skips merge-group runs while keeping the required stat assert.deepEqual(result.report?.reasons, ['merge-group-skip']); }); +test('copilot-review-gate skips throughput fork repos before any live lookup', async () => { + const { runCopilotReviewGate } = await loadModule(); + let reviewsCalled = false; + let threadsCalled = false; + + const result = await runCopilotReviewGate({ + argv: createArgv([ + '--event-name', + 'pull_request_target', + '--repo', + 'svelderrainruiz/compare-vi-cli-action', + '--pr', + '304', + '--head-sha', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + '--base-ref', + 'develop', + '--draft', + 'false', + ]), + loadReviewsFn: async () => { + reviewsCalled = true; + return []; + }, + loadThreadsFn: async () => { + threadsCalled = true; + return []; + }, + writeReportFn: () => 'memory://copilot-review-gate-throughput-fork.json', + appendStepSummaryFn: () => {}, + }); + + assert.equal(result.exitCode, 0); + assert.equal(result.report?.status, 'pass'); + assert.equal(result.report?.gateState, 'skipped'); + assert.deepEqual(result.report?.reasons, ['throughput-fork-skip']); + assert.equal(result.report?.signals.gateApplies, false); + assert.equal(reviewsCalled, false); + assert.equal(threadsCalled, false); +}); + test('copilot-review-gate passes stale but clean follow-up heads after an earlier Copilot review', async () => { const { runCopilotReviewGate } = await loadModule(); const currentHead = 'cccccccccccccccccccccccccccccccccccccccc'; @@ -355,6 +396,72 @@ test('copilot-review-gate can evaluate the current-head state from the collected assert.equal(threadsCalled, false); }); +test('copilot-review-gate reads the signal artifact before throughput-fork preflight skipping', async (t) => { + const { runCopilotReviewGate } = await loadModule(); + let reviewsCalled = false; + let threadsCalled = false; + const signalPath = createSignalFixture(t, 'copilot-review-signal-signal-only.json'); + + const result = await runCopilotReviewGate({ + argv: createArgv([ + '--event-name', + 'pull_request_target', + '--pr', + '885', + '--head-sha', + '9999999999999999999999999999999999999999', + '--base-ref', + 'develop', + '--draft', + 'false', + '--signal', + signalPath, + ]), + readSignalFn: () => ({ + schema: 'priority/copilot-review-signal@v1', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + pullRequest: { + number: 885, + url: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/885', + draft: false, + headSha: '9999999999999999999999999999999999999999', + baseRef: 'develop', + }, + latestCopilotReview: { + id: '15', + state: 'COMMENTED', + commitId: '9999999999999999999999999999999999999999', + submittedAt: '2026-03-08T06:05:00Z', + url: 'https://github.com/example/review/15', + isCurrentHead: true, + bodySummary: 'Current-head Copilot review.', + }, + staleReviews: [], + unresolvedThreads: [], + actionableComments: [], + errors: [], + }), + loadReviewsFn: async () => { + reviewsCalled = true; + return []; + }, + loadThreadsFn: async () => { + threadsCalled = true; + return []; + }, + writeReportFn: () => 'memory://copilot-review-gate-signal-only.json', + appendStepSummaryFn: () => {}, + }); + + assert.equal(result.exitCode, 0); + assert.equal(result.report?.source.mode, 'signal'); + assert.equal(result.report?.status, 'pass'); + assert.equal(result.report?.gateState, 'ready'); + assert.deepEqual(result.report?.reasons, ['current-head-review-clean']); + assert.equal(reviewsCalled, false); + assert.equal(threadsCalled, false); +}); + test('copilot-review-gate reports an error when loading reviews fails', async () => { const { runCopilotReviewGate } = await loadModule(); diff --git a/tools/priority/__tests__/standing-priority-workflow-contract.test.mjs b/tools/priority/__tests__/standing-priority-workflow-contract.test.mjs new file mode 100644 index 000000000..d76d27012 --- /dev/null +++ b/tools/priority/__tests__/standing-priority-workflow-contract.test.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { readFileSync } from 'node:fs'; + +const repoRoot = process.cwd(); +const canonicalUpstreamSlug = 'LabVIEW-Community-CI-CD/compare-vi-cli-action'; + +function readRepoFile(relativePath) { + return readFileSync(path.join(repoRoot, relativePath), 'utf8'); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +test('issue-event standing-priority snapshot resolves fork context through the canonical upstream slug', () => { + const workflow = readRepoFile('.github/workflows/issue-snapshot.yml'); + const stepPattern = new RegExp( + [ + '- name: Sync standing-priority snapshot', + '[\\s\\S]*?', + `AGENT_PRIORITY_UPSTREAM_REPOSITORY: ${escapeRegExp(canonicalUpstreamSlug)}`, + '[\\s\\S]*?', + 'node tools/npm/run-script\\.mjs priority:sync:lane' + ].join('') + ); + + assert.match(workflow, stepPattern); +}); + +test('validate standing-priority snapshot resolves fork context through the canonical upstream slug', () => { + const workflow = readRepoFile('.github/workflows/validate.yml'); + const stepPattern = new RegExp( + [ + '- name: Sync standing-priority snapshot', + '[\\s\\S]*?', + `AGENT_PRIORITY_UPSTREAM_REPOSITORY: ${escapeRegExp(canonicalUpstreamSlug)}`, + '[\\s\\S]*?', + 'node tools/npm/run-script\\.mjs priority:sync:lane' + ].join('') + ); + + assert.match(workflow, stepPattern); +}); diff --git a/tools/priority/copilot-review-gate.mjs b/tools/priority/copilot-review-gate.mjs index bab41dc72..0f25f151c 100644 --- a/tools/priority/copilot-review-gate.mjs +++ b/tools/priority/copilot-review-gate.mjs @@ -17,6 +17,7 @@ const DEFAULT_GATED_BASE_REFS = ['develop']; const DEFAULT_POLL_ATTEMPTS = 1; const DEFAULT_POLL_DELAY_MS = 10000; const GITHUB_API_URL = 'https://api.github.com'; +const CANONICAL_REPOSITORY = 'LabVIEW-Community-CI-CD/compare-vi-cli-action'; const COPILOT_LOGINS = new Set([ 'copilot', @@ -195,6 +196,11 @@ export function parseRepoSlug(repo) { return { owner, repo: repoName }; } +function isCanonicalRepository(repo) { + const normalized = normalizeText(repo)?.toLowerCase(); + return normalized === CANONICAL_REPOSITORY.toLowerCase(); +} + function isCopilotLogin(login) { const normalized = normalizeText(login)?.toLowerCase(); return normalized ? COPILOT_LOGINS.has(normalized) : false; @@ -559,10 +565,11 @@ function evaluateGateOutcome({ }) { const reasons = []; const normalizedBaseRef = normalizeBaseRef(pullRequest.baseRef)?.toLowerCase() ?? null; + const canonicalRepository = isCanonicalRepository(repository); const gateApplies = eventName !== 'merge_group' && pullRequest.draft !== true && - Boolean(normalizedBaseRef && gatedBaseRefs.includes(normalizedBaseRef)); + Boolean(normalizedBaseRef && gatedBaseRefs.includes(normalizedBaseRef) && canonicalRepository); const summary = { copilotReviewCount: reviews.length, @@ -620,6 +627,9 @@ function evaluateGateOutcome({ } else if (!normalizedBaseRef || !gatedBaseRefs.includes(normalizedBaseRef)) { gateState = 'skipped'; reasons.push('base-ref-not-gated'); + } else if (!canonicalRepository) { + gateState = 'skipped'; + reasons.push('throughput-fork-skip'); } else if (errors.length > 0) { status = 'fail'; gateState = 'error'; @@ -953,18 +963,24 @@ export async function runCopilotReviewGate({ let report; try { - const preflightPullRequest = buildPullRequest(options); + const signalPathExists = options.signalPath && existsSync(path.resolve(process.cwd(), options.signalPath)); + const signalReport = signalPathExists ? readSignalFn(options.signalPath) : null; + const preflightPullRequest = buildPullRequest(options, signalReport); const preflightBaseRef = normalizeBaseRef(preflightPullRequest.baseRef)?.toLowerCase() ?? null; + const preflightRepository = + normalizeText(signalReport?.repository) ?? + normalizeText(options.repo); const shouldSkipWithoutLookup = options.eventName === 'merge_group' || preflightPullRequest.draft === true || !preflightBaseRef || - !options.gatedBaseRefs.includes(preflightBaseRef); + !options.gatedBaseRefs.includes(preflightBaseRef) || + (preflightRepository !== null && !isCanonicalRepository(preflightRepository)); if (shouldSkipWithoutLookup) { report = evaluateGateOutcome({ eventName: options.eventName, - repository: normalizeText(options.repo), + repository: preflightRepository, sourceMode: options.eventName === 'merge_group' ? 'merge-group' : 'metadata', pullRequest: preflightPullRequest, reviews: [], @@ -973,8 +989,7 @@ export async function runCopilotReviewGate({ gatedBaseRefs: options.gatedBaseRefs, now, }); - } else if (options.signalPath && existsSync(path.resolve(process.cwd(), options.signalPath))) { - const signalReport = readSignalFn(options.signalPath); + } else if (signalReport) { report = buildReportFromSignal(options, signalReport, now); } else { const reviews = await loadReviewsFn(options);