Explain CI failure #6
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
| # © 2026 NetApp, Inc. All Rights Reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # See the NOTICE file in the repo root for trademark and attribution details. | |
| name: Explain CI failure | |
| # When CI / PR Guard / Validate Examples fails on a PR, post a sticky | |
| # comment with plain-English fix guidance keyed to the failing job. | |
| # | |
| # Runs in the workflow_run context so it has API write permission even | |
| # for fork PRs (the fork's PR-context workflows cannot write back). | |
| # Safe because no untrusted code is checked out — we only read REST API | |
| # metadata and post a comment. | |
| on: | |
| workflow_run: | |
| workflows: ["CI", "PR Guard", "Validate Examples"] | |
| types: [completed] | |
| permissions: | |
| pull-requests: write | |
| actions: read | |
| contents: read | |
| jobs: | |
| explain: | |
| name: Post fix guidance | |
| if: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const STICKY_KEY = '<!-- pace:explain-failure -->'; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const run = context.payload.workflow_run; | |
| const repoUrl = `https://github.com/${owner}/${repo}`; | |
| // Map failing job name -> contributor-facing fix guidance. | |
| // Job names must match the `name:` of each job in the upstream workflows. | |
| const HINTS = { | |
| 'validate-and-lint': | |
| '**Ruff lint or format check failed.** Reproduce locally:\n' + | |
| '```bash\n' + | |
| 'make lint # see what failed\n' + | |
| 'ruff check python/ --fix # auto-fix lint\n' + | |
| 'ruff format python/ # auto-fix format\n' + | |
| '```\n' + | |
| 'Then commit and push.', | |
| 'commitlint': | |
| '**Commit message does not follow Conventional Commits.** Required format:\n' + | |
| '```\n<type>(<scope>): <description>\n```\n' + | |
| 'Valid types: `build, chore, ci, doc, feat, fix, perf, refactor, revert, style, test`. ' + | |
| 'Valid scopes: `python, ansible, terraform, docs, ci, deps`.\n\n' + | |
| 'To rewrite the last commit:\n' + | |
| '```bash\n' + | |
| 'git commit --amend -m "feat(python): your description"\n' + | |
| 'git push --force-with-lease\n' + | |
| '```', | |
| 'secret-scan': | |
| '**TruffleHog flagged a possible leaked credential** in your diff. ' + | |
| 'Even if it looks like a placeholder, it can fail when it matches a real secret format.\n\n' + | |
| '1. Replace the value with an obvious placeholder (`changeme`, `<PASSWORD>`).\n' + | |
| '2. **If you ever pushed a real secret, rotate it immediately.** Force-push is not enough — ' + | |
| 'the value is in git history on the remote.\n' + | |
| '3. If it is a verified false positive, add the path to `.trufflehogignore`.', | |
| 'Validate YAML syntax': | |
| '**A changed YAML file failed parsing.** Open it in your editor — the YAML extension ' + | |
| 'highlights the bad line. Common causes: tabs (use spaces), unquoted special characters, ' + | |
| 'mismatched indentation.', | |
| 'Ansible — syntax & lint': | |
| '**`ansible-playbook --syntax-check` or `ansible-lint` failed.** Reproduce locally:\n' + | |
| '```bash\nmake ansible-lint\n```\n' + | |
| `See [docs/troubleshooting.md](${repoUrl}/blob/main/docs/troubleshooting.md#ansible-issues) for common Ansible errors.`, | |
| 'Terraform — fmt, validate & lint': | |
| '**Terraform format, validate, or tflint failed.** Reproduce locally:\n' + | |
| '```bash\nmake terraform-validate\n```\n' + | |
| 'Most failures are formatting — fix all of them with:\n' + | |
| '```bash\nterraform fmt -recursive terraform/\n```', | |
| }; | |
| const prs = run.pull_requests || []; | |
| if (prs.length === 0) { | |
| core.info('workflow_run has no associated PR; nothing to comment on.'); | |
| return; | |
| } | |
| const issue_number = prs[0].number; | |
| const { data: jobsData } = await github.rest.actions.listJobsForWorkflowRun({ | |
| owner, repo, run_id: run.id, per_page: 100, | |
| }); | |
| const failedJobs = jobsData.jobs | |
| .filter(j => j.conclusion === 'failure') | |
| .map(j => j.name); | |
| if (failedJobs.length === 0) { | |
| core.info('No failed jobs in this run.'); | |
| return; | |
| } | |
| const sections = failedJobs.map(name => { | |
| const hint = HINTS[name] || | |
| `**${name}** failed — open the [run logs](${run.html_url}) to see the cause.`; | |
| return `### ${name}\n\n${hint}`; | |
| }); | |
| const body = | |
| `${STICKY_KEY}\n` + | |
| `### A CI check failed — here is how to fix it\n\n` + | |
| `**Workflow:** [${run.name}](${run.html_url}) · ` + | |
| `**Run #${run.run_number}** · ` + | |
| `**Failed jobs:** ${failedJobs.length}\n\n` + | |
| sections.join('\n\n---\n\n') + '\n\n' + | |
| `Push a fix and CI re-runs automatically; this comment updates with the next failure (or stays put if the same check fails again). ` + | |
| `Stuck? Comment on the PR and a maintainer will help — typical response time is 1 business day.\n\n` + | |
| `<sub>Auto-generated · ` + | |
| `[explain-failure.yml](${repoUrl}/blob/main/.github/workflows/explain-failure.yml)</sub>`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const existing = comments.find(c => c.body && c.body.includes(STICKY_KEY)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, repo, comment_id: existing.id, body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number, body, | |
| }); | |
| } |