Skip to content

Commit 1981e0d

Browse files
authored
fix: prevent stale ci-passed labels (#13095)
* fix: prevent stale ci-passed labels - remove stale ci-passed labels immediately on synchronize and reopened - re-add ci-passed only after a successful CI Check on a label-eligible PR - guard against race condition where an older workflow_run could re-add the label after a new push by comparing head SHAs - check whether ci-passed label exists before removal to avoid silently swallowing real API errors - add issues: write permission required by the Labels API - remove artifact handoff from ci-checks.yml; derive PR metadata directly from workflow_run Signed-off-by: Jaison Paul <paul.jaison@gmail.com> * fix: handle ci-passed label eligibility transitions Signed-off-by: Jaison Paul <paul.jaison@gmail.com> * fix: guard label triggers to avoid wasted CI runner minutes Promote label eligibility checks from step-level to job-level conditions so ineligible PRs are 'skipped' instead of 'success', preventing wasted downstream workflow_run triggers. Guard 'unlabeled' events to only react to eligibility-relevant label changes (needs-ok-to-test removal in ci-checks, ok-to-test removal in add-ci-passed-label), avoiding the 50-min polling loop restart on unrelated label changes like size/L or lgtm. Signed-off-by: Jaison Paul <paul.jaison@gmail.com> * fix: add symmetric guard for labeled events in CI check workflow Skip the polling loop when unrelated labels (size/L, lgtm, etc.) are added to an eligible PR. Only the 'ok-to-test' label addition can change eligibility, mirroring the existing 'unlabeled' guard. Signed-off-by: Jaison Paul <paul.jaison@gmail.com> --------- Signed-off-by: Jaison Paul <paul.jaison@gmail.com>
1 parent 371e9eb commit 1981e0d

File tree

2 files changed

+87
-74
lines changed

2 files changed

+87
-74
lines changed

.github/workflows/add-ci-passed-label.yml

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,116 @@
1-
# This workflow adds the 'ci-passed' label to a pull request once the 'CI Check' workflow completes successfully.
2-
# Resets the 'ci-passed' label status when a pull request is synchronized or reopened,
3-
# indicating that changes have been pushed and CI needs to rerun.
1+
# This workflow removes a stale 'ci-passed' label whenever a PR is updated
2+
# or its eligibility labels change, then adds it back only after the
3+
# 'CI Check' workflow completes successfully for a label-eligible PR.
44
name: Add CI Passed Label
55

66
on:
7+
pull_request_target:
8+
types:
9+
- synchronize
10+
- reopened
11+
- labeled
12+
- unlabeled
713
workflow_run:
814
workflows: ["CI Check"]
915
types:
1016
- completed
1117

1218
permissions:
1319
pull-requests: write
14-
checks: read
15-
actions: read
20+
issues: write
1621

1722
jobs:
1823
fetch_data:
19-
name: Fetch workflow payload
24+
name: Fetch PR eligibility
2025
runs-on: ubuntu-latest
2126
if: >
22-
github.event.workflow_run.event == 'pull_request' &&
27+
github.event_name == 'workflow_run' &&
2328
github.event.workflow_run.conclusion == 'success'
2429
outputs:
25-
pr_number: ${{ steps.extract.outputs.pr_number }}
26-
event_action: ${{ steps.extract.outputs.event_action }}
30+
pr_number: ${{ steps.pr.outputs.pr_number }}
31+
eligible: ${{ steps.pr.outputs.eligible }}
2732
steps:
28-
- name: 'Download artifact'
33+
- name: Check PR label eligibility
34+
id: pr
2935
uses: actions/github-script@v8
3036
with:
3137
script: |
32-
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
38+
const workflowEvent = context.payload.workflow_run.event;
39+
if (workflowEvent !== "pull_request" && workflowEvent !== "pull_request_target") {
40+
core.info(`Skipping workflow_run event ${workflowEvent}`);
41+
core.setOutput("eligible", "false");
42+
return;
43+
}
44+
45+
const headRepositoryOwner = context.payload.workflow_run.head_repository?.owner?.login;
46+
const headBranch = context.payload.workflow_run.head_branch;
47+
const runHeadSha = context.payload.workflow_run.head_sha;
48+
if (!headRepositoryOwner || !headBranch || !runHeadSha) {
49+
core.info("workflow_run is missing head repository, branch, or SHA metadata.");
50+
core.setOutput("eligible", "false");
51+
return;
52+
}
53+
54+
const { data: pullRequests } = await github.rest.pulls.list({
3355
owner: context.repo.owner,
3456
repo: context.repo.repo,
35-
run_id: ${{github.event.workflow_run.id}},
36-
});
37-
const matchArtifact = artifacts.data.artifacts.find((artifact) => {
38-
return artifact.name === "pr";
57+
state: "open",
58+
head: `${headRepositoryOwner}:${headBranch}`,
3959
});
4060
41-
if (!matchArtifact) {
42-
throw new Error(
43-
`Required artifact "pr" was not found for workflow run ID ${{github.event.workflow_run.id}}. ` +
44-
"It may have expired or failed to upload. Ensure the CI workflow uploads a 'pr' artifact."
61+
const pullRequest = pullRequests.find((candidate) => candidate.head.sha === runHeadSha);
62+
if (!pullRequest) {
63+
core.info(
64+
`No open pull request matched ${headRepositoryOwner}:${headBranch} at ${runHeadSha}.`
4565
);
66+
core.setOutput("eligible", "false");
67+
return;
4668
}
4769
48-
const download = await github.rest.actions.downloadArtifact({
49-
owner: context.repo.owner,
50-
repo: context.repo.repo,
51-
artifact_id: matchArtifact.id,
52-
archive_format: 'zip',
53-
});
54-
const fs = require('fs');
55-
fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data));
70+
const labels = new Set(pullRequest.labels.map((label) => label.name));
71+
const eligible = labels.has("ok-to-test") && !labels.has("needs-ok-to-test");
5672
57-
- name: Unzip artifact
58-
run: unzip pr.zip
73+
core.info(`PR #${pullRequest.number} labels: ${Array.from(labels).join(", ")}`);
74+
core.setOutput("pr_number", String(pullRequest.number));
75+
core.setOutput("eligible", eligible ? "true" : "false");
5976
60-
- name: Extract PR information
61-
id: extract
62-
run: |
63-
pr_number=$(cat ./pr_number)
64-
event_action=$(cat ./event_action)
65-
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
66-
echo "event_action=${event_action}" >> "$GITHUB_OUTPUT"
67-
6877
reset_ci_passed_label:
69-
name: Reset 'ci-passed' label on PR Synchronization
78+
name: Reset stale 'ci-passed' label
7079
runs-on: ubuntu-latest
71-
needs: fetch_data
80+
# Only run on pull_request_target events that can actually affect ci-passed:
81+
# - synchronize/reopened: new commits invalidate previous CI results
82+
# - labeled with 'needs-ok-to-test': PR became ineligible
83+
# - unlabeled with 'ok-to-test': PR became ineligible
84+
# This avoids spinning up a runner for unrelated label changes.
85+
if: >
86+
github.event_name == 'pull_request_target' &&
87+
(
88+
github.event.action == 'synchronize' ||
89+
github.event.action == 'reopened' ||
90+
(github.event.action == 'labeled' && github.event.label.name == 'needs-ok-to-test') ||
91+
(github.event.action == 'unlabeled' && github.event.label.name == 'ok-to-test')
92+
)
7293
steps:
73-
- name: Check and reset label
94+
- name: Remove existing 'ci-passed' label
7495
run: |
75-
if [[ "${{ needs.fetch_data.outputs.event_action }}" == "synchronize" || "${{ needs.fetch_data.outputs.event_action }}" == "reopened" ]]; then
76-
echo "Resetting 'ci-passed' label as changes were pushed (event: ${{ needs.fetch_data.outputs.event_action }})."
77-
gh pr edit ${{ needs.fetch_data.outputs.pr_number }} --remove-label "ci-passed" --repo "$GITHUB_REPOSITORY" || echo "Label not present"
96+
pr_number=${{ github.event.pull_request.number }}
97+
echo "Checking for stale 'ci-passed' label on PR #${pr_number} after ${{ github.event.action }}"
98+
labels=$(gh pr view "${pr_number}" --repo "$GITHUB_REPOSITORY" --json labels --jq '.labels[].name')
99+
100+
if echo "${labels}" | grep -qx 'ci-passed'; then
101+
echo "Removing stale 'ci-passed' label from PR #${pr_number}"
102+
gh pr edit "${pr_number}" --remove-label "ci-passed" --repo "$GITHUB_REPOSITORY"
103+
else
104+
echo "Label 'ci-passed' not present on PR #${pr_number}; nothing to remove."
78105
fi
79106
env:
80107
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81108

82109
add_ci_passed_label:
83110
name: Add 'ci-passed' label
84-
runs-on: ubuntu-latest
85-
needs: [fetch_data, reset_ci_passed_label]
111+
runs-on: ubuntu-latest
112+
needs: fetch_data
113+
if: needs.fetch_data.outputs.eligible == 'true'
86114
steps:
87115
- name: Add 'ci-passed' label
88116
run: |

.github/workflows/ci-checks.yml

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,28 @@
1-
# This workflow checks if all CI checks have passed by polling every 5 minutes for a total of 8 attempts.
1+
# This workflow checks if all CI checks have passed by polling every 5 minutes.
22
name: CI Check
33

44
on:
55
pull_request_target:
6-
types: [opened, synchronize, reopened, labeled]
6+
types: [opened, synchronize, reopened, labeled, unlabeled]
77

88
jobs:
99
check_ci_status:
1010
runs-on: ubuntu-latest
11+
# Only run for eligible PRs: must have 'ok-to-test' and not 'needs-ok-to-test'.
12+
# For 'labeled' events, only react when 'ok-to-test' is added.
13+
# For 'unlabeled' events, only react when 'needs-ok-to-test' is removed.
14+
# These are the only label changes that can change eligibility, avoiding
15+
# restarting the 50-min polling loop on unrelated labels like 'size/L' or 'lgtm'.
16+
# Using a job-level condition so ineligible PRs are 'skipped' rather than 'success',
17+
# which prevents a wasted downstream workflow_run trigger.
18+
if: >
19+
(github.event.action != 'labeled' || github.event.label.name == 'ok-to-test') &&
20+
(github.event.action != 'unlabeled' || github.event.label.name == 'needs-ok-to-test') &&
21+
contains(github.event.pull_request.labels.*.name, 'ok-to-test') &&
22+
!contains(github.event.pull_request.labels.*.name, 'needs-ok-to-test')
1123
permissions:
1224
checks: read
1325
steps:
14-
- name: Check out the repository
15-
uses: actions/checkout@v6
16-
17-
- name: Check for 'needs-ok-to-test' and 'ok-to-test' labels
18-
id: label_check
19-
run: |
20-
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'needs-ok-to-test') }}" == "true" ]]; then
21-
echo "Label 'needs-ok-to-test' found. Skipping the workflow."
22-
exit 0
23-
fi
24-
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ok-to-test') }}" == "true" ]]; then
25-
echo "Label 'ok-to-test' found. Continuing the workflow."
26-
else
27-
echo "Label 'ok-to-test' not found. Skipping the workflow."
28-
exit 0
29-
fi
30-
3126
- name: Check if all CI checks passed
3227
uses: wechuli/allcheckspassed@0b68b3b7d92e595bcbdea0c860d05605720cf479
3328
with:
@@ -41,13 +36,3 @@ jobs:
4136
checks_exclude: '^Cleanup artifacts$,^Upload results$,^Agent$,^Prepare$'
4237
env:
4338
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44-
- name: Save PR payload
45-
shell: bash
46-
run: |
47-
mkdir -p ./pr
48-
echo ${{ github.event.pull_request.number }} >> ./pr/pr_number
49-
echo ${{ github.event.action }} >> ./pr/event_action
50-
- uses: actions/upload-artifact@v7
51-
with:
52-
name: pr
53-
path: pr/

0 commit comments

Comments
 (0)