Skip to content

fix(ci): classify-paths SIGPIPE race can skip Go/SDK suites #447

Description

@EricAndrechek

Area: ci — fail-open gating bug · found while running make ci on an unrelated SDK branch

scripts/classify-paths.sh can report code=false for a change set that is code, which makes CI and the pre-push hook skip the Go and SDK test suites.

Cause

Both classification branches pipe into grep -q under set -euo pipefail (line 42, and the same shape at line 49):

if printf '%s\n' "$files" | grep -qvE '^(docs/|.*\.md$|…)'; then
  echo "code=true"
else
  echo "code=false"
fi

grep -q exits as soon as its condition is decided. printf is then writing to a closed pipe, takes SIGPIPE (141), and pipefail makes that the pipeline's exit status — so the if takes the else branch even though grep succeeded.

It only bites when grep can exit early, i.e. when the first path already decides the answer. That's why dep-bump-go (go.mod go.sum — first line is non-matching, so grep -qv exits after line 1) is the single failing case, while mixed-docs-go passes: its first line docs/x.md matches, so grep reads on and printf finishes first.

Reproduction

Deterministic once the input is big enough that printf can't win the race:

$ bash -c 'set -euo pipefail; files=$(printf "go.mod\n%.0s" {1..200000});
  if printf "%s\n" "$files" | grep -qvE "^(docs/|.*\.md$)"; then echo code=true; else echo "code=false <-- WRONG"; fi'
code=false <-- WRONG

At the 2-line size the unit test uses it is load-dependent: scripts/classify-paths.test.sh passes standalone (5/5 for me) and passed in a pre-commit make verify, then failed minutes later on the same tree under make ci's 14-way parallel verify:

FAIL dep-bump-go        want code=true docs=false, got code=false docs=false

Why it matters

This is fail-open, and it is worse in CI than locally. scripts/ci/classify-changes.sh pipes the GitHub API file list through this script, and real PRs carry many more paths than the test's two — more input means printf is more likely to still be writing when grep exits, not less. A misclassification silently drops the Go + SDK suites for that run, and the aggregator still reports green. The local pre-push hook consults the same classifier to decide between the full make ci marker and the lighter make verify marker.

Line 49 (docs=) has the identical shape and the same latent race; it just needs a first-path match to trigger.

Suggested fix

Drop the pipe so there is no SIGPIPE to propagate — a here-string keeps the script dependency-free and pure:

if grep -qvE '^(docs/|…)' <<<"$files"; then

Worth a test that pins the failure mode rather than only the happy path, since the current case only catches it probabilistically — e.g. assert against an input large enough to force the race deterministically.

Introduced in #312. Not urgent-looking day to day precisely because it is intermittent, which is the argument for fixing it rather than re-running.

Activity

  1. coderabbitai commented on Aug 10, 2026

    @coderabbitai
    🔗 Related PRs

    #147 - chore: claude code native improvements [closed]
    #312 - ci: split ci into a job dag and isolate docs deploys from pr code [closed]
    #329 - ci: drop the #312 transition guards and v1 cache fallbacks [closed]


    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. EricAndrechek commented on Oct 7, 2026

    @EricAndrechek
    MemberAuthor

    Fixed by #549. scripts/classify-paths.sh:54-60 now passes the file list to grep with a here-string, so there is no pipe for a SIGPIPE to break, and the large-input case in scripts/classify-paths.test.sh pins it with 5,000 paths. Part of #740.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions