Repository navigation
docs(audits): archive the four unreferenced 2026-06 audits (minimum arm) #17115
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check Links | |
| # Repo-internal link gate (#6028). | |
| # | |
| # `pull_request` was commented out in a59dc59e (2026-01-28) as a rider on an | |
| # unrelated commit, with no rationale recorded; the gate then sat dormant for | |
| # six months while `lychee.toml` and `fail: true` kept it looking alive. | |
| # Maintainer ruling 2026-08-07: restore the trigger, check REPO-INTERNAL links | |
| # only, and land it advisory-first (NOT in the required set) until it has shown | |
| # a stable green streak. | |
| # | |
| # ⛔ No `merge_group` trigger on purpose: this is an advisory lane, and an | |
| # advisory gate does not get to consume merge-queue capacity. If it is ever | |
| # promoted into the required set, `merge_group` MUST be added in the same | |
| # change or the queue stalls on a required check that never reports (#6121). | |
| # | |
| # ⚠️ This job checks a link's FILE, never its `#anchor` — `lychee.toml` sets | |
| # `include_fragments = "none"`, and a link to a heading that does not exist is | |
| # reported `[200] OK` here (measured, #7484). Do NOT read a green run as | |
| # "the anchors are fine". The fragment half is owned by `pnpm check:doc-anchors` | |
| # in `lint.yml`, which is REQUIRED where this lane is advisory; `lychee.toml`'s | |
| # `include_fragments` comment records why the enum is not the fix. | |
| # | |
| # `paths:` is what the two checks read: lychee's input globs in `LYCHEE_ARGS` | |
| # below (taken as whole `content/` and `docs/` trees, so an image or page a | |
| # link points at inside them counts too), its config, and the ADR step's | |
| # `docs/adr/**` and script. Measured on the last 49 landings of `main` | |
| # (6390fe52): 10 touched one of these, so the other 39 would not have run it. | |
| # ⚠️ The filter is blind to the TARGET side outside those trees: a PR that | |
| # only renames or deletes a file a doc links to (a `packages/*` directory | |
| # from CONTRIBUTING.md, `LICENSE` from LICENSING.md) skips this lane, and the | |
| # break shows on the next PR that touches a doc, or on a `workflow_dispatch` | |
| # run. Keep this list equal to `LYCHEE_ARGS`. ⛔ Promoting the lane into the | |
| # required set removes this filter in the same change: a required check | |
| # skipped by `paths:` never reports, the #6121 stall above. | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - 'content/**' | |
| - 'docs/**' | |
| - 'README.md' | |
| - 'ARCHITECTURE.md' | |
| - 'CHANGELOG.md' | |
| - 'RELEASE_NOTES.md' | |
| - 'CONTRIBUTING.md' | |
| - 'ROADMAP.md' | |
| - 'LICENSING.md' | |
| - 'CODE_OF_CONDUCT.md' | |
| - 'lychee.toml' | |
| - '.lycheeignore' | |
| - 'scripts/check-adr-links.mjs' | |
| - 'scripts/invoked-as.mjs' | |
| - '.github/workflows/check-links.yml' | |
| # A newer run for the same pull request supersedes this one, so the older run is | |
| # cancelled. The group key is ci.yml's shape; `cancel-in-progress` is true only on | |
| # `pull_request`, so no other event's run is ever cancelled. | |
| concurrency: | |
| group: check-links-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.after || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| link-checker: | |
| name: Check Documentation Links | |
| runs-on: ubuntu-latest | |
| # Run wall over the 100 most recent completed runs to 2026-10-27: p99 0.8 min, max | |
| # 1.0. The bound covers the lychee retry path (two binary downloads and the 15 s wait) | |
| # with headroom. | |
| timeout-minutes: 10 | |
| # Least privilege: the job only reads the tree and runs lychee offline. | |
| # There is no issue-filing step, and `--offline` makes zero network | |
| # requests, so neither `issues: write` nor a GITHUB_TOKEN is needed. | |
| permissions: | |
| contents: read | |
| # The lychee argv lives here, not inline on the step, because the step is | |
| # invoked TWICE (attempt + retry, see #8238 below) and two copies of a | |
| # 9-line argv is a drift hazard: an edit to one copy silently changes what | |
| # the retry checks relative to the first attempt. One definition, two | |
| # readers. `github.workspace` is available in a job-level `env`. | |
| env: | |
| # `--offline` is the internal-only mechanism, and it lives HERE rather | |
| # than in lychee.toml on purpose: the equivalent `offline = true` | |
| # config key is silently ignored by older lychee (measured: ignored on | |
| # 0.19.1, honoured on the 0.24.2 pinned below). A determinism guarantee | |
| # must not depend on which lychee the action happens to install, so it is | |
| # asserted at the invocation site. | |
| # | |
| # Offline means only `file://` targets are resolved -- every http(s) | |
| # link is reported EXCLUDED, never requested. That is what makes this | |
| # gate deterministic and free of external-network flake. | |
| # | |
| # --root-dir is what makes ROOT-RELATIVE links checkable. Most internal | |
| # links in content/** are site routes (`/docs/permissions`), and lychee | |
| # hard-errors on those unless it is told which directory `/` means. | |
| # The Fumadocs content root is `content/`, so `/docs/x` resolves to | |
| # content/docs/x -- and --fallback-extensions supplies the .mdx/.md | |
| # suffix that a site route omits. Without this pair the gate cannot go | |
| # green at all: 1286 root-relative links fail as "Cannot resolve | |
| # root-relative link ... provide a root dir". | |
| # | |
| # ⛔ `docs/adr/` stays OUT of lychee's reach: `'docs/**/*.md'` below | |
| # would sweep it in, so `--exclude-path '^docs/adr/'` takes it back out | |
| # (a regex over the input path as globbed, measured to drop exactly that | |
| # directory on the pinned 0.24.2 while `lychee.toml`'s own `exclude_path` | |
| # entries keep applying -- the CLI flag adds to them, it does not replace | |
| # them). The decision registry has ONE link owner, the `Check ADR | |
| # cross-links` step below, which can freeze a pre-existing break on a | |
| # shrink-only baseline and fail on a NEW one -- something neither | |
| # `exclude` nor `.lycheeignore` can express, because neither ever tells | |
| # you an entry stopped being needed. Checking the same files twice, under | |
| # two rule sets, would put two verdicts on one link. | |
| # `ARCHITECTURE.md` joined this glob in #6867; `docs/**` and the root | |
| # history/governance pages (CHANGELOG, RELEASE_NOTES, CONTRIBUTING, | |
| # ROADMAP, LICENSING, CODE_OF_CONDUCT) joined in #26087, each landing | |
| # clean once its dead links were fixed in the same PR, so none needs a | |
| # KNOWN_DEAD_TARGETS-style baseline. | |
| LYCHEE_ARGS: >- | |
| --offline | |
| --root-dir ${{ github.workspace }}/content | |
| --fallback-extensions mdx,md | |
| --config lychee.toml | |
| --exclude-path '^docs/adr/' | |
| 'content/**/*.md' | |
| 'content/**/*.mdx' | |
| 'README.md' | |
| 'ARCHITECTURE.md' | |
| 'docs/**/*.md' | |
| 'CHANGELOG.md' | |
| 'RELEASE_NOTES.md' | |
| 'CONTRIBUTING.md' | |
| 'ROADMAP.md' | |
| 'LICENSING.md' | |
| 'CODE_OF_CONDUCT.md' | |
| # ⛔ Pin the lychee binary explicitly rather than inheriting the action's | |
| # default (#8238). Three comments in this file already reason about "the | |
| # pinned lychee 0.24.2" -- the `--offline` argument above turns on which | |
| # version runs -- but nothing here pinned it: the version came from | |
| # `lycheeverse/lychee-action@v2`'s own `lycheeVersion` default, which | |
| # moves whenever the `v2` tag moves. The determinism claim was true only | |
| # by coincidence. Asserting it at the invocation site is the same rule | |
| # `--offline` is held to, applied to the thing `--offline` depends on. | |
| LYCHEE_VERSION: v0.24.2 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| # ADR cross-link gate (#6592). `docs/adr/` is deliberately NOT added to the | |
| # lychee globs below -- see the ⛔ note there for why -- so the decision | |
| # registry gets its own resolver, in the same advisory lane. | |
| # | |
| # Deliberately `node` and not `pnpm`: the script has zero dependencies, so | |
| # this lane needs no setup-node / corepack / `pnpm install`, and the whole | |
| # advisory job stays a checkout plus two checks. It is also why an author | |
| # can run it locally in any container (`pnpm check:adr-links` when a | |
| # workspace install exists, `node scripts/check-adr-links.mjs` when it does | |
| # not) -- which lychee, installed by the action at CI time, cannot offer. | |
| - name: Check ADR cross-links | |
| run: node scripts/check-adr-links.mjs --self-test && node scripts/check-adr-links.mjs | |
| # ── #8238: the action's binary download is a single-shot curl ─────────── | |
| # | |
| # `lychee-action@v2`'s `lychee-setup` step fetches the release tarball | |
| # with a bare `curl -sfLO` -- no `--retry`, no cache, and a `rm -rf` of | |
| # its download dir on every run, so `actions/cache` cannot reach it (the | |
| # action re-downloads unconditionally whatever is already on disk; a | |
| # cache step here would read as coverage while doing nothing). | |
| # | |
| # When that one curl loses, `curl` exits 22 ("HTTP page not retrieved"), | |
| # the action's `Install lychee` and `Run Lychee` steps both report | |
| # `skipped`, and this job goes red having examined ZERO links. Measured | |
| # on three unrelated PRs in one afternoon (2026-08-12), all three job | |
| # logs read directly and byte-identical in shape: | |
| # | |
| # #8128 17:23:49Z run 31622391000 job 94200196323 exit 22, 497ms | |
| # #8205 20:08:47Z run 31636151516 job 94246838620 exit 22, 177ms | |
| # #8225 21:21:28Z run 31642129140 job 94266966755 exit 22 | |
| # | |
| # Transient, not systemic: every one cleared on the next attempt, and | |
| # PR #8227 went green at 21:27:53Z, six minutes after #8225's failure. | |
| # | |
| # ⛔ This is NOT gate weakening and must not become it. `fail: true` | |
| # stays on both attempts and neither is allowed to fail open: a retry | |
| # that exhausts still fails the job -- it just now says WHY. | |
| - name: Check links with lychee | |
| id: lychee | |
| # Attempt 1 only. `continue-on-error` here hands the verdict to the | |
| # retry below; it does NOT let a failure through, because the retry | |
| # step carries no such escape and its failure fails the job. | |
| continue-on-error: true | |
| uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 | |
| with: | |
| lycheeVersion: ${{ env.LYCHEE_VERSION }} | |
| args: ${{ env.LYCHEE_ARGS }} | |
| # Fail the job if broken links are found | |
| fail: true | |
| # A back-to-back retry is not obviously enough. Every observed recovery | |
| # was tens of seconds to tens of minutes later, so retrying within the | |
| # same second would be retrying inside the same blip. 15s is a judgement, | |
| # not a measurement -- the retry is the part the evidence supports. Costs | |
| # nothing on a green run: this step is `skipped` when attempt 1 passes. | |
| - name: Wait before retrying the lychee setup | |
| if: steps.lychee.outcome == 'failure' | |
| run: sleep 15 | |
| - name: Check links with lychee (retry) | |
| id: lychee-retry | |
| if: steps.lychee.outcome == 'failure' | |
| uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 | |
| with: | |
| lycheeVersion: ${{ env.LYCHEE_VERSION }} | |
| args: ${{ env.LYCHEE_ARGS }} | |
| fail: true | |
| # ── #8238, the legibility half: distinguish "links are broken" from ──── | |
| # "the link check never ran" | |
| # | |
| # Both outcomes were the same red before this step, and telling them | |
| # apart cost every reader a job-log read. `exit_code` is the | |
| # discriminator, and it is exact rather than heuristic: the action's | |
| # `entrypoint.sh` writes `exit_code=$LYCHEE_EXIT_CODE` to `$GITHUB_OUTPUT` | |
| # BEFORE it exits, so a genuine broken-link failure carries a value (2) | |
| # while a setup failure skips `Run Lychee` entirely and leaves the output | |
| # unset. Empty ⇒ lychee never executed ⇒ nothing about the links was | |
| # examined, whatever the check's name suggests. | |
| # | |
| # `$GITHUB_STEP_SUMMARY` is safe to append to here: the only writer of | |
| # that file in this job is `entrypoint.sh`, which never ran in the one | |
| # case this step fires. | |
| - name: Report a setup failure as "the link check did not run" | |
| if: always() && steps.lychee-retry.outcome == 'failure' && steps.lychee-retry.outputs.exit_code == '' | |
| run: | | |
| { | |
| echo "## ⚠️ The link check did not run" | |
| echo | |
| echo "\`lychee\` was never executed, so **no link in this repository was" | |
| echo "examined** — this red says nothing about the documentation links," | |
| echo "and nothing about the files this PR touches." | |
| echo | |
| echo "Both attempts failed while \`lycheeverse/lychee-action@v2\` was" | |
| echo "downloading the \`${LYCHEE_VERSION}\` binary from the GitHub releases" | |
| echo "CDN (\`curl\` exit 22). This is a known transient failure (#8238);" | |
| echo "re-running the job is the expected remedy." | |
| echo | |
| echo "The gate remains fail-closed on purpose: a link check that could" | |
| echo "not run must not report success." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| echo "::error title=Link check did not run::lychee setup failed twice (binary download, curl exit 22). No links were checked — see #8238. Re-run the job." | |
| # Fail-closed, asserted here rather than inherited. The retry step has | |
| # already failed the job, so this `exit 1` is redundant today -- and | |
| # deliberately so: if anyone ever adds `continue-on-error` to the | |
| # retry, the "did not run" case must still be red, not a vacuous | |
| # green (#4690). | |
| exit 1 |