Fix #1436: retry streamdeck validate on transient network errors - #1451
Conversation
The Elgato CLI's manifestUrlsExist rule does a live HEAD request to the manifest's URL field; any fetch error other than ENOTFOUND (UND_ERR_SOCKET, ECONNRESET, 'fetch failed', ...) is rethrown and crashes the whole validate run, flaking unrelated PRs' CI (#1432, #1434) with no code defect. Wrap the validate invocation in a bounded retry (3 attempts, exponential backoff) that retries ONLY transient network failures; real validation errors fail fast. Both CI workflows call 'pnpm validate', so this covers both sites. Regression test simulates a transient failure then success (fails without the retry, passes with it) and pins fail-fast on real errors.
…e spawn errors - Drop over-broad 'network' substring (could false-retry a manifest mentioning it); add specific ENETUNREACH/ENETDOWN codes. - Surface the spawn error message when the CLI can't be launched, instead of exiting 1 with empty output. - Clarify the deliberate EAI_AGAIN (retry) vs ENOTFOUND (fail-fast) asymmetry.
|
Integration review (streamdeck architect) — APPROVE. Low-risk tier (CI/build script), read line-by-line. The part that lifts this above "wrap it in a retry": the builder went and found the actual failure path in the CLI rather than pattern-matching the symptom. The Verified myself:
Consultation was a full 3-way — gemini APPROVE, codex APPROVE, claude COMMENT with nits addressed — so this lane did not run under the degraded-board policy; codex availability is restored. |
Shorter, verb-first name for the validate retry wrapper.
Name the wrapper scripts/validate.mjs and the exported loop runWithBackoff; no 'retry' in file or identifier names.
Note on my integration review above: it is stale in its file references, and I want that on the record rather than discovered later. I reviewed this PR when the helper was The red is real and is this lane's own defect, not the known flakes: Two process observations worth recording, since both cost real signal today:
|
…t-error check-types (tsc, separate from vitest) failed: the multi-line import's @ts-expect-error covered 'import {' while TS7016 landed on the 'from' line, leaving it unsuppressed and the directive unused (TS2578). Fix the cause, not the directive placement: add scripts/validate.d.mts so the NodeNext import resolves with real types, and drop the suppression. The test now gets genuine type-checking of the helper's API.
|
Correcting my comment above — two claims in it were wrong. Evidence first, then what actually holds. I checked the branch's run history rather than its current state, and the Wrong claim 1: "a pre-gate green hid a genuine defect behind a stale result." There was no green to go stale. The failure was visible on every push; nobody read the workflow it was in. The merge-window problem (#1462) is real for delaying merges and invalidating measurements, but it concealed nothing here, and I withdraw the framing — as the maintainer has withdrawn their own version of it. Wrong claim 2: the renames caused the Identical pair, identical lines, original filenames. The defect predates the renames entirely; they carried it along unchanged. My account was a plausible story fitted to a correlation (renames landed, then a stale directive appeared) instead of a log I had not yet read. What actually holds, and it is the whole of it: the directive sits on line 2 immediately above What I am taking from this, since it is the third time today I have explained an observation with a mechanism I had not verified: "is anything failing right now" is a different question from "has this branch ever been green", and only the run history answers the second. Reading current state and inferring history is the same error family as trusting an exit code or a "not found" — a signal that is true about an instant, mistaken for a fact about a state. My approval remains stale on filenames regardless, and I will re-verify substance against the current files before this merges. |
|
Re-anchored integration review against head The fix is structural, not cosmetic. It adds I checked the declaration against the implementation, since a hand-written Substance from my original review still holds under the new names:
On the renames, for the record: both were the owner's direct instructions to the builder after my review ("validate with retry is verbose for a file name", then dropping the term retry altogether, which also drove |
Summary
The streamdeck
validateCI step intermittently failed unrelated PRs (#1432, #1434) withUND_ERR_SOCKET/fetch failedand no code defect. This wraps the validate invocation so a transient network hiccup no longer fails the job: it runs the command again a few times with backoff before giving up.Fixes #1436
Root Cause
Not a schema-update fetch (as the issue title guessed). Traced to the Elgato CLI's
manifestUrlsExistvalidation rule (@elgato/cli,src/validation/plugin/rules/manifest-urls-exist.ts): it does a livefetch(URL, { method: 'HEAD' })against the manifest's top-levelURL(ours ishttps://github.com/cluesmith/codev) on every run. Its catch block turns onlyENOTFOUNDinto a graceful validation error; any other fetch failure (UND_ERR_SOCKET, ECONNRESET, ETIMEDOUT, "fetch failed") is rethrown and crashes the wholevalidatecommand — putting the network on CI's pass/fail path.Approach — and why
Bounded re-attempts with backoff (the architect's stated preference), not offline/caching. I verified the offline route empirically:
streamdeck validate --helpexposes--no-update-check("Disables updating schemas") and--force-update-check, but those gate only the schema update fetch — they do not disable the manifest-URL reachability probe, so an offline flag would not remove this flake. Schemas are already bundled locally via@elgato/schemas. Running the command again on transient failure is therefore the correct and only clean fix; it also keeps validation fresh with no new moving parts.Fix
apps/streamdeck/scripts/validate.mjs: runsstreamdeck validateup to 3 times with exponential backoff (1s, 2s), making another attempt only when the failure output matches a transient-network signature; real validation errors fail fast on the first attempt (no masking, no wasted backoff), and an exhausted transient run still surfaces a non-zero exit so CI fails loudly.ENOTFOUNDis deliberately excluded (the CLI reports it as a normal "must be resolvable" error); a spawn failure surfaces its error message rather than an empty exit 1.apps/streamdeck/package.json:validatenow calls the helper; the inlinestreamdeck validatein the localpackagescript is swapped 1:1 for the same helper (no script restructuring). Both CI workflows (test.yml:113,sdk-canary.yml:57) runpnpm validate, so this covers both flake sites.Mirrors the existing
scripts/render-action-icons.mjs+ matching-vitest-test pattern.Test Plan
src/__tests__/validate.test.ts, 8 tests): simulates a transient failure then success (fails against a single-attempt impl, passes with the backoff loop), pins fail-fast on real errors, exponential backoff spacing, and the transient/ENOTFOUND signature boundary. Verified it fails without the fix.pnpm build)Validation successful(exit 0); a real error (missingbin/plugin.js) fails fast (exit 1)CMAP review
gemini = APPROVE (HIGH), codex = APPROVE (HIGH), claude = COMMENT (HIGH, non-blocking). Claude's substantive nits addressed: dropped the over-broad
'network'matcher (added specificENETUNREACH/ENETDOWN), surfaced spawn-failure diagnostics, clarified the EAI_AGAIN-vs-ENOTFOUND boundary, and corrected the test count above.