Skip to content
Open
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
82 changes: 82 additions & 0 deletions .github/workflows/label-pr-review-state.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ permissions:
issues: write
checks: read
statuses: write
# Reading a commit's parents is metadata only; no PR code is checked out or executed.
contents: read

concurrency:
group: label-pr-review-state
Expand Down Expand Up @@ -86,6 +88,11 @@ jobs:
color: 'c2e0c6',
description: 'Fresh community approval on the current head; maintainer review still required',
},
{
name: 'stacked',
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
color: 'c2c2c2',
description: 'Head commit sits on another open PR, so its diff is only this unit',
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
},
];
const guideMarker = '<!-- zoo-code-pr-review-process -->';
const codeRabbitLabelMarkerPrefix = '<!-- coderabbit-review-label:';
Expand Down Expand Up @@ -170,12 +177,24 @@ jobs:
return pr.head?.repo?.full_name && pr.head.repo.full_name !== pr.base?.repo?.full_name;
}

const stackedLabel = 'stacked';
if (!isReadOnlyRun) {
for (const label of labelDefinitions) {
try {
await github.rest.issues.getLabel({ owner, repo, name: label.name });
} catch (error) {
if (error.status !== 404) throw error;
if (label.name === stackedLabel) {
// stacked is only a reviewer filter, so a failed provisioning must not stop
// reconciliation for every PR. Every other managed label still fails closed,
// because reconciliation cannot work without it.
try {
await github.rest.issues.createLabel({ owner, repo, ...label });
} catch (createError) {
core.warning(`Could not create the ${stackedLabel} label: ${createError.message}; continuing without it`);
}
continue;
}
await github.rest.issues.createLabel({ owner, repo, ...label });
}
}
Expand Down Expand Up @@ -244,6 +263,54 @@ jobs:
}
}

// A stacked unit is detected from the commit graph: its head commit has exactly one
// parent, and that parent is another open PR's head. The label describes that detected
// relationship only -- it does not claim the PR targets the branch below it, which is
// how GitHub describes a focused stacked diff. Reviewers filter on this label to tell a
// unit apart from a standalone PR. It is orthogonal to the review-state labels, so it is
// reconciled separately and never removed by reconcileLabels.

async function buildOpenPrHeads() {
const candidates = await github.paginate(github.rest.pulls.list, {
owner, repo, state: 'open', per_page: 100,
});
const heads = new Map();
for (const candidate of candidates) {
if (candidate.head?.sha) heads.set(candidate.head.sha.toLowerCase(), candidate.number);
}
return heads;
}

async function reconcileStackedLabel(pr, openPrHeads) {
if (openPrHeads === null) {
// The map could not be read this run; leave the label as it is rather than
// deciding from an empty map.
return;
}
if (isReadOnlyRun && isForkPR(pr)) {
core.info(`PR #${pr.number}: fork PR on a read-only run — skipping the stacked label`);
return;
}
try {
const { data: commit } = await github.rest.git.getCommit({ owner, repo, commit_sha: pr.head.sha });
const parents = Array.isArray(commit.parents) ? commit.parents : [];
const parentPr = parents.length === 1
? openPrHeads.get(String(parents[0].sha).toLowerCase())
: undefined;
const hasLabel = pr.labels.some(label => label.name === stackedLabel);
if (hasLabel && parentPr === undefined) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: stackedLabel });
pr.labels = pr.labels.filter(label => label.name !== stackedLabel);
} else if (parentPr && !hasLabel) {
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [stackedLabel] });
pr.labels = pr.labels.concat([{ name: stackedLabel }]);
core.info(`PR #${pr.number}: stacked on #${parentPr}`);
}
} catch (error) {
core.warning(`PR #${pr.number}: could not reconcile the stacked label: ${error.message}`);
}
}

async function findReviewGuide(pr) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
Expand Down Expand Up @@ -534,8 +601,23 @@ jobs:

const failures = [];

// Only read the open-PR map when there is a PR to reconcile, so an
// unassociated workflow run still resolves to zero API reads.
// A failed open-PR list call must not stop review-state reconciliation. The map is left
// unset so reconcileStackedLabel skips label mutation for this run: an empty map would
// look like "no open parents" and remove valid stacked labels.
let openPrHeads = null;
if (prs.length > 0) {
try {
openPrHeads = await buildOpenPrHeads();
} catch (error) {
core.warning(`Could not read the open pull request map: ${error.message}; skipping the stacked label for this run`);
}
}

for (const pr of prs) {
let failurePhase = 'ci-pending';
await reconcileStackedLabel(pr, openPrHeads);
try {
const selfReferentialRequirements = (requiredChecks ?? []).filter(check =>
check.context === reviewGateName || check.context === reconciliationCheckName
Expand Down
Loading
Loading