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',
+ });
+ }