diff --git a/.github/workflows/label-failed-prs.yml b/.github/workflows/label-failed-prs.yml new file mode 100644 index 00000000..df0a13d8 --- /dev/null +++ b/.github/workflows/label-failed-prs.yml @@ -0,0 +1,104 @@ +name: Label Failed Pull Requests + +on: + workflow_run: + types: [completed] + workflow_dispatch: + +permissions: + actions: read + contents: read + pull-requests: write + issues: write + +jobs: + label-failed-prs: + runs-on: ubuntu-latest + steps: + - name: Reconcile failed-action labels + uses: actions/github-script@v7 + with: + script: | + const label = 'action-failed'; + const failedConclusions = new Set([ + 'failure', + 'timed_out', + 'startup_failure', + 'action_required' + ]); + + async function reconcile(prNumber) { + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + if (pr.state !== 'open') return; + + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: pr.head.sha, + per_page: 100 + } + ); + + const relevantRuns = runs.filter(run => + run.name !== context.workflow && + run.event === 'pull_request' && + run.status === 'completed' + ); + + const hasFailure = relevantRuns.some(run => + failedConclusions.has(run.conclusion) + ); + + const labels = pr.labels.map(item => item.name); + const hasLabel = labels.includes(label); + + if (hasFailure && !hasLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [label] + }); + core.info(`Added ${label} to PR #${prNumber}`); + } else if (!hasFailure && hasLabel) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: label + }); + core.info(`Removed ${label} from PR #${prNumber}`); + } else { + core.info(`PR #${prNumber}: no label change required`); + } + } + + if (context.eventName === 'workflow_dispatch') { + const prs = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100 + }); + core.info(`Reconciling ${prs.length} open PR(s)`); + for (const pr of prs) await reconcile(pr.number); + return; + } + + const run = context.payload.workflow_run; + if (!run || run.name === context.workflow) return; + + const prNumbers = new Set((run.pull_requests || []).map(pr => pr.number)); + if (prNumbers.size === 0) { + core.info('Completed workflow run is not associated with an open pull request.'); + return; + } + + for (const prNumber of prNumbers) await reconcile(prNumber);