From 93abf6d44797844b19f873585c74bbed5f0228e3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 19 Oct 2026 12:02:09 -0700 Subject: [PATCH 1/3] Add maintainer label override for Lychee HTTP 429 failures Read current PR labels for authored and rendered checks. The explicit override accepts only HTTP 429 failures after retries, retaining reports and all other blocking checks. Normal policies remain unchanged. Signed-off-by: Ralf W. Grosse-Kunstleve --- .github/actions/lychee-links/action.yml | 4 ++- .github/workflows/build-docs.yml | 16 ++++++++++ .github/workflows/pre-commit.yml | 29 ++++++++++++++++-- ci/README.md | 30 +++++++++++++++++- ci/tools/retry_lychee.py | 7 +++-- ci/tools/run_lychee_precommit.py | 4 +-- ci/tools/tests/test_retry_lychee.py | 17 ++++++++--- ci/tools/tests/test_run_lychee_precommit.py | 34 ++++++++++++++++++--- 8 files changed, 123 insertions(+), 18 deletions(-) diff --git a/.github/actions/lychee-links/action.yml b/.github/actions/lychee-links/action.yml index d38796444e1..5c0a1cb754b 100644 --- a/.github/actions/lychee-links/action.yml +++ b/.github/actions/lychee-links/action.yml @@ -10,7 +10,9 @@ inputs: description: "Which hook to run: authored or rendered" required: true policy: - description: "check accepts up to 10 rate-limited URLs; cache-warm reports every unresolved link as a warning" + description: >- + check accepts up to 10 rate-limited URLs; cache-warm warns for any unresolved + link; allow-429 accepts only HTTP 429 failures under the maintainer override default: check refresh: description: "Check links afresh and publish the successful results as the new baseline" diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 322d772f1f0..264f308c4ee 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -295,12 +295,28 @@ jobs: with: python-version: "3.14" + # Full CI runs on a pull-request/N mirror. Use its PR number to query + # current labels, including when rerunning an older workflow attempt. + - name: Read the PR's Lychee rate-limit override + if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }} + id: lychee-policy + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + allow_429="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels | any(.name == "ci: lychee-non-blocking")')" + echo "allow-429=$allow_429" >> "$GITHUB_OUTPUT" + if [[ "$allow_429" == true ]]; then + echo "::notice::ci: lychee-non-blocking permits only HTTP 429 failures after retries." + fi + - name: Check rendered documentation links through pre-commit if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }} timeout-minutes: 60 uses: ./.github/actions/lychee-links with: kind: rendered + policy: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'allow-429' || 'check' }} - name: Upload docs GitHub Pages artifact if: ${{ inputs.deploy-docs }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index d493c212879..3c350c52aa0 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -22,6 +22,7 @@ concurrency: permissions: contents: read + pull-requests: read defaults: run: @@ -139,19 +140,43 @@ jobs: LYCHEE_SMOKE_DIR: ${{ steps.lychee-smoke.outputs.directory }} run: python ci/tools/smoke_test_lychee_precommit.py --cleanup "$LYCHEE_SMOKE_DIR" + # Query current labels so applying or removing the override takes effect + # on a rerun, which otherwise keeps the original event's label snapshot. + - name: Read the PR's Lychee rate-limit override + if: github.event_name == 'pull_request' + id: lychee-policy + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + allow_429="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels | any(.name == "ci: lychee-non-blocking")')" + echo "allow-429=$allow_429" >> "$GITHUB_OUTPUT" + if [[ "$allow_429" == true ]]; then + echo "::notice::ci: lychee-non-blocking permits only HTTP 429 failures after retries." + fi + # Warm only Lychee from the shared nightly baseline, retaining successful # checks between up to three attempts. The full suite below runs once: # formatter changes must remain failures. A missing baseline is supported; - # the first pass then checks afresh. + # the first pass then checks afresh. With the maintainer label, this is + # also the final 429-only link gate. - name: Warm the Lychee cache + id: lychee-check uses: ./.github/actions/lychee-links with: kind: authored - policy: cache-warm + policy: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'allow-429' || 'cache-warm' }} - name: Run pre-commit + # Run the remaining hooks even if the override's link gate fails. + # That earlier failure still makes the job fail. + if: ${{ !cancelled() && (success() || (steps.lychee-policy.outputs.allow-429 == 'true' && steps.lychee-check.outcome == 'failure')) }} env: GITHUB_TOKEN: ${{ github.token }} + # The override uses the reported link result above. All other hooks + # still run once; normal runs also invoke Lychee in the full suite. + SKIP: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'check-precommit-installed,lychee' || 'check-precommit-installed' }} run: pre-commit run --all-files --show-diff-on-failure - name: Save pre-commit cache diff --git a/ci/README.md b/ci/README.md index d364dc40c67..13f9aeb9370 100644 --- a/ci/README.md +++ b/ci/README.md @@ -48,7 +48,35 @@ fail explicitly. Per-attempt reports and a summary are kept for seven days. | Nightly (`lychee.yml`) | Afresh authored and rendered checks | Unresolved links are warnings; setup, execution, and report errors fail | The warm-up only fills the cache so the full suite, whose formatter changes must -stay failures, mostly reuses successful checks. +stay failures, mostly reuses successful checks. With the maintainer override, +the driver's report-based result becomes the final link gate, and the full +suite skips its duplicate Lychee invocation. Every other hook still runs once. + +### Maintainer Override For HTTP 429 + +When rate limiting persists, maintainers can apply `ci: lychee-non-blocking` +to the PR and rerun the affected Linux/Windows pre-commit jobs or rendered +documentation job. Applying or removing the label does not itself start a +new workflow run. Each job queries the PR's current labels, so a rerun sees +the current decision rather than the original event's label snapshot. + +The label selects the driver's `allow-429` policy for both authored and +rendered checks. Lychee still runs and retries normally; only a final report +whose remaining failures are all HTTP 429 responses from external HTTP(S) +URLs can pass with a warning. This explicit override has no URL-count limit. +The warning and retained reports identify the URLs that remain unverified. +Successful checks retain their usual cache behavior; failures are never cached. + +Any other failure still blocks, including a mixture of HTTP 429s and other +errors, broken local files or fragments, timeouts, and HTTP 403, 404, or 5xx +responses. Hook setup, invalid or missing reports, failed label lookups, and +the offline smoke checks remain blocking. Other exceptional failures require +the normal admin-merge decision. + +The label applies only to PR checks. Local pre-commit runs, default-branch +checks, and nightly cache production keep their existing policies. Without +the label, authored checks remain strict and rendered checks retain their +existing allowance for at most ten distinct HTTP 429 URLs. ### Shared URL Caches diff --git a/ci/tools/retry_lychee.py b/ci/tools/retry_lychee.py index 59e90257e69..524933dcbd7 100644 --- a/ci/tools/retry_lychee.py +++ b/ci/tools/retry_lychee.py @@ -157,7 +157,10 @@ def finish_link_failures(report: Path, data: Report, exit_codes: list[int], mess warning = True elif all(failure.rate_limited for failure in data.failures): message += f" {distinct_urls} distinct HTTP 429 URLs remain unverified" - if distinct_urls <= MAX_RATE_LIMITED_URLS: + if policy == "allow-429": + message += "; the maintainer override accepts only these rate limits with a warning." + warning = True + elif distinct_urls <= MAX_RATE_LIMITED_URLS: message += f" (limit: {MAX_RATE_LIMITED_URLS}); accepting these rate limits with a warning." warning = True else: @@ -184,7 +187,7 @@ def retry_lychee( require_link_check_exit(initial_exit_code, 1) if max_attempts < 1: raise ValueError("max_attempts must be positive") - if policy not in ("check", "cache-warm"): + if policy not in ("check", "cache-warm", "allow-429"): raise ValueError(f"Unknown lychee failure policy: {policy}") exit_codes = [initial_exit_code] diff --git a/ci/tools/run_lychee_precommit.py b/ci/tools/run_lychee_precommit.py index fbc4fa5f3ac..6b8f81077e6 100644 --- a/ci/tools/run_lychee_precommit.py +++ b/ci/tools/run_lychee_precommit.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Warm link caches or check rendered HTML through the configured pre-commit hooks.""" +"""Check documentation links or warm caches through the configured pre-commit hooks.""" from __future__ import annotations @@ -89,7 +89,7 @@ def run_checks(kind: str, report: Path, max_attempts: int = 3, *, policy: str = def main(arguments: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("kind", choices=("authored", "rendered")) - parser.add_argument("--policy", choices=("cache-warm", "check"), default="check") + parser.add_argument("--policy", choices=("cache-warm", "check", "allow-429"), default="check") parser.add_argument("--max-attempts", type=int, default=3) parser.add_argument("--report", type=Path, required=True) args = parser.parse_args(arguments) diff --git a/ci/tools/tests/test_retry_lychee.py b/ci/tools/tests/test_retry_lychee.py index 4cd2a07492b..8bada184e9f 100644 --- a/ci/tools/tests/test_retry_lychee.py +++ b/ci/tools/tests/test_retry_lychee.py @@ -350,10 +350,10 @@ def test_one_attempt_limit_keeps_transient_failure_without_retry(tmp_path, monke assert retry_lychee.retry_lychee(2, report, max_attempts=1, run_attempt=unexpected_retry) == 2 -@pytest.mark.parametrize("url_count,expected", [(10, 0), (11, 2)]) +@pytest.mark.parametrize("policy,url_count,expected", [("check", 10, 0), ("check", 11, 2), ("allow-429", 11, 0)]) @pytest.mark.agent_authored(model="gpt-6") def test_rate_limit_allowance_counts_distinct_urls_after_all_attempts( - tmp_path, monkeypatch, capsys, url_count, expected + tmp_path, monkeypatch, capsys, policy, url_count, expected ): report = tmp_path / "lychee.json" data = report_data(errors=[response(f"https://example.test/{number}") for number in range(url_count)], total=100) @@ -365,13 +365,16 @@ def run(output): write_report(output, data) return 2 - assert retry_lychee.retry_lychee(2, report, run_attempt=run) == expected + assert retry_lychee.retry_lychee(2, report, policy=policy, run_attempt=run) == expected assert delays == [60, 120] assert json.loads(report.read_text(encoding="utf-8")) == data text = report.with_suffix(".md").read_text(encoding="utf-8") assert f"{url_count} distinct HTTP 429 URLs remain unverified" in text assert "| 1 | 2 |\n| 2 | 2 |\n| 3 | 2 |" in text assert ("::warning::" in capsys.readouterr().out) == (expected == 0) + if policy == "allow-429": + assert "maintainer" in text.lower() + assert len(list(tmp_path.glob("lychee-attempt-*.json"))) == 3 for number in range(url_count): assert f"https://example.test/{number}" in text @@ -393,23 +396,27 @@ def test_rate_limit_allowance_deduplicates_exact_urls_across_sources(tmp_path, c "other_failure,timed_out", [ (response(code=404), False), + (response(code=403), False), (response(code=503), False), (response(code=None), True), (response(code=429), True), (response("mailto:someone@example.test"), False), (response("https:missing-host"), False), (response("https://[malformed"), False), + (response("file:///docs/guide.html", code=429), False), + (response("file:///docs/guide.html#missing", code=None, details="Anchor not found"), False), ], ) +@pytest.mark.parametrize("policy", ["check", "allow-429"]) @pytest.mark.agent_authored(model="gpt-6") -def test_429_allowance_never_accepts_other_failures(tmp_path, capsys, other_failure, timed_out): +def test_429_allowance_never_accepts_other_failures(tmp_path, capsys, other_failure, timed_out, policy): report = tmp_path / "lychee.json" data = report_data( errors=[response(), *([] if timed_out else [other_failure])], timeouts=[other_failure] if timed_out else [] ) write_report(report, data) - assert retry_lychee.retry_lychee(2, report, max_attempts=1, run_attempt=unexpected_retry) == 2 + assert retry_lychee.retry_lychee(2, report, max_attempts=1, policy=policy, run_attempt=unexpected_retry) == 2 assert "::warning::" not in capsys.readouterr().out diff --git a/ci/tools/tests/test_run_lychee_precommit.py b/ci/tools/tests/test_run_lychee_precommit.py index 1f9134cdacd..b8f3c2db262 100644 --- a/ci/tools/tests/test_run_lychee_precommit.py +++ b/ci/tools/tests/test_run_lychee_precommit.py @@ -136,18 +136,42 @@ def run_hook(kind, output): assert "example.test" not in report.with_suffix(".md").read_text() +@pytest.mark.parametrize("policy", ["cache-warm", "allow-429"]) @pytest.mark.agent_authored(model="gpt-6.1-sol") -def test_missing_native_status_is_fatal_even_for_nightly_cache_warming(tmp_path, monkeypatch, capsys): +def test_missing_native_status_is_fatal_even_under_relaxed_link_policies(tmp_path, monkeypatch, capsys, policy): monkeypatch.setattr( run_lychee_precommit.subprocess, "run", lambda *_args, **_kwargs: subprocess.CompletedProcess([], 1) ) - assert ( - run_lychee_precommit.main(["authored", "--policy", "cache-warm", "--report", str(tmp_path / "report.json")]) - == 1 - ) + assert run_lychee_precommit.main(["authored", "--policy", policy, "--report", str(tmp_path / "report.json")]) == 1 assert "no native status" in capsys.readouterr().out +@pytest.mark.parametrize("kind", ["authored", "rendered"]) +@pytest.mark.agent_authored(model="gpt-6") +def test_cli_maintainer_override_accepts_only_rate_limits_after_retrying(tmp_path, monkeypatch, kind): + report = tmp_path / "report.json" + calls = [] + + def run_hook(selected_kind, output): + calls.append(selected_kind) + data = native_report(code=2, total=20, status=429) + data["errors"] = 11 + data["error_map"]["guide.md"] = [ + {"url": f"https://example.test/{number}", "status": {"text": "Rate limited", "code": 429}} + for number in range(11) + ] + output.write_text(json.dumps(data)) + return 2 + + monkeypatch.setattr(run_lychee_precommit, "run_hook", run_hook) + monkeypatch.setattr(retry_lychee.time, "sleep", lambda _: None) + + assert run_lychee_precommit.main([kind, "--policy", "allow-429", "--report", str(report)]) == 0 + assert calls == [kind] * 3 + assert len(list(tmp_path.glob("report-attempt-*.json"))) == 3 + assert "maintainer" in report.with_suffix(".md").read_text().lower() + + @pytest.mark.agent_authored(model="gpt-6.1-sol") def test_zero_attempts_rejected_before_running_precommit(tmp_path, monkeypatch): monkeypatch.setattr(run_lychee_precommit, "run_hook", lambda *_args: pytest.fail("Unexpected invocation")) From 7f14d1084533093380c3b622413d49693a83dad0 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 19 Oct 2026 14:34:08 -0700 Subject: [PATCH 2/3] test: remove obsolete Lychee rerun environment cleanup LYCHEE_ARGS belonged to the removed rerun path and is no longer read. Keep this cleanup separate from the maintainer HTTP 429 override, as requested in review. Signed-off-by: Ralf W. Grosse-Kunstleve --- ci/tools/tests/test_run_lychee_precommit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ci/tools/tests/test_run_lychee_precommit.py b/ci/tools/tests/test_run_lychee_precommit.py index b8f3c2db262..2425be06738 100644 --- a/ci/tools/tests/test_run_lychee_precommit.py +++ b/ci/tools/tests/test_run_lychee_precommit.py @@ -128,7 +128,6 @@ def run_hook(kind, output): monkeypatch.setattr(run_lychee_precommit, "run_hook", run_hook) monkeypatch.setattr(retry_lychee.time, "sleep", delays.append) - monkeypatch.delenv("LYCHEE_ARGS", raising=False) assert run_lychee_precommit.run_checks("authored", report, policy="cache-warm") == 0 assert len(attempts) == 2 assert delays == [60] From b7b4f2fc515b90a23e683086a261a548d2e15589 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 19 Oct 2026 15:24:36 -0700 Subject: [PATCH 3/3] ci: retry Lychee label lookups and fall back to the default policy Read current PR labels once in the shared composite action. Retry failed or malformed lookups three times with 5/10-second delays, then warn and retain the normal link policy. An unconfirmed label never enables the override. Expose the early label decision to the authored pre-commit job so other hooks still run after an ordinary labeled link failure. Nightly and default-branch calls do not query PR labels. Signed-off-by: Ralf W. Grosse-Kunstleve --- .github/actions/lychee-links/action.yml | 41 ++++++++++++++++++++++++- .github/workflows/build-docs.yml | 18 ++--------- .github/workflows/pre-commit.yml | 23 +++----------- ci/README.md | 14 ++++++--- 4 files changed, 56 insertions(+), 40 deletions(-) diff --git a/.github/actions/lychee-links/action.yml b/.github/actions/lychee-links/action.yml index 5c0a1cb754b..670c7ad074d 100644 --- a/.github/actions/lychee-links/action.yml +++ b/.github/actions/lychee-links/action.yml @@ -17,12 +17,51 @@ inputs: refresh: description: "Check links afresh and publish the successful results as the new baseline" default: "false" + pr-number: + description: "PR whose current maintainer override label should be read; empty skips the lookup" + default: "" + +outputs: + allow-429: + description: "Whether the current PR label explicitly enables the HTTP 429 override" + value: ${{ steps.lychee-policy.outputs.allow-429 }} # Callers provide Python and the checked-out repository; the build of any # rendered documentation must already be complete. runs: using: composite steps: + - name: Read the PR's Lychee rate-limit override + id: lychee-policy + shell: bash --noprofile --norc -euo pipefail {0} + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ inputs.pr-number }} + run: | + allow_429=false + if [[ -n "$PR_NUMBER" ]]; then + # Reruns keep the original event's labels, so query the current decision. + # Only an exact boolean response can enable the maintainer override. + lookup_ok=false + for attempt in 1 2 3; do + if result="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels | any(.name == "ci: lychee-non-blocking")')"; then + case "$result" in + true|false) allow_429="$result"; lookup_ok=true; break ;; + esac + fi + if (( attempt < 3 )); then + sleep $((attempt * 5)) + fi + done + if [[ "$lookup_ok" != true ]]; then + echo "::warning::Could not read a valid PR label decision; using the default link policy. Rerun to apply ci: lychee-non-blocking." + elif [[ "$allow_429" == true ]]; then + echo "::notice::ci: lychee-non-blocking permits only HTTP 429 failures after retries." + fi + fi + echo "allow-429=$allow_429" >> "$GITHUB_OUTPUT" + - name: Describe the link cache id: cache shell: bash --noprofile --norc -euo pipefail {0} @@ -58,7 +97,7 @@ runs: shell: bash --noprofile --norc -euo pipefail {0} env: KIND: ${{ inputs.kind }} - POLICY: ${{ inputs.policy }} + POLICY: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'allow-429' || inputs.policy }} GITHUB_TOKEN: ${{ github.token }} run: | # Install into the interpreter that runs the driver, whatever shell diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 264f308c4ee..6e52bc71b15 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -295,28 +295,14 @@ jobs: with: python-version: "3.14" - # Full CI runs on a pull-request/N mirror. Use its PR number to query - # current labels, including when rerunning an older workflow attempt. - - name: Read the PR's Lychee rate-limit override - if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }} - id: lychee-policy - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - run: | - allow_429="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels | any(.name == "ci: lychee-non-blocking")')" - echo "allow-429=$allow_429" >> "$GITHUB_OUTPUT" - if [[ "$allow_429" == true ]]; then - echo "::notice::ci: lychee-non-blocking permits only HTTP 429 failures after retries." - fi - - name: Check rendered documentation links through pre-commit if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }} timeout-minutes: 60 uses: ./.github/actions/lychee-links with: kind: rendered - policy: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'allow-429' || 'check' }} + # Full CI's pull-request/N mirror supplies the PR number. + pr-number: ${{ env.PR_NUMBER }} - name: Upload docs GitHub Pages artifact if: ${{ inputs.deploy-docs }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 3c350c52aa0..a0cf2aa0b49 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -140,22 +140,6 @@ jobs: LYCHEE_SMOKE_DIR: ${{ steps.lychee-smoke.outputs.directory }} run: python ci/tools/smoke_test_lychee_precommit.py --cleanup "$LYCHEE_SMOKE_DIR" - # Query current labels so applying or removing the override takes effect - # on a rerun, which otherwise keeps the original event's label snapshot. - - name: Read the PR's Lychee rate-limit override - if: github.event_name == 'pull_request' - id: lychee-policy - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - allow_429="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels | any(.name == "ci: lychee-non-blocking")')" - echo "allow-429=$allow_429" >> "$GITHUB_OUTPUT" - if [[ "$allow_429" == true ]]; then - echo "::notice::ci: lychee-non-blocking permits only HTTP 429 failures after retries." - fi - # Warm only Lychee from the shared nightly baseline, retaining successful # checks between up to three attempts. The full suite below runs once: # formatter changes must remain failures. A missing baseline is supported; @@ -166,17 +150,18 @@ jobs: uses: ./.github/actions/lychee-links with: kind: authored - policy: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'allow-429' || 'cache-warm' }} + policy: cache-warm + pr-number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }} - name: Run pre-commit # Run the remaining hooks even if the override's link gate fails. # That earlier failure still makes the job fail. - if: ${{ !cancelled() && (success() || (steps.lychee-policy.outputs.allow-429 == 'true' && steps.lychee-check.outcome == 'failure')) }} + if: ${{ !cancelled() && (success() || (steps.lychee-check.outputs.allow-429 == 'true' && steps.lychee-check.outcome == 'failure')) }} env: GITHUB_TOKEN: ${{ github.token }} # The override uses the reported link result above. All other hooks # still run once; normal runs also invoke Lychee in the full suite. - SKIP: ${{ steps.lychee-policy.outputs.allow-429 == 'true' && 'check-precommit-installed,lychee' || 'check-precommit-installed' }} + SKIP: ${{ steps.lychee-check.outputs.allow-429 == 'true' && 'check-precommit-installed,lychee' || 'check-precommit-installed' }} run: pre-commit run --all-files --show-diff-on-failure - name: Save pre-commit cache diff --git a/ci/README.md b/ci/README.md index 13f9aeb9370..dcd5047b05a 100644 --- a/ci/README.md +++ b/ci/README.md @@ -57,8 +57,14 @@ suite skips its duplicate Lychee invocation. Every other hook still runs once. When rate limiting persists, maintainers can apply `ci: lychee-non-blocking` to the PR and rerun the affected Linux/Windows pre-commit jobs or rendered documentation job. Applying or removing the label does not itself start a -new workflow run. Each job queries the PR's current labels, so a rerun sees -the current decision rather than the original event's label snapshot. +new workflow run. The shared link-check action queries the PR's current labels, +so a rerun sees the current decision rather than the original event's snapshot. +The lookup makes up to three attempts, waiting 5 and 10 seconds between them. +If every attempt fails or returns an invalid response, the action warns and +keeps the caller's default policy without enabling the override: authored +checks remain strict in the full pre-commit suite, and rendered checks keep +their allowance for at most ten distinct HTTP 429 URLs. Rerun the affected job +to retry the lookup and apply the label; other pre-commit hooks still run. The label selects the driver's `allow-429` policy for both authored and rendered checks. Lychee still runs and retries normally; only a final report @@ -69,8 +75,8 @@ Successful checks retain their usual cache behavior; failures are never cached. Any other failure still blocks, including a mixture of HTTP 429s and other errors, broken local files or fragments, timeouts, and HTTP 403, 404, or 5xx -responses. Hook setup, invalid or missing reports, failed label lookups, and -the offline smoke checks remain blocking. Other exceptional failures require +responses. Hook setup, invalid or missing reports, and the offline smoke +checks remain blocking. Other exceptional failures require the normal admin-merge decision. The label applies only to PR checks. Local pre-commit runs, default-branch