From aee254cecfa497d62e9d3807febe803e0d8c43c2 Mon Sep 17 00:00:00 2001 From: Thomas Hodges Date: Fri, 2 Oct 2026 15:29:52 -0500 Subject: [PATCH 1/2] Follow the CCIP v2 redirect in linkcheck vercel.json 301-redirects /ccip/v2 and /ccip/v2/:path* to /ccip and /ccip/:path*, so those links are valid on the live site but have no file in the static build. Add lychee remap rules for the redirect, with a fixture and checker that keep a missing page or anchor failing and leave /ccip/v1 and /ccip/v20 untouched. --- .github/workflows/test.yml | 13 ++ lychee.toml | 15 ++ src/scripts/link-check/check-ccip-v2-remap.sh | 183 ++++++++++++++++++ .../fixtures/ccip-v2-remap/should-fail.html | 13 ++ .../fixtures/ccip-v2-remap/should-pass.html | 25 +++ .../ccip/evm/getting-started/index.html | 1 + .../ccip-v2-remap/static/ccip/index.html | 1 + .../ccip/v1/evm/getting-started/index.html | 1 + .../static/ccip/v20/page/index.html | 1 + 9 files changed, 253 insertions(+) create mode 100755 src/scripts/link-check/check-ccip-v2-remap.sh create mode 100644 src/scripts/link-check/fixtures/ccip-v2-remap/should-fail.html create mode 100644 src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html create mode 100644 src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/evm/getting-started/index.html create mode 100644 src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/index.html create mode 100644 src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v1/evm/getting-started/index.html create mode 100644 src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v20/page/index.html diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaf4852f93f..4331df1c177 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,6 +50,9 @@ jobs: - 'src/scripts/markdown-fidelity-exceptions.ts' - 'src/scripts/markdown-fidelity-baseline.json' - 'astro.config.*' + - 'lychee.toml' + - 'vercel.json' + - 'src/scripts/link-check/**' any_non_solidity: - '**' - '!public/samples/**/*.sol' @@ -203,6 +206,16 @@ jobs: npm ci --prefer-offline --no-audit fi + - name: Check CCIP v2 Remap Fixture + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + lycheeVersion: v0.24.2 + args: >- + --root-dir $GITHUB_WORKSPACE/src/scripts/link-check/fixtures/ccip-v2-remap/static + --config lychee.toml + src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html + fail: true + - name: Build Site run: npm run build env: diff --git a/lychee.toml b/lychee.toml index e3c1d3016b5..b6eb70a34b6 100644 --- a/lychee.toml +++ b/lychee.toml @@ -65,4 +65,19 @@ exclude = [ exclude_path = [ 'ccip/api-reference/evm/v162/.*', 'ccip/api-reference/evm/v163/.*', +] + +# Rewrite the two CCIP v2 redirects declared in vercel.json so the checker sees +# the same pages a browser would after the 301: +# /ccip/v2/:path* -> /ccip/:path* +# /ccip/v2 -> /ccip +# Both rules are anchored on purpose. A bare "/ccip/v2 /ccip" prefix remap would +# also rewrite unrelated paths such as /ccip/v20 into /ccip0. Lychee 0.24.2 uses +# the Rust regex crate: no look-around, and the replacement must be a URL. A bare +# "?" is a regex quantifier, so a literal question mark must be written as \?. +remap = [ + "/ccip/v2/ /ccip/", + "/ccip/v2$ /ccip", + "/ccip/v2# /ccip#", + "/ccip/v2\\? /ccip?", ] \ No newline at end of file diff --git a/src/scripts/link-check/check-ccip-v2-remap.sh b/src/scripts/link-check/check-ccip-v2-remap.sh new file mode 100755 index 00000000000..41848acd5ff --- /dev/null +++ b/src/scripts/link-check/check-ccip-v2-remap.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# +# Guards the CCIP v2 -> /ccip remap in lychee.toml. +# +# vercel.json 301-redirects /ccip/v2/:path* to /ccip/:path* and /ccip/v2 to /ccip +# on the live site, so links that are valid in production must not fail +# linkcheck-internal. The static build has no /ccip/v2 directory, so lychee needs +# the matching `remap` rules to follow the redirect. +# +# This script proves three things: +# 1. Real pages reached through the /ccip/v2 alias pass, including anchors, and +# the URLs lychee actually resolves keep unrelated paths such as /ccip/v1 +# and /ccip/v20 intact. +# 2. Real misses still fail, and the output shows the remap was applied. +# 3. vercel.json still declares exactly the redirects the remap mirrors. +# +# Usage: check-ccip-v2-remap.sh [path-to-lychee] +# Defaults to `lychee` on PATH when no argument is given. + +set -uo pipefail + +LYCHEE="${1:-lychee}" +EVIDENCE="/tmp/linkcheck-ccip-v2-remap-evidence.txt" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +FIXTURE_DIR="${REPO_ROOT}/src/scripts/link-check/fixtures/ccip-v2-remap" +STATIC_DIR="${FIXTURE_DIR}/static" + +failures=0 +fail() { + echo "FAIL: $*" >&2 + failures=$((failures + 1)) +} + +if ! command -v "${LYCHEE}" >/dev/null 2>&1 && [ ! -x "${LYCHEE}" ]; then + echo "FAIL: lychee binary not found: ${LYCHEE}" >&2 + exit 1 +fi + +cd "${REPO_ROOT}" || exit 1 + +: > "${EVIDENCE}" +{ + echo "=== lychee: ${LYCHEE} ($("${LYCHEE}" --version 2>&1)) ===" + echo +} >> "${EVIDENCE}" + +run_lychee() { + # Extra flags (such as --dump) may be passed before the fixture path. + "${LYCHEE}" \ + --root-dir "${STATIC_DIR}" \ + --config lychee.toml \ + "$@" +} + +# --- 1. The pass fixture must be clean ------------------------------------- +pass_output="$(run_lychee src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html 2>&1)" +pass_status=$? +{ + echo "=== should-pass.html (exit ${pass_status}) ===" + echo "${pass_output}" + echo +} >> "${EVIDENCE}" + +if [ "${pass_status}" -ne 0 ]; then + fail "should-pass.html: expected exit 0, got ${pass_status}" +fi + +# A clean exit is not enough on its own: an over-broad remap can rewrite a real +# URL into a different URL that also happens to resolve. Inspect the resolved +# URLs directly. In particular, a bare "?" in a Rust regex is a quantifier, so +# "/ccip/v2? /ccip?" silently rewrites /ccip/v1 and /ccip/v20. +dump_output="$(run_lychee --dump src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html 2>&1)" +dump_status=$? +{ + echo "=== should-pass.html --dump (exit ${dump_status}) ===" + echo "${dump_output}" + echo +} >> "${EVIDENCE}" + +if [ "${dump_status}" -ne 0 ]; then + fail "should-pass.html --dump: expected exit 0, got ${dump_status}" +fi + +# Paths that must survive the remap unchanged. +for survivor in \ + "ccip/v1/evm/getting-started" \ + "ccip/v20/page" \ + "ccip/evm/getting-started?x=1" \ + "ccip?x=1"; do + if ! grep -qF -- "${survivor}" <<<"${dump_output}"; then + fail "should-pass.html --dump: resolved URLs are missing '${survivor}'" + fi +done + +# Rewrites that prove a literal '?' was treated as a regex quantifier. +for corrupted in "ccip?1" "ccip?0"; do + if grep -qF -- "${corrupted}" <<<"${dump_output}"; then + fail "should-pass.html --dump: resolved URLs contain '${corrupted}'; a remap rule is treating '?' as a regex quantifier" + fi +done + +# --- 2. The fail fixture must still fail, and show the remap --------------- +fail_output="$(run_lychee src/scripts/link-check/fixtures/ccip-v2-remap/should-fail.html 2>&1)" +fail_status=$? +{ + echo "=== should-fail.html (exit ${fail_status}) ===" + echo "${fail_output}" + echo +} >> "${EVIDENCE}" + +if [ "${fail_status}" -eq 0 ]; then + fail "should-fail.html: expected a non-zero exit, got 0" +fi + +for marker in "Cannot find fragment" "File not found" "Remaps:"; do + if ! grep -qF -- "${marker}" <<<"${fail_output}"; then + fail "should-fail.html: output is missing '${marker}'" + fi +done + +# The v2 URL must be shown rewritten onto its /ccip equivalent before the check, +# otherwise the pass fixture is passing for some other reason. +if ! grep -qE 'ccip/v2/not-a-real-page --> [^ ]*ccip/not-a-real-page' <<<"${fail_output}"; then + fail "should-fail.html: output does not show /ccip/v2/... remapped onto /ccip/..." +fi + +# --- 3. vercel.json must still declare the redirects we mirror ------------- +redirect_check="$(python3 - "${REPO_ROOT}/vercel.json" <<'PY' +import json +import sys + +expected = [ + ("/ccip/v2/:path*", "/ccip/:path*"), + ("/ccip/v2", "/ccip"), +] + +with open(sys.argv[1]) as handle: + config = json.load(handle) + +redirects = config.get("redirects", []) +problems = [] +for source, destination in expected: + match = next((r for r in redirects if r.get("source") == source), None) + if match is None: + problems.append(f"vercel.json has no redirect with source {source!r}") + elif match.get("destination") != destination: + problems.append( + f"vercel.json redirect {source!r} now points at " + f"{match.get('destination')!r}, expected {destination!r}" + ) + +print("\n".join(problems)) +PY +)" +redirect_status=$? +{ + echo "=== vercel.json redirect check (exit ${redirect_status}) ===" + if [ -n "${redirect_check}" ]; then + echo "${redirect_check}" + else + echo "ok: /ccip/v2/:path* -> /ccip/:path* and /ccip/v2 -> /ccip" + fi + echo +} >> "${EVIDENCE}" + +if [ "${redirect_status}" -ne 0 ]; then + fail "vercel.json could not be parsed" +fi +if [ -n "${redirect_check}" ]; then + fail "vercel.json no longer mirrors the lychee remap: ${redirect_check//$'\n'/; }" +fi + +# --- Verdict --------------------------------------------------------------- +echo "Evidence written to ${EVIDENCE}" +if [ "${failures}" -ne 0 ]; then + echo "ccip-v2 remap check FAILED (${failures} problem(s))" >&2 + exit 1 +fi + +echo "ccip-v2 remap check passed" +exit 0 diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/should-fail.html b/src/scripts/link-check/fixtures/ccip-v2-remap/should-fail.html new file mode 100644 index 00000000000..d571531cadd --- /dev/null +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/should-fail.html @@ -0,0 +1,13 @@ + + + + + CCIP v2 remap fixture: expected to fail + + + + + diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html b/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html new file mode 100644 index 00000000000..c791c54ff9d --- /dev/null +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html @@ -0,0 +1,25 @@ + + + + + CCIP v2 remap fixture: expected to pass + + + + + diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/evm/getting-started/index.html b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/evm/getting-started/index.html new file mode 100644 index 00000000000..1f2cebadf13 --- /dev/null +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/evm/getting-started/index.html @@ -0,0 +1 @@ +

Overview

diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/index.html b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/index.html new file mode 100644 index 00000000000..9c5a25983d0 --- /dev/null +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/index.html @@ -0,0 +1 @@ +

Home

diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v1/evm/getting-started/index.html b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v1/evm/getting-started/index.html new file mode 100644 index 00000000000..9ef14c4621a --- /dev/null +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v1/evm/getting-started/index.html @@ -0,0 +1 @@ +

Old

diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v20/page/index.html b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v20/page/index.html new file mode 100644 index 00000000000..d1f4ecd3b73 --- /dev/null +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/static/ccip/v20/page/index.html @@ -0,0 +1 @@ +

Twenty

From d27ab56127dd1d3c6ba3aa8822f0cad0a14d9f7f Mon Sep 17 00:00:00 2001 From: Thomas Hodges Date: Fri, 2 Oct 2026 15:44:35 -0500 Subject: [PATCH 2/2] Format --- src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html b/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html index c791c54ff9d..14da86c8191 100644 --- a/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html +++ b/src/scripts/link-check/fixtures/ccip-v2-remap/should-pass.html @@ -16,7 +16,8 @@
  • v1 page is untouched
  • v20 page is not rewritten
  • - external link, skipped in offline mode