Skip to content

Explain CI failure

Explain CI failure #6

# © 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,
});
}