Skip to content
Closed
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
13 changes: 11 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,23 @@ cd web
npm run fast-track-candidates
```

The script evaluates open PRs against the approved #307 criteria:
The script evaluates open PRs against the approved criteria (#307, amended by #445):

- title prefix is one of `fix:`, `test:`, `docs:`, `chore:`, `a11y:`, `polish:`
- 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 👎 veto reaction on the PR

**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
active `CHANGES_REQUESTED` reviews is eligible for fast-track even if its linked issue
was closed before the PR merged. This covers cases where a linked issue was closed
prematurely — the governance process completed, and the approval count is strong evidence
of peer consensus. The script marks waiver-eligible PRs with `[high-approval waiver]` in
its output.

**Important:** For PRs with fewer than 6 approvals, keep the linked issue open until the
PR merges to maintain fast-track eligibility.

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

Expand Down
128 changes: 128 additions & 0 deletions web/scripts/__tests__/fast-track-candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { describe, expect, it } from 'vitest';
import {
countDistinctApprovals,
evaluateEligibility,
FAST_TRACK_HIGH_APPROVAL_WAIVER_THRESHOLD,
hasAllowedPrefix,
hasChangesRequested,
isMergeReady,
normalizeMergeStateStatus,
} from '../fast-track-candidates';
Expand Down Expand Up @@ -47,6 +49,36 @@ describe('merge state helpers', () => {
});
});

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-nurse' } },
])
).toBe(true);
});

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

it('returns false for undefined reviews', () => {
expect(hasChangesRequested(undefined)).toBe(false);
});
});

describe('FAST_TRACK_HIGH_APPROVAL_WAIVER_THRESHOLD', () => {
it('is 6', () => {
expect(FAST_TRACK_HIGH_APPROVAL_WAIVER_THRESHOLD).toBe(6);
});
});

describe('evaluateEligibility', () => {
it('marks PR eligible when all criteria pass', () => {
const result = evaluateEligibility({
Expand All @@ -66,6 +98,7 @@ describe('evaluateEligibility', () => {
expect(result.approvals).toBe(2);
expect(result.ciState).toBe('SUCCESS');
expect(result.linkedOpenIssues).toEqual([307]);
expect(result.waiverApplied).toBe(false);
});

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

it('grants high-approval waiver when ≥6 approvals and linked issue is closed', () => {
const sixApprovers = [
{ state: 'APPROVED', author: { login: 'agent-a' } },
{ state: 'APPROVED', author: { login: 'agent-b' } },
{ state: 'APPROVED', author: { login: 'agent-c' } },
{ state: 'APPROVED', author: { login: 'agent-d' } },
{ state: 'APPROVED', author: { login: 'agent-e' } },
{ state: 'APPROVED', author: { login: 'agent-f' } },
];
const result = evaluateEligibility({
number: 200,
title: 'fix: apply high-approval waiver',
url: 'https://example.test/pr/200',
latestReviews: sixApprovers,
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [{ number: 445, state: 'CLOSED' }],
});

expect(result.eligible).toBe(true);
expect(result.waiverApplied).toBe(true);
expect(result.approvals).toBe(6);
expect(result.reasons).toEqual([]);
});

it('does not grant waiver when a CHANGES_REQUESTED review is present', () => {
const reviews = [
{ state: 'APPROVED', author: { login: 'agent-a' } },
{ state: 'APPROVED', author: { login: 'agent-b' } },
{ state: 'APPROVED', author: { login: 'agent-c' } },
{ state: 'APPROVED', author: { login: 'agent-d' } },
{ state: 'APPROVED', author: { login: 'agent-e' } },
{ state: 'APPROVED', author: { login: 'agent-f' } },
{ state: 'CHANGES_REQUESTED', author: { login: 'agent-g' } },
];
const result = evaluateEligibility({
number: 201,
title: 'fix: blocked by changes-requested',
url: 'https://example.test/pr/201',
latestReviews: reviews,
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [{ number: 445, state: 'CLOSED' }],
});

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

it('does not grant waiver when approval count is below threshold', () => {
const result = evaluateEligibility({
number: 202,
title: 'fix: insufficient approvals for waiver',
url: 'https://example.test/pr/202',
latestReviews: [
{ state: 'APPROVED', author: { login: 'agent-a' } },
{ state: 'APPROVED', author: { login: 'agent-b' } },
{ state: 'APPROVED', author: { login: 'agent-c' } },
{ state: 'APPROVED', author: { login: 'agent-d' } },
{ state: 'APPROVED', author: { login: 'agent-e' } },
],
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [{ number: 445, state: 'CLOSED' }],
});

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

it('sets waiverApplied to false when PR has an open linked issue (no waiver needed)', () => {
const sixApprovers = [
{ state: 'APPROVED', author: { login: 'agent-a' } },
{ state: 'APPROVED', author: { login: 'agent-b' } },
{ state: 'APPROVED', author: { login: 'agent-c' } },
{ state: 'APPROVED', author: { login: 'agent-d' } },
{ state: 'APPROVED', author: { login: 'agent-e' } },
{ state: 'APPROVED', author: { login: 'agent-f' } },
];
const result = evaluateEligibility({
number: 203,
title: 'fix: waiver not needed, open issue present',
url: 'https://example.test/pr/203',
latestReviews: sixApprovers,
statusCheckRollup: [{ status: 'COMPLETED', conclusion: 'SUCCESS' }],
closingIssuesReferences: [{ number: 445, state: 'OPEN' }],
});

expect(result.eligible).toBe(true);
expect(result.waiverApplied).toBe(false);
});

it('does not treat same-number issues in other repos as open', () => {
const result = evaluateEligibility(
{
Expand Down
41 changes: 38 additions & 3 deletions web/scripts/fast-track-candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const FAST_TRACK_PREFIXES = [
'polish:',
] as const;

/**
* Minimum distinct approvals required to qualify for the high-approval waiver.
* A PR meeting this threshold with no active CHANGES_REQUESTED reviews is
* considered fast-track eligible even when its linked issue was closed before
* the PR merged. Approved in #445.
*/
export const FAST_TRACK_HIGH_APPROVAL_WAIVER_THRESHOLD = 6;

interface ReviewNode {
state?: string;
author?: {
Expand Down Expand Up @@ -54,6 +62,8 @@ export interface EligibilityResult {
approvals: number;
ciState: string;
linkedOpenIssues: number[];
/** True when the high-approval waiver (#445) was the reason a missing linked issue was allowed. */
waiverApplied: boolean;
}

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

interface Report {
Expand Down Expand Up @@ -125,6 +136,9 @@ function printHelp(): void {
console.log(
'Usage: npm run fast-track-candidates -- [--repo=owner/name] [--limit=200] [--json]'
);
console.log(
`High-approval waiver: PRs with ≥${FAST_TRACK_HIGH_APPROVAL_WAIVER_THRESHOLD} approvals and no CHANGES_REQUESTED may skip the linked-issue requirement.`
);
}

export function hasAllowedPrefix(title: string): boolean {
Expand All @@ -151,6 +165,14 @@ export function countDistinctApprovals(
return approvedBy.size;
}

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

function getCiState(pr: PullRequestNode): string {
const checks = pr.statusCheckRollup ?? [];
if (checks.length === 0) {
Expand Down Expand Up @@ -210,6 +232,15 @@ export function evaluateEligibility(
const ciState = getCiState(pr);
const linkedOpenIssues = getLinkedOpenIssues(pr, issueStates, repo);

// High-approval waiver (#445): a PR with ≥6 distinct approvals and no
// CHANGES_REQUESTED reviews may skip the open linked-issue requirement.
// The waiver covers cases where a linked issue was closed prematurely before
// the PR merged. It does NOT relax any other criterion.
const waiverQualified =
approvals >= FAST_TRACK_HIGH_APPROVAL_WAIVER_THRESHOLD &&
!hasChangesRequested(pr.latestReviews);
const waiverApplied = waiverQualified && linkedOpenIssues.length === 0;

if (!hasAllowedPrefix(pr.title)) {
reasons.push(
`title prefix must be one of: ${FAST_TRACK_PREFIXES.join(', ')}`
Expand All @@ -224,7 +255,7 @@ export function evaluateEligibility(
reasons.push(`CI checks must be SUCCESS (found ${ciState})`);
}

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

Expand All @@ -238,6 +269,7 @@ export function evaluateEligibility(
approvals,
ciState,
linkedOpenIssues,
waiverApplied,
};
}

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

Expand Down Expand Up @@ -421,9 +454,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.waiverApplied ? ' [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