Summary
Both nightly sweeps post a ✅ clean sticky comment to a PR whose suite never ran. When the test job fails before a verdict is written, Write status JSON still runs (if: always()), the unset output stringifies to false, and the comment script's final else branch renders the green tick.
This is the #1941 defect one job downstream. #1941 was "a non-zero exit is not proof of a content conflict"; this is "a job that failed is not proof that nothing regressed." Both come from a verdict being inferred from an absent signal rather than from evidence.
The direction matters: #1941 produced a false alarm (annoying, self-limiting — someone eventually investigates). This produces a false all-clear on a regression detector, which nothing corrects.
Affected
| Workflow |
State |
Severity |
backend-unit-nightly.yml |
on dev today |
latent — reachable when the checkout or PR-head fetch fails |
backend-unit-nightly.yml (PR #1952) |
in review |
adds a deliberate exit 1 branch that routes into it |
integration-nightly.yml (PR #1963) |
in review |
same, and creates a new comment rather than only updating one |
#1963 is the worst case: the unit nightly guards creation with } else if (status.regression || status.merge_conflict), so a false clean can only overwrite an existing sticky. The integration nightly's } else { calls createComment unconditionally, and it is a new workflow where no PR has a sticky yet — so the first bad night posts a fresh green tick to up to 20 PRs.
Mechanism
- name: Write status JSON
if: always() # runs even when the job failed
run: |
merge_conflict='${{ steps.merge.outputs.merge_conflict }}' # unset -> ''
regression='${{ steps.diff.outputs.regression }}' # unset -> ''
if [ -z "$regression" ]; then regression="false"; fi
jq -n ... '{merge_conflict: ($merge_conflict == "true"), # '' == "true" -> false
regression: ($regression == "true")}' # false
{merge_conflict: false, regression: false} is indistinguishable from a genuine clean run, so the comment job renders:
✅ Nightly unit-suite clean when this PR is merged into dev
The comment job is if: always() with needs: [discover, test] and never inspects needs.test.result, so a failed test job is invisible to it.
Reachable paths: base checkout fails · PR-head fetch fails · the merge fails for a non-conflict reason (#1952's new branch) · the stack never boots or writes no JUnit (#1963's :274-279). #1963's :271-273 explicitly claims the comment step "posts nothing for this PR rather than something false" — it posts something false.
Suggested fix
Absence of a verdict is its own state and must not collapse into "clean". Refuse to write the status file:
- name: Write status JSON
if: always()
run: |
merge_conflict='${{ steps.merge.outputs.merge_conflict }}'
if [ -z "$merge_conflict" ]; then
echo "::warning::merge verdict unknown — no status written, sticky left untouched"
exit 0
fi
Both comment jobs enumerate status-pr*.json, so a missing file means that PR is skipped and any existing sticky is left alone. Uploads are already if-no-files-found: warn.
Applies to the regression side too: regression defaulting to false is correct only when the merge conflicted (the diff legitimately never ran) — not when the diff step was skipped because the job died.
A stronger belt, if wanted: have the comment job consult needs.test.result and refuse to render a clean verdict when the matrix did not succeed.
Acceptance criteria
Related
Summary
Both nightly sweeps post a ✅ clean sticky comment to a PR whose suite never ran. When the
testjob fails before a verdict is written,Write status JSONstill runs (if: always()), the unset output stringifies tofalse, and the comment script's finalelsebranch renders the green tick.This is the #1941 defect one job downstream. #1941 was "a non-zero exit is not proof of a content conflict"; this is "a job that failed is not proof that nothing regressed." Both come from a verdict being inferred from an absent signal rather than from evidence.
The direction matters: #1941 produced a false alarm (annoying, self-limiting — someone eventually investigates). This produces a false all-clear on a regression detector, which nothing corrects.
Affected
backend-unit-nightly.ymldevtodaybackend-unit-nightly.yml(PR #1952)exit 1branch that routes into itintegration-nightly.yml(PR #1963)#1963 is the worst case: the unit nightly guards creation with
} else if (status.regression || status.merge_conflict), so a false clean can only overwrite an existing sticky. The integration nightly's} else {callscreateCommentunconditionally, and it is a new workflow where no PR has a sticky yet — so the first bad night posts a fresh green tick to up to 20 PRs.Mechanism
{merge_conflict: false, regression: false}is indistinguishable from a genuine clean run, so the comment job renders:The
commentjob isif: always()withneeds: [discover, test]and never inspectsneeds.test.result, so a failedtestjob is invisible to it.Reachable paths: base checkout fails · PR-head fetch fails · the merge fails for a non-conflict reason (#1952's new branch) · the stack never boots or writes no JUnit (#1963's
:274-279). #1963's:271-273explicitly claims the comment step "posts nothing for this PR rather than something false" — it posts something false.Suggested fix
Absence of a verdict is its own state and must not collapse into "clean". Refuse to write the status file:
Both comment jobs enumerate
status-pr*.json, so a missing file means that PR is skipped and any existing sticky is left alone. Uploads are alreadyif-no-files-found: warn.Applies to the regression side too:
regressiondefaulting tofalseis correct only when the merge conflicted (the diff legitimately never ran) — not when the diff step was skipped because the job died.A stronger belt, if wanted: have the
commentjob consultneeds.test.resultand refuse to render a clean verdict when the matrix did not succeed.Acceptance criteria
testjob that fails before producing a verdict results in no sticky comment change for that PR — neither created nor updatedregression=falseis only written when a diff actually ran, or when the merge legitimately conflictedbackend-unit-nightly.ymlandintegration-nightly.yml(coordinate with fix(ci): give the nightly's merge check a common ancestor (#1941) #1952 / ci: give the live-instance integration suite a nightly CI home (#1896) #1963 to avoid conflicting edits)Related
if ! git mergecollapsing every failure into "conflict"); itsdocs/memory/learnings.mdentry generalises the rule this violates: "anything else must fail the job loudly rather than post a confident wrong answer." The job does fail loudly. The comment job does not hear about it.