Skip to content

PR Workflow Failure Notifier #2929

PR Workflow Failure Notifier

PR Workflow Failure Notifier #2929

# 🚨 COPILOT PROHIBITION WARNING 🚨
#
# THIS FILE IS CRITICAL TO DEPLOYMENT WORKFLOW MONITORING AND PR MANAGEMENT
#
# ⛔ COPILOT AGENTS ARE STRICTLY PROHIBITED FROM MAKING ANY CHANGES TO THIS FILE
# ⛔ WITHOUT EXPLICIT WRITTEN CONSENT FROM THE REPOSITORY OWNER (@litlfred)
#
# This workflow monitors deployment failures and manages PR notifications.
# Changes can disrupt the deployment feedback system and PR management.
#
# 🔒 REQUIRED PROCESS FOR CHANGES:
# 1. Request explicit consent from @litlfred in a GitHub comment
# 2. Wait for written approval before making ANY changes
# 3. Document the explicit consent in the commit message
# 4. Test extensively in a separate environment before merging
#
# 🚨 END PROHIBITION WARNING 🚨
name: PR Workflow Failure Notifier
# This workflow runs after any workflow fails and posts a rich comment on the associated PR
# It provides:
# - Link to repository
# - Link to workflow run (for logs)
# - Link to workflow YAML definition file
# - Workflow chain information
# - Error log information
# Requirements met:
# - Minimal intrusion: runs separately using workflow_run trigger
# - Centralized failure detection: monitors all workflows in one place
# - Rich PR comments: includes all required metadata and links
# - No per-workflow customization needed: automatic detection
# - Maintainable: single file with robust error handling
on:
workflow_run:
workflows: [
"Deploy Feature Branch",
"Deploy Landing Page",
"PR Commit Feedback",
"PR Review Deployment",
"Code Quality Checks"
]
types:
- completed
permissions:
contents: read
pull-requests: write
actions: read
jobs:
notify-pr-on-failure:
# Only run if the workflow failed
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
steps:
- name: Get workflow failure details and find associated PR
id: failure_details
uses: actions/github-script@v8
with:
script: |
const { workflow_run } = context.payload;
console.log(`Workflow "${workflow_run.name}" failed`);
console.log(`Run ID: ${workflow_run.id}`);
console.log(`Conclusion: ${workflow_run.conclusion}`);
// Get associated PRs for this workflow run
const prs = workflow_run.pull_requests || [];
if (prs.length === 0) {
console.log('No associated PRs found for this workflow run');
core.setOutput('has_pr', 'false');
return;
}
const pr = prs[0]; // Use the first PR if multiple
console.log(`Found associated PR #${pr.number}`);
// Gather workflow metadata
const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
const runUrl = workflow_run.html_url;
// Try to determine workflow file name from the workflow name
let workflowFileName = '';
const workflowNameMap = {
'Deploy Feature Branch': 'branch-deployment.yml',
'Deploy Landing Page': 'landing-page-deployment.yml',
'PR Commit Feedback': 'pr-commit-feedback.yml',
'PR Review Deployment': 'review.yml',
'Code Quality Checks': 'code-quality.yml'
};
workflowFileName = workflowNameMap[workflow_run.name] ||
(workflow_run.name.toLowerCase().replace(/\s+/g, '-') + '.yml');
const workflowYamlUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/.github/workflows/${workflowFileName}`;
// Analyze workflow chain - check if this was triggered by another workflow
let chainInfo = `- **Workflow**: ${workflow_run.name} ([YAML](${workflowYamlUrl}))`;
if (workflow_run.triggering_actor) {
chainInfo += `\n ↳ **Triggered by**: ${workflow_run.triggering_actor.login}`;
}
// Add trigger event information
if (workflow_run.event) {
chainInfo += `\n ↳ **Event**: ${workflow_run.event}`;
}
// Store all the information for the next step
core.setOutput('has_pr', 'true');
core.setOutput('pr_number', pr.number.toString());
core.setOutput('workflow_name', workflow_run.name);
core.setOutput('run_id', workflow_run.id.toString());
core.setOutput('repo_url', repoUrl);
core.setOutput('run_url', runUrl);
core.setOutput('workflow_yaml_url', workflowYamlUrl);
core.setOutput('workflow_file_name', workflowFileName);
core.setOutput('chain_info', chainInfo);
core.setOutput('head_sha', workflow_run.head_sha);
core.setOutput('head_branch', workflow_run.head_branch);
- name: Get workflow logs for error details
id: get_logs
if: steps.failure_details.outputs.has_pr == 'true'
uses: actions/github-script@v8
with:
script: |
const runId = '${{ steps.failure_details.outputs.run_id }}';
try {
// Get the jobs for this workflow run to find failed jobs
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId
});
console.log(`Found ${jobs.total_count} jobs for run ${runId}`);
// Find failed jobs
const failedJobs = jobs.jobs.filter(job => job.conclusion === 'failure');
console.log(`Found ${failedJobs.length} failed jobs`);
let errorSummary = '';
if (failedJobs.length > 0) {
errorSummary = '### Failed Jobs:\n';
for (const job of failedJobs.slice(0, 3)) { // Limit to first 3 failed jobs
errorSummary += `- **${job.name}**: ${job.conclusion}\n`;
if (job.steps) {
const failedSteps = job.steps.filter(step => step.conclusion === 'failure');
if (failedSteps.length > 0) {
errorSummary += ` - Failed steps: ${failedSteps.map(s => s.name).join(', ')}\n`;
}
}
}
if (failedJobs.length > 3) {
errorSummary += `- ... and ${failedJobs.length - 3} more failed jobs\n`;
}
} else {
errorSummary = '### Error Details:\nJob failure details not available. Check the workflow logs for more information.\n';
}
core.setOutput('error_summary', errorSummary);
} catch (error) {
console.error('Error fetching job details:', error);
core.setOutput('error_summary', '### Error Details:\nUnable to fetch detailed error information. Check the workflow logs for more details.\n');
}
- name: Post failure notification comment on PR
if: steps.failure_details.outputs.has_pr == 'true'
uses: actions/github-script@v8
with:
script: |
const prNumber = parseInt('${{ steps.failure_details.outputs.pr_number }}');
const workflowName = '${{ steps.failure_details.outputs.workflow_name }}';
const runId = '${{ steps.failure_details.outputs.run_id }}';
const repoUrl = '${{ steps.failure_details.outputs.repo_url }}';
const runUrl = '${{ steps.failure_details.outputs.run_url }}';
const workflowYamlUrl = '${{ steps.failure_details.outputs.workflow_yaml_url }}';
const chainInfo = '${{ steps.failure_details.outputs.chain_info }}';
const errorSummary = '${{ steps.get_logs.outputs.error_summary }}';
const headSha = '${{ steps.failure_details.outputs.head_sha }}';
const headBranch = '${{ steps.failure_details.outputs.head_branch }}';
const failedAt = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
// Create comprehensive failure comment
const commentBody = "❌ **PR Workflow Failure Notification**\n\n" +
"- [Repo Cabinet (Source)](" + repoUrl + ")\n" +
"- [Workflow Run](" + runUrl + ")\n" +
"- [Workflow Definition](" + workflowYamlUrl + ")\n\n" +
"### Workflow Chain:\n" +
chainInfo + "\n\n" +
"### Failure Details:\n" +
"- **Branch**: `" + headBranch + "`\n" +
"- **Commit**: `" + (headSha ? headSha.substring(0, 7) : 'unknown') + "`\n" +
"- **Failed at**: " + failedAt + "\n\n" +
errorSummary + "\n\n" +
"### 🔧 Next Steps:\n" +
"1. **Check Logs**: Click the [Workflow Run](" + runUrl + ") link above to view detailed error logs\n" +
"2. **Review Code**: Check the failed commit for syntax errors, build issues, or test failures\n" +
"3. **Fix Issues**: Address the errors shown in the logs\n" +
"4. **Retry**: Push new commits or manually rerun the workflow\n\n" +
"### 🔄 Quick Actions:\n" +
"- 📄 [View detailed logs](" + runUrl + ")\n" +
"- 🔄 [Rerun workflow](" + repoUrl + "/actions/workflows/" + workflowName.toLowerCase().replace(/\\s+/g, '-') + ".yml)\n" +
"- 📝 [View workflow file](" + workflowYamlUrl + ")\n\n" +
"---\n" +
"💡 *This is an automated notification for workflow failures. Fix the issues above and the next workflow run should succeed.*";
try {
// Check for existing failure notification comments to avoid duplicates
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
// Look for existing workflow failure comments for this specific run
const existingComment = comments.find(comment =>
comment.body.includes('PR Workflow Failure Notification') &&
comment.body.includes(runId)
);
if (existingComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: commentBody
});
console.log(`✅ Updated existing failure notification comment for run ${runId} on PR #${prNumber}`);
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody
});
console.log(`✅ Posted new failure notification comment for run ${runId} on PR #${prNumber}`);
}
} catch (error) {
console.error('Error posting failure comment:', error);
// Don't fail the workflow if comment posting fails
}