|
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. |
4 | 4 | name: Add CI Passed Label |
5 | 5 |
|
6 | 6 | on: |
| 7 | + pull_request_target: |
| 8 | + types: |
| 9 | + - synchronize |
| 10 | + - reopened |
| 11 | + - labeled |
| 12 | + - unlabeled |
7 | 13 | workflow_run: |
8 | 14 | workflows: ["CI Check"] |
9 | 15 | types: |
10 | 16 | - completed |
11 | 17 |
|
12 | 18 | permissions: |
13 | 19 | pull-requests: write |
14 | | - checks: read |
15 | | - actions: read |
| 20 | + issues: write |
16 | 21 |
|
17 | 22 | jobs: |
18 | 23 | fetch_data: |
19 | | - name: Fetch workflow payload |
| 24 | + name: Fetch PR eligibility |
20 | 25 | runs-on: ubuntu-latest |
21 | 26 | if: > |
22 | | - github.event.workflow_run.event == 'pull_request' && |
| 27 | + github.event_name == 'workflow_run' && |
23 | 28 | github.event.workflow_run.conclusion == 'success' |
24 | 29 | 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 }} |
27 | 32 | steps: |
28 | | - - name: 'Download artifact' |
| 33 | + - name: Check PR label eligibility |
| 34 | + id: pr |
29 | 35 | uses: actions/github-script@v8 |
30 | 36 | with: |
31 | 37 | 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({ |
33 | 55 | owner: context.repo.owner, |
34 | 56 | 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}`, |
39 | 59 | }); |
40 | 60 |
|
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}.` |
45 | 65 | ); |
| 66 | + core.setOutput("eligible", "false"); |
| 67 | + return; |
46 | 68 | } |
47 | 69 |
|
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"); |
56 | 72 |
|
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"); |
59 | 76 |
|
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 | | - |
68 | 77 | reset_ci_passed_label: |
69 | | - name: Reset 'ci-passed' label on PR Synchronization |
| 78 | + name: Reset stale 'ci-passed' label |
70 | 79 | 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 | + ) |
72 | 93 | steps: |
73 | | - - name: Check and reset label |
| 94 | + - name: Remove existing 'ci-passed' label |
74 | 95 | 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." |
78 | 105 | fi |
79 | 106 | env: |
80 | 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
81 | 108 |
|
82 | 109 | add_ci_passed_label: |
83 | 110 | 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' |
86 | 114 | steps: |
87 | 115 | - name: Add 'ci-passed' label |
88 | 116 | run: | |
|
0 commit comments