Post Merge Actions #362
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Post Merge Actions | |
| on: | |
| pull_request: | |
| types: [closed] | |
| workflow_run: | |
| workflows: ["Run CI unittests"] | |
| types: [completed] | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: write | |
| pull-requests: read | |
| jobs: | |
| notify-ci-failure: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' || github.event.workflow_run.conclusion != 'success' | |
| steps: | |
| - name: Find failed CI run | |
| id: failed-ci | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const GRAALVMBOT_LOGIN = "graalvmbot"; | |
| const TARGET_WORKFLOW_NAME = "Run CI unittests"; | |
| async function getPullRequest(prNumber) { | |
| const {data: pr} = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| return pr; | |
| } | |
| async function wasClosedByGraalVmBot(prNumber) { | |
| for await (const response of github.paginate.iterator(github.rest.issues.listEvents, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| per_page: 100, | |
| })) { | |
| for (const event of response.data) { | |
| if (event.event === "closed" && event.actor && event.actor.login === GRAALVMBOT_LOGIN) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| async function isEligiblePullRequest(pr, checkCloseActor) { | |
| if (!pr || !pr.number || pr.state !== "closed") { | |
| console.log("PR is not closed. Skipping CI failure notification."); | |
| return false; | |
| } | |
| if (checkCloseActor && !(await wasClosedByGraalVmBot(pr.number))) { | |
| console.log(`PR #${pr.number} was not closed by ${GRAALVMBOT_LOGIN}. Skipping CI failure notification.`); | |
| return false; | |
| } | |
| const assignees = pr.assignees || []; | |
| if (assignees.length !== 1) { | |
| console.log(`Expected exactly 1 assignee, found ${assignees.length}. Skipping CI failure notification.`); | |
| return false; | |
| } | |
| return true; | |
| } | |
| async function alreadyCommented(prNumber, marker) { | |
| for await (const response of github.paginate.iterator(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| per_page: 100, | |
| })) { | |
| if (response.data.some(comment => comment.body && comment.body.includes(marker))) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| let pr = null; | |
| let failedRun = null; | |
| let checkCloseActor = false; | |
| if (context.eventName === "pull_request") { | |
| pr = context.payload.pull_request; | |
| const sender = context.payload.sender; | |
| if (!sender || sender.login !== GRAALVMBOT_LOGIN) { | |
| console.log(`PR closed by ${sender ? sender.login : "unknown"}, not ${GRAALVMBOT_LOGIN}. Skipping CI check.`); | |
| return; | |
| } | |
| if (!(await isEligiblePullRequest(pr, false))) { | |
| return; | |
| } | |
| const runsResp = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: "ci-unittests.yml", | |
| head_sha: pr.head.sha, | |
| }); | |
| failedRun = runsResp.data.workflow_runs.find(run => | |
| run.name === TARGET_WORKFLOW_NAME && | |
| run.head_sha === pr.head.sha && | |
| run.status === "completed" && | |
| run.conclusion !== "success" | |
| ); | |
| if (!failedRun) { | |
| console.log("No completed failed CI workflow found for the PR yet. The workflow_run trigger will handle a later failure."); | |
| return; | |
| } | |
| } else if (context.eventName === "workflow_run") { | |
| const run = context.payload.workflow_run; | |
| if (!run || run.name !== TARGET_WORKFLOW_NAME) { | |
| console.log("Not the target workflow_run event."); | |
| return; | |
| } | |
| if (run.conclusion === "success") { | |
| console.log("CI workflow succeeded. No notification needed."); | |
| return; | |
| } | |
| if (!run.pull_requests || run.pull_requests.length === 0) { | |
| console.log("Workflow run has no associated pull request."); | |
| return; | |
| } | |
| pr = await getPullRequest(run.pull_requests[0].number); | |
| checkCloseActor = true; | |
| failedRun = run; | |
| } else { | |
| console.log(`Unsupported event: ${context.eventName}`); | |
| return; | |
| } | |
| if (!(await isEligiblePullRequest(pr, checkCloseActor))) { | |
| return; | |
| } | |
| const marker = `<!-- graalpy-ci-post-merge-failure:${pr.head.sha} -->`; | |
| if (await alreadyCommented(pr.number, marker)) { | |
| console.log(`Failure notification was already posted for PR #${pr.number} and SHA ${pr.head.sha}.`); | |
| return; | |
| } | |
| const assignees = pr.assignees || []; | |
| core.setOutput('assignee', assignees[0].login); | |
| core.setOutput('failed_run_url', failedRun.html_url); | |
| core.setOutput('failed_run_id', failedRun.id); | |
| core.setOutput('pr_number', pr.number); | |
| core.setOutput('comment_marker', marker); | |
| console.log(`Found failed CI workflow: ${failedRun.html_url}`); | |
| - name: Download merged test report | |
| if: ${{ steps.failed-ci.outputs.failed_run_url != '' }} | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: merged_test_reports | |
| path: report | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ steps.failed-ci.outputs.failed_run_id }} | |
| continue-on-error: true | |
| - name: Post failure comment | |
| if: ${{ steps.failed-ci.outputs.failed_run_url != '' }} | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const assignee = '${{ steps.failed-ci.outputs.assignee }}'; | |
| const runUrl = '${{ steps.failed-ci.outputs.failed_run_url }}'; | |
| const prNumber = Number('${{ steps.failed-ci.outputs.pr_number }}'); | |
| const marker = '${{ steps.failed-ci.outputs.comment_marker }}'; | |
| let body = `${marker}\n@${assignee} - CI workflow failed: [View workflow](${runUrl})`; | |
| try { | |
| const reportPath = 'report/merged_test_reports.json'; | |
| if (fs.existsSync(reportPath)) { | |
| const data = JSON.parse(fs.readFileSync(reportPath, 'utf8')); | |
| const failed = data.map(t => t.name); | |
| if (failed.length) { | |
| const list = failed.map(n => `- ${n}`).join('\n'); | |
| body = `${marker}\n@${assignee} - CI workflow failed: [View workflow](${runUrl})\nFailed tests:\n\n${list}`; | |
| } | |
| } | |
| } catch (e) { | |
| console.log(`Error parsing test report: ${e}`); | |
| } | |
| await github.rest.issues.createComment({ | |
| issue_number: prNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body, | |
| }); |