Skip to content

npm e2e: stop Yarn dying of EPIPE when a grep -q closes its pipe - #468

Merged
schmitch merged 1 commit into
mainfrom
claude/npm-e2e-epipe
Oct 24, 2026
Merged

schmitch merged 1 commit into
mainfrom
claude/npm-e2e-epipe

Conversation

@schmitch

Copy link
Copy Markdown
Member

The npm-e2e job (from #463) can fail on a race in its own script, as it did in the merge queue's run of #465. That run had the same tree that had passed on #465's own PR run.

── legacy login with Yarn, and logout ──
Error: write EPIPE
    at Wt.writeLine (…/yarn/package/bin/yarn.js:177:6121)
FAIL: yarn npm whoami

node yarn.js npm whoami | grep -q "$admin_user": grep -q exits at its first match. When Yarn writes another line after that, it writes into a closed pipe, dies of the unhandled EPIPE, and with set -o pipefail the check fails although the whoami was right. Whether Yarn's last write lands before or after grep exits is timing, hence the intermittent failure.

Change

.github/scripts/npm-e2e.sh now reads a client's output whole and then compares it, as its other checks already do ([ "$(npm whoami …)" = … ]). The same applies to the other checks that piped a client into grep -q:

  • yarn npm whoami;
  • the two npm view … version | grep -qx checks;
  • npm dist-tag ls … | grep -q legacy && fail. Besides the race, that check passed silently when npm dist-tag ls itself failed.

No server code changes, so there is no changelog fragment.

Tests

  • .github/scripts/npm-e2e.sh locally with npm 11.21, pnpm 11.5 and Yarn 4 (fetched by the script): passed.
  • bash -n on the script.

Generated by Claude Code

grep -q exits at its first match, so a client that writes after it writes
into a closed pipe. Yarn dies of that (EPIPE, an unhandled error), and
with pipefail `yarn npm whoami | grep -q` failed the merge queue's run
of #465 although the whoami was right. The whoami, the two npm view
checks and the dist-tag ls check now capture the output and compare it,
the way the script's other checks do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BaGpNGgkQV54XvgL5D9ay2
@schmitch
schmitch added this pull request to the merge queue Oct 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 24, 2026

Copy link
Copy Markdown
Member Author

The merge queue's run of this PR (run 776) failed one check, test, in crates/cache/tests/l1_unit.rs. This PR only changes .github/scripts/npm-e2e.sh, and the same check passed on its PR run.

a_factory_chosen_lifetime_shortens_the_ttl ... FAILED
assertion `left == right` failed: still live  (left: 2, right: 1)   l1_unit.rs:524

The test gives an entry a 100 ms lifetime and expects it still live on the very next call. On a runner busy with the parallel test binaries, more than 100 ms passed between the two calls, so the entry had expired and the loader ran again. No fix for it exists yet. One that keeps the test's meaning with half-second margins (about 5 s instead of 0.8 s):

-    assert_eq!(load(Duration::from_millis(100)).await, 1);
-    assert_eq!(load(Duration::from_millis(100)).await, 1, "still live");
-    tokio::time::sleep(Duration::from_millis(200)).await;
-    assert_eq!(load(Duration::from_secs(60)).await, 2, "expired at 100 ms");
-    tokio::time::sleep(Duration::from_millis(300)).await;
-    assert_eq!(load(Duration::from_secs(60)).await, 2, "capped at the TTL");
-    tokio::time::sleep(Duration::from_millis(300)).await;
-    assert_eq!(load(Duration::from_secs(60)).await, 3, "expired at 500 ms");
+    // TTL 3 s (the `Duration::from_millis(500)` above becomes `from_secs(3)`).
+    assert_eq!(load(Duration::from_secs(1)).await, 1);
+    assert_eq!(load(Duration::from_secs(1)).await, 1, "still live");
+    tokio::time::sleep(Duration::from_millis(1500)).await;
+    assert_eq!(load(Duration::from_secs(60)).await, 2, "expired at 1 s");
+    tokio::time::sleep(Duration::from_secs(1)).await;
+    assert_eq!(load(Duration::from_secs(60)).await, 2, "capped at the TTL");
+    tokio::time::sleep(Duration::from_millis(2500)).await;
+    assert_eq!(load(Duration::from_secs(60)).await, 3, "expired at 3 s");

It belongs in a pull request of its own, not in this one. Re-queueing this PR should pass, since the race rarely hits.


Generated by Claude Code

@schmitch
schmitch added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 4d36ade Oct 24, 2026
18 checks passed
@schmitch
schmitch deleted the claude/npm-e2e-epipe branch October 24, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants