fix(cicd): exclude spec- and docs-only PRs from the Release QA report - #37484
fix(cicd): exclude spec- and docs-only PRs from the Release QA report#37484nollymar wants to merge 1 commit into
Conversation
Since Spec-Kit landed, every feature ships two PRs and PR 1 carries spec.md alone under specs/<issue>-<slug>/. Those PRs contain nothing runnable, so QA has nothing to exercise and they never earn a QA label — yet the release QA report counted them as un-QA'd, inflating the Slack warning and @-mentioning their authors on every release. Pure documentation PRs had the same problem. The report had no idea what files a PR touched: classifyExclusion only ever read authorType, author, labels and title. So: - github.ts: new fetchChangedFiles, batched GraphQL (20 PRs per query) rather than one REST listFiles per PR. Unlike fetchClosingIssueRefs it swallows errors instead of re-throwing — an unknown file list just means no path-based exclusion, i.e. the previous behaviour, so failing the whole QA section over it would be the worse trade. - exclusions.ts: isDocumentationPath + a 4th rule, applied after the title heuristics so a bot-authored spec PR still reads bot-author. Documentation is specs/**, docs/** and loose *.md / *.mdx. Agent tooling is checked first and stays in QA scope (.claude/, .agents/, .cursor/, .specify/, any CLAUDE.md / AGENTS.md) — in this repo that markdown *is* the deliverable, and without the exception .claude/skills/x/SKILL.md would match the markdown pattern and drop a real feature out of the report. - types.ts: spec-only and docs-only reasons; changedFiles?: string[] where undefined means unknown, deliberately distinct from []. Fail safe throughout: a PR is excluded only on a file list fetched in full whose every path is documentation. An absent or truncated list keeps the PR in QA scope — over-reporting beats silently hiding a code change. Excluded PRs also now render on a clean release. Both renderers previously returned before that section when nothing was flagged, and after this change the common case is exactly that — nothing flagged *because* the gaps were spec PRs — which would leave the drop in counts unexplained. Slack is untouched and still silent when nothing needs review. Verified against v26.09.03-01...v26.09.09-01 (38 PRs): missing 25 -> 17, excluded 0 -> 9. All 9 re-checked independently via gh — every one genuinely spec/docs-only, and no docs-only PR was left behind. The truncation guard fired for real on #37423 (>100 files), which correctly stayed in QA scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @nollymar's task in 1m 25s —— View job Code Review — Release QA report filtering
This is a well-designed, well-documented change. The New Issues
Notes (non-blocking)
No security concerns: PR numbers are re-validated with Verdict: No blocking issues. The one Medium is an informational observation about batch-error granularity, not a defect — the outcome is safe in every case. |
Closes #37486
Proposed Changes
The release QA report (
.github/scripts/release-qa-status) flags every merged PR whose linked issue lacks aQA : Passed/QA : Not Needed/QA : Failedlabel. Since Spec-Kit landed, every feature ships two PRs and PR 1 carriesspec.mdalone underspecs/<issue>-<slug>/. Those PRs contain nothing runnable, so QA has nothing to exercise and they never earn a QA label — yet the report counted them as un-QA'd, inflating the:rotating_light:Slack warning and @-mentioning their authors on every release. Pure documentation PRs had the same problem.The report had no idea what files a PR touched:
classifyExclusiononly ever readauthorType,author,labelsandtitle. So:github.ts— newfetchChangedFiles: batched GraphQL (20 PRs per query) rather than one RESTlistFilesper PR, so the cost is roughly one extra request per twenty on top of the 3+ REST calls per PR the tool already makes. Unlike its siblingfetchClosingIssueRefsit swallows GraphQL errors instead of re-throwing — there, an empty result silently demotes PRs tounlinkedand floods Slack, so failing loudly is right; here an unknown file list just means no path-based exclusion, i.e. exactly the previous behaviour, and killing the whole QA section over it would be the worse trade.exclusions.ts— newisDocumentationPath()plus a 4th rule inclassifyExclusion, applied after the title heuristics so a bot-authored spec PR still readsbot-author.types.ts— two newExclusionReasonvalues (spec-only,docs-only) andchangedFiles?: string[], whereundefinedmeans unknown and is deliberately distinct from[].format.ts— Excluded-section blurbs name the new reasons, and that section now renders on a clean release (see Additional Info).What counts as documentation:
specs/**,docs/**, and loose*.md/*.mdx.What stays in QA scope — agent tooling is checked first:
.claude/,.agents/,.cursor/,.specify/, and anyCLAUDE.md/AGENTS.mdat any depth. In this repo that markdown is the deliverable — #37309 (feat(skills): add dot-pr-spec-summary) shipped a whole feature without touching a non-markdown file. Without the exception,.claude/skills/x/SKILL.mdwould match the markdown pattern and drop a real feature out of the report.Fail safe throughout: a PR is excluded only when its file list was fetched in full and every path is documentation. An absent or truncated list keeps the PR in QA scope — over-reporting beats silently hiding a code change. GitHub caps
firstat 100 and this isn't paginated past that, which is a judgement call rather than an oversight: the list exists only to answer "is every file documentation?", and a 100+ file PR never is.Blast radius is CI tooling only — a standalone TypeScript CLI run by
.github/workflows/cicd_6-release.yml. No new files, no changes to the workflow, and nothing underdotCMS/orcore-web/.Checklist
Number.isInteger(n) && n > 0before alias interpolation, matching the existing guard infetchClosingIssueRefsAdditional Info
Verified against a real release —
v26.09.03-01...v26.09.09-01, 38 PRs, run before and after the change:All 9 newly-excluded PRs were re-checked independently via
gh, not through the code under test: every one is genuinely spec/docs-only (#37103, #37188, #37189, #37190, #37392, #37404, #37430, #37434, #37437). The reverse check found no false negatives — no docs-only PR was left in the flagged buckets. The truncation guard also fired for real on #37423 (>100 files), which correctly stayed in QA scope.78 tests pass (
cd .github/scripts/release-qa-status && npm ci && npm test);npx tsc --noEmitis clean.Two things a reviewer should weigh:
renderText/renderMarkdown. Both bailed onflagged === 0before the Excluded section. After this change the common case is exactly that — nothing flagged because the gaps were spec PRs — so the drop in counts would go unexplained. The Excluded table now renders on clean releases too, keeping the skip list auditable.renderSlackis untouched and still returns the empty string when nothing needs review, so this adds no Slack noise. This is a small behaviour change beyond the strict minimum; easy to revert if you'd rather keep the early return.dotcms agent setup#37392 movedpassed→excluded. A spec PR whose linked issue already carriedQA : Passed. Correct under the new rules, but it does meanpassedno longer counts spec PRs that happened to be labelled.Unrelated observation, not fixed here:
npm start -- --format json > filewrites npm's own banner ahead of the JSON, so a redirect isn't valid JSON. Pre-existing and harmless in the workflow (markdown into$GITHUB_STEP_SUMMARY), but worth knowing if anyone scripts against the JSON output.Screenshots
n/a — CLI output only. Text format on the verification run:
🤖 Generated with Claude Code