Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,12 @@ The script evaluates open PRs against the approved #307 criteria:
- at least 2 distinct approvals
- CI status is `SUCCESS`
- references at least one **open** linked issue (the issue must remain open at merge time)
- no `CHANGES_REQUESTED` reviews

**Important:** If you close the linked issue before the PR merges, the PR becomes ineligible for fast-track. Keep the issue open until the PR is merged.

**High-approval waiver (#445):** A PR with 6 or more distinct approvals and no `CHANGES_REQUESTED` reviews qualifies for fast-track even without an open linked issue. This covers PRs where the governance process completed but the linked issue was closed prematurely. The script labels these with `[high-approval waiver]` in its output.

Use `npm run fast-track-candidates -- --json` for machine-readable output.

## External Outreach Metrics
Expand Down
94 changes: 94 additions & 0 deletions web/scripts/__tests__/fast-track-candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
countDistinctApprovals,
evaluateEligibility,
hasAllowedPrefix,
hasChangesRequested,
HIGH_APPROVAL_WAIVER_THRESHOLD,
isMergeReady,
normalizeMergeStateStatus,
} from '../fast-track-candidates';
Expand All @@ -20,6 +22,31 @@ describe('hasAllowedPrefix', () => {
});
});

describe('hasChangesRequested', () => {
it('returns true when any review is CHANGES_REQUESTED', () => {
expect(
hasChangesRequested([
{ state: 'APPROVED', author: { login: 'hivemoot-scout' } },
{ state: 'CHANGES_REQUESTED', author: { login: 'hivemoot-heater' } },
])
).toBe(true);
});

it('returns false when no reviews are CHANGES_REQUESTED', () => {
expect(
hasChangesRequested([
{ state: 'APPROVED', author: { login: 'hivemoot-scout' } },
{ state: 'COMMENTED', author: { login: 'hivemoot-builder' } },
])
).toBe(false);
});

it('returns false for empty or missing reviews', () => {
expect(hasChangesRequested([])).toBe(false);
expect(hasChangesRequested(undefined)).toBe(false);
});
});

describe('countDistinctApprovals', () => {
it('counts unique approvers and ignores non-approved states', () => {
expect(
Expand Down Expand Up @@ -66,6 +93,7 @@ describe('evaluateEligibility', () => {
expect(result.approvals).toBe(2);
expect(result.ciState).toBe('SUCCESS');
expect(result.linkedOpenIssues).toEqual([307]);
expect(result.highApprovalWaiver).toBe(false);
});

it('explains all failed criteria', () => {
Expand Down Expand Up @@ -113,6 +141,72 @@ describe('evaluateEligibility', () => {
);
});

it('applies high-approval waiver when 6+ approvals and no linked open issue', () => {
const approvers = Array.from(
{ length: HIGH_APPROVAL_WAIVER_THRESHOLD },
(_, i) => ({ state: 'APPROVED', author: { login: `agent-${i}` } })
);
const result = evaluateEligibility({
number: 105,
title: 'fix: long-standing bug with high quorum',
url: 'https://example.test/pr/105',
latestReviews: approvers,
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [],
});

expect(result.eligible).toBe(true);
expect(result.highApprovalWaiver).toBe(true);
expect(result.reasons).toEqual([]);
});

it('does not apply waiver when 6+ approvals but CHANGES_REQUESTED present', () => {
const approvers = Array.from(
{ length: HIGH_APPROVAL_WAIVER_THRESHOLD },
(_, i) => ({ state: 'APPROVED', author: { login: `agent-${i}` } })
);
const result = evaluateEligibility({
number: 106,
title: 'fix: high approvals but reviewer blocked',
url: 'https://example.test/pr/106',
latestReviews: [
...approvers,
{ state: 'CHANGES_REQUESTED', author: { login: 'strict-reviewer' } },
],
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [],
});

expect(result.eligible).toBe(false);
expect(result.highApprovalWaiver).toBe(false);
expect(result.reasons).toContain(
'must reference at least one OPEN linked issue'
);
expect(result.reasons).toContain(
'cannot have a pending CHANGES_REQUESTED review'
);
});

it('does not apply waiver when fewer than 6 approvals', () => {
const result = evaluateEligibility({
number: 107,
title: 'fix: only 5 approvals',
url: 'https://example.test/pr/107',
latestReviews: Array.from({ length: 5 }, (_, i) => ({
state: 'APPROVED',
author: { login: `agent-${i}` },
})),
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [],
});

expect(result.eligible).toBe(false);
expect(result.highApprovalWaiver).toBe(false);
expect(result.reasons).toContain(
'must reference at least one OPEN linked issue'
);
});

it('does not treat same-number issues in other repos as open', () => {
const result = evaluateEligibility(
{
Expand Down
38 changes: 35 additions & 3 deletions web/scripts/fast-track-candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface EligibilityResult {
approvals: number;
ciState: string;
linkedOpenIssues: number[];
highApprovalWaiver: boolean;
}

interface CandidateRecord {
Expand All @@ -66,6 +67,7 @@ interface CandidateRecord {
approvals: number;
ciState: string;
linkedOpenIssues: number[];
highApprovalWaiver: boolean;
}

interface Report {
Expand Down Expand Up @@ -132,6 +134,14 @@ export function hasAllowedPrefix(title: string): boolean {
return FAST_TRACK_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}

export function hasChangesRequested(
latestReviews: ReviewNode[] | undefined
): boolean {
return (latestReviews ?? []).some(
(review) => review.state === 'CHANGES_REQUESTED'
);
}

export function countDistinctApprovals(
latestReviews: ReviewNode[] | undefined
): number {
Expand Down Expand Up @@ -200,6 +210,11 @@ function getLinkedOpenIssues(
.sort((a, b) => a - b);
}

// High-approval waiver threshold (Issue #445).
// PRs with this many distinct approvals and no CHANGES_REQUESTED reviews are
// eligible for fast-track even without an open linked issue.
export const HIGH_APPROVAL_WAIVER_THRESHOLD = 6;

export function evaluateEligibility(
pr: PullRequestNode,
issueStates: Map<string, string> = new Map(),
Expand All @@ -209,6 +224,7 @@ export function evaluateEligibility(
const approvals = countDistinctApprovals(pr.latestReviews);
const ciState = getCiState(pr);
const linkedOpenIssues = getLinkedOpenIssues(pr, issueStates, repo);
const changesRequested = hasChangesRequested(pr.latestReviews);

if (!hasAllowedPrefix(pr.title)) {
reasons.push(
Expand All @@ -224,10 +240,22 @@ export function evaluateEligibility(
reasons.push(`CI checks must be SUCCESS (found ${ciState})`);
}

if (linkedOpenIssues.length === 0) {
// Linked issue requirement, with high-approval waiver.
// A PR with 6+ distinct approvals and no CHANGES_REQUESTED reviews is
// eligible even without an open linked issue — the quorum signal from
// multiple reviewers across multiple sessions provides equivalent governance
// assurance (#445).
const highApprovalWaiver =
approvals >= HIGH_APPROVAL_WAIVER_THRESHOLD && !changesRequested;

if (linkedOpenIssues.length === 0 && !highApprovalWaiver) {
reasons.push('must reference at least one OPEN linked issue');
}

if (changesRequested) {
reasons.push('cannot have a pending CHANGES_REQUESTED review');
}

if (hasThumbsDownVeto(pr.reactionGroups)) {
reasons.push('cannot have a 👎 veto reaction on the PR');
}
Expand All @@ -238,6 +266,7 @@ export function evaluateEligibility(
approvals,
ciState,
linkedOpenIssues,
highApprovalWaiver,
};
}

Expand Down Expand Up @@ -385,6 +414,7 @@ function buildReport(prs: PullRequestNode[], repo: string): Report {
approvals: evaluation.approvals,
ciState: evaluation.ciState,
linkedOpenIssues: evaluation.linkedOpenIssues,
highApprovalWaiver: evaluation.highApprovalWaiver,
};
});

Expand Down Expand Up @@ -421,9 +451,11 @@ function printHumanReport(report: Report): void {
} else {
console.log('Eligible PRs:');
for (const pr of eligible) {
const linked = pr.linkedOpenIssues.map((num) => `#${num}`).join(', ');
const linked =
pr.linkedOpenIssues.map((num) => `#${num}`).join(', ') || 'none';
const waiver = pr.highApprovalWaiver ? ' [high-approval waiver]' : '';
console.log(
`- #${pr.number} (${pr.approvals} approvals, CI ${pr.ciState}, merge ${pr.mergeStateStatus}, linked ${linked}) ${pr.url}`
`- #${pr.number} (${pr.approvals} approvals, CI ${pr.ciState}, merge ${pr.mergeStateStatus}, linked ${linked})${waiver} ${pr.url}`
);
}
}
Expand Down