Skip to content

bug(ci): both nightlies post a false ✅ "clean" to PRs whose suite never ran #2029

Description

@obasilakis

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

  • A test job that fails before producing a verdict results in no sticky comment change for that PR — neither created nor updated
  • Verified for each reachable path: checkout failure, PR-head fetch failure, non-conflict merge failure, missing JUnit XML
  • regression=false is only written when a diff actually ran, or when the merge legitimately conflicted
  • A guard test covers the unknown-verdict path in each workflow — the existing suites assert the happy verdicts and have no coverage of the absent one
  • Applied to backend-unit-nightly.yml and integration-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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions