From f28d39c8682484751ba1a186eb159180290744e8 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Thu, 25 Jun 2026 10:17:44 +0200 Subject: [PATCH] fix(ci): harden links job against linkinator exit-13 crash (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit linkinator@6's CLI runs `await main()` at the top level; on newer Node the event loop can empty while that await is pending, aborting the crawl with exit 13 ("unsettled top-level await") after fetching only /. That's a tooling crash, not a broken link, producing false-red Internal links runs that block PRs. Pin the exact version (6.3.0) so an upstream 6.x patch can't silently shift the behaviour, and wrap the invocation in a retry that fires *only* on exit 13 — real broken-link findings exit 1 and still fail the job immediately. Applied to both the internal (blocking) and external (daily) jobs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/links.yml | 44 +++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index b7070dab..5967a7a9 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -45,8 +45,28 @@ jobs: sleep 1 done [ -n "$up" ] || { echo "::error::preview server did not start"; exit 1; } - npx --yes linkinator@6 http://localhost:4321 \ - --recurse --skip "^https?://(?!localhost)" + # linkinator@6's CLI runs `await main()` at the top level; on newer Node + # the event loop can empty while that await is still pending, aborting the + # crawl with exit 13 ("unsettled top-level await") after fetching only /. + # That's a tooling crash, not a broken link — real breakage exits 1. Pin + # the exact version so an upstream 6.x patch can't shift the behaviour, and + # retry *only* on the exit-13 crash, never on a genuine finding. See #58. + attempt=1 + max=3 + while :; do + set +e + npx --yes linkinator@6.3.0 http://localhost:4321 \ + --recurse --skip "^https?://(?!localhost)" + code=$? + set -e + [ "$code" -eq 0 ] && break + if [ "$code" -eq 13 ] && [ "$attempt" -lt "$max" ]; then + echo "::warning::linkinator crashed with exit 13 (unsettled top-level await); retrying ($attempt/$max)" + attempt=$((attempt + 1)) + continue + fi + exit "$code" + done # Blocking: runs daily and on demand. A failed run signals rotted external # links that need fixing. Kept off PRs so a flaky upstream (IETF / W3C / MDN @@ -100,5 +120,21 @@ jobs: sleep 1 done [ -n "$up" ] || { echo "::error::preview server did not start"; exit 1; } - npx --yes linkinator@6 http://localhost:4321 \ - --config linkinator.config.json + # Same exit-13 foot-gun as the internal job (see #58): pin the exact + # version and retry only on the top-level-await crash, not on real rot. + attempt=1 + max=3 + while :; do + set +e + npx --yes linkinator@6.3.0 http://localhost:4321 \ + --config linkinator.config.json + code=$? + set -e + [ "$code" -eq 0 ] && break + if [ "$code" -eq 13 ] && [ "$attempt" -lt "$max" ]; then + echo "::warning::linkinator crashed with exit 13 (unsettled top-level await); retrying ($attempt/$max)" + attempt=$((attempt + 1)) + continue + fi + exit "$code" + done