Skip to content

[finding] Nothing distinguishes "zero packages affected" from "the affected-set computation silently produced nothing" — an empty changed-files.txt is never checked #10057

Description

@os-steve

Found while testing whether the shard partitioner's emptiness guard actually fires, as part of #10046. Filed unassigned. This is a latent gap, not a live bug — the mitigations below are real and were clearly designed in; recording it because the residual hole is the exact shape (#4690) the surrounding machinery exists to prevent, and nothing currently names it.

What was measured

scripts/partition-test-shards.mjs's shape assertion works as its header advertises. Fed deliberately broken payloads it exits 1 and names the cause:

items key removed -> exit=1
items is an object -> exit=1

But an empty document and a populated one are indistinguishable to it, by design — both exit 0:

EMPTY (count 0, items []) -> exit=0, stderr "shard 1/3: 0/0 packages, weight 0", 0 stdout lines
NON-EMPTY (count 2, items 2) -> exit=0, stderr "shard 1/3: 1/2 packages, weight 414", 1 stdout line

Zero stdout lines then short-circuits the test step in ci.yml:

if [ ! -s "$RUNNER_TEMP/shard-packages.txt" ]; thenecho "No packages on this shard — nothing to test."exit 0fi

— a green Test Core that ran nothing.

Why the partitioner is the wrong place to fix it

Zero is frequently the correct answer here, unlike #4690's scanner:

  • a PR touching only .github/, content/docs/ or scripts/ (with no declared cross-package glob hit) genuinely affects no package;
  • with 3 shards, any run with fewer than 3 packages legitimately leaves a shard empty.

So a blanket "red on empty" at the partitioner would be wrong. The header already states the contract correctly — zero lines means "nothing to run", NOT "no filter".

The mitigations that already exist

Worth stating so nobody re-derives them: every failing producer falls back to the full package list, never to an empty one. ci.yml has three such branches — unresolvable merge-base, a failed git diff, and a failed --union-into — each falling back to turbo ls --output=json over the whole workspace. Steps run under bash -e, so a non-zero turbo ls --affected fails the step outright. A crashing selection therefore cannot yield an empty shard.

The residual hole

What none of that covers is a producer that exits 0 with a wrong, plausible, empty answer. The concrete unchecked signal:

if ! git diff --name-only "$SCM_BASE" HEAD > "$RUNNER_TEMP/changed-files.txt" \|| ! node scripts/check-cross-package-test-inputs.mjs ... ; then

Only the exit status of that git diff is checked. Its emptiness is not. A PR whose merge-base resolved to something wrong-but-valid produces an empty changed-file list, which yields zero affected packages, which yields a green shard that tested nothing — and every log line along the way reads as a normal quiet PR.

An empty changed-files.txt on a pull_request event is not a legitimate state: a PR always differs from its merge base. That makes it a decidable signal, unlike shard emptiness itself, and it is the one place where "no packages selected" and "selection failed" can actually be told apart.

Suggested shape (deliberately not implemented here): treat an empty changed-files.txt on a pull_request event as a reason to take the existing full-package-list fallback, with a ::warning:: naming it — same posture as the merge-base fallback next to it (#6195), same reasoning: doubt costs minutes, not coverage.

⚠️ Note the shard attestation (#6082) does not cover this: it attests "shard N of 3 ran and every step passed", which is exactly what a shard that tested nothing does.

Refs: #10046 (where this was measured), #4690 ("zero is a broken scan, not a clean repo"), #6195 (the merge-base fallback whose posture this would follow), #6082 (shard attestation), #7802.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions