diff --git a/.github/workflows/report-workflow-failures.yml b/.github/workflows/report-workflow-failures.yml new file mode 100644 index 00000000000..8c367668552 --- /dev/null +++ b/.github/workflows/report-workflow-failures.yml @@ -0,0 +1,202 @@ +name: Report workflow failures + +on: + workflow_run: + workflows: + - Codespaces Prebuilds + types: + - completed + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.workflow_id }} + +jobs: + create-or-update-issue: + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + runs-on: ubuntu-latest + permissions: + actions: read + issues: write + steps: + - name: Create or update issue + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const workflowRun = context.payload.workflow_run; + const latestRuns = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowRun.workflow_id, + status: 'completed', + per_page: 1, + }); + const latestRun = latestRuns.data.workflow_runs[0]; + + if ( + latestRun?.id !== workflowRun.id || + latestRun?.run_attempt !== workflowRun.run_attempt + ) { + return; + } + + const workflow = await github.rest.actions.getWorkflow({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowRun.workflow_id, + }); + const title = `Investigate ${workflow.data.name} workflow failure`; + const issueMarkerText = `workflow-failure:${workflowRun.workflow_id}`; + const issueMarker = ``; + const issues = await github.paginate(github.rest.search.issuesAndPullRequests, { + q: [ + `repo:${context.repo.owner}/${context.repo.repo}`, + 'is:issue', + 'is:open', + `"${issueMarkerText}"`, + 'in:body', + ].join(' '), + per_page: 100, + }); + const failureIssues = issues + .filter( + (issue) => + !issue.pull_request && issue.body?.includes(issueMarker), + ) + .sort((a, b) => a.number - b.number); + + const jobs = await github.paginate( + github.rest.actions.listJobsForWorkflowRun, + { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: workflowRun.id, + filter: 'latest', + per_page: 100, + }, + ); + const failedJobs = jobs.filter((job) => job.conclusion === 'failure'); + const escapeHtml = (text) => + text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + const failedJobListItems = failedJobs.map( + (job) => + `- ${escapeHtml(job.name)}`, + ); + + if (failedJobListItems.length === 0) { + failedJobListItems.push(`- Failed run`); + } + + const body = [ + issueMarker, + 'The following jobs are failing in this workflow:', + '', + ...failedJobListItems, + '', + 'cc @github/primer-engineering to investigate and fix', + ].join('\n'); + + const issue = { + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + }; + + let issueNumber; + + if (failureIssues.length > 0) { + await github.rest.issues.update({ + ...issue, + issue_number: failureIssues[0].number, + }); + issueNumber = failureIssues[0].number; + await Promise.all( + failureIssues.slice(1).map((duplicate) => + github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: duplicate.number, + state: 'closed', + state_reason: 'not_planned', + }), + ), + ); + } else { + const createdIssue = await github.rest.issues.create(issue); + issueNumber = createdIssue.data.number; + } + + try { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + assignees: ['copilot'], + }); + } catch (error) { + core.warning( + `Failed to assign @copilot to issue #${issueNumber}. This is expected if Copilot assignment is not enabled for this repository. Error: ${error?.message ?? String(error)}`, + ); + } + + close-issue: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + permissions: + actions: read + issues: write + steps: + - name: Close issue + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const workflowRun = context.payload.workflow_run; + const latestRuns = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowRun.workflow_id, + status: 'completed', + per_page: 1, + }); + const latestRun = latestRuns.data.workflow_runs[0]; + + if ( + latestRun?.id !== workflowRun.id || + latestRun?.run_attempt !== workflowRun.run_attempt + ) { + return; + } + + const issueMarkerText = `workflow-failure:${workflowRun.workflow_id}`; + const issueMarker = ``; + const issues = await github.paginate(github.rest.search.issuesAndPullRequests, { + q: [ + `repo:${context.repo.owner}/${context.repo.repo}`, + 'is:issue', + 'is:open', + `"${issueMarkerText}"`, + 'in:body', + ].join(' '), + per_page: 100, + }); + const failureIssues = issues.filter( + (issue) => + !issue.pull_request && issue.body?.includes(issueMarker), + ); + + for (const issue of failureIssues) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'completed', + }); + }