7474 }
7575 }
7676
77+ // Fetch required status check names from the branch ruleset.
78+ // Uses the public /rules/branches endpoint — no admin token needed.
79+ // Falls back to blocking on all checks if the endpoint is unavailable.
80+ let requiredCheckNames = null;
81+ try {
82+ const { data: rules } = await github.request(
83+ 'GET /repos/{owner}/{repo}/rules/branches/{branch}',
84+ { owner, repo, branch: 'main' },
85+ );
86+ const statusRule = rules.find(r => r.type === 'required_status_checks');
87+ if (statusRule) {
88+ requiredCheckNames = new Set(
89+ statusRule.parameters.required_status_checks.map(c => c.context),
90+ );
91+ core.info(`Required checks: ${[...requiredCheckNames].join(', ')}`);
92+ }
93+ } catch (err) {
94+ core.warning(`Could not fetch branch rules, falling back to all checks: ${err.message}`);
95+ }
96+
7797 const failures = [];
7898
7999 for (const pr of prs) {
85105 continue;
86106 }
87107
88- // Check aggregate CI status for the PR's head commit.
89- // We combine check runs (Actions) and commit statuses (external CIs).
108+ // Check CI status for required checks on the PR's head commit only.
109+ // Scoping to required checks avoids advisory checks (e.g. codecov/patch)
110+ // incorrectly blocking label assignment on otherwise-ready PRs.
90111 const [checkRuns, commitStatusRes] = await Promise.all([
91112 github.paginate(github.rest.checks.listForRef, {
92113 owner, repo, ref: pr.head.sha, per_page: 100,
@@ -96,28 +117,38 @@ jobs:
96117 }),
97118 ]);
98119
99- // Exclude this workflow's own check run to avoid self-referential loops.
100- const relevantRuns = checkRuns.filter(
101- run => run.name !== 'Reconcile PR review state labels',
102- );
120+ // Filter to required checks only (or all checks if rules unavailable).
121+ // Always exclude this workflow's own run to avoid self-referential loops.
122+ const relevantRuns = checkRuns.filter(run => {
123+ if (run.name === 'Reconcile PR review state labels') return false;
124+ return requiredCheckNames ? requiredCheckNames.has(run.name) : true;
125+ });
126+
127+ // For commit statuses (external CIs), there's no per-status name filtering
128+ // available from getCombinedStatusForRef — it aggregates all statuses.
129+ // If required checks are known, we only use commitStatus as a signal when
130+ // no required check runs exist for this ref (i.e. pure status-based CI).
131+ const useCommitStatus = !requiredCheckNames || relevantRuns.length === 0;
103132
104- core.debug(`PR #${pr.number}: ${relevantRuns.length} check run(s), commit status=${commitStatusRes.data.state}`);
133+ core.debug(`PR #${pr.number}: ${relevantRuns.length} required check run(s), commit status=${commitStatusRes.data.state} (used=${useCommitStatus}) `);
105134 for (const run of relevantRuns) {
106135 core.debug(` check: "${run.name}" status=${run.status} conclusion=${run.conclusion}`);
107136 }
108137
109138 const ciPending = relevantRuns.some(
110139 run => run.status === 'queued' || run.status === 'in_progress',
111- ) || commitStatusRes.data.state === 'pending';
140+ ) || (useCommitStatus && commitStatusRes.data.state === 'pending') ;
112141
113142 const ciFailed = !ciPending && (
114143 relevantRuns.some(
115144 run => run.status === 'completed' &&
116145 run.conclusion !== 'success' &&
117146 run.conclusion !== 'skipped' &&
118147 run.conclusion !== 'neutral',
119- ) || commitStatusRes.data.state === 'failure' ||
148+ ) || (useCommitStatus && (
149+ commitStatusRes.data.state === 'failure' ||
120150 commitStatusRes.data.state === 'error'
151+ ))
121152 );
122153
123154 // While CI is running or has failed, remove state labels and move on.
0 commit comments