From f8723bfe0a5d2276733b7cae9264dfd3d61500b0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:59:00 +0100 Subject: [PATCH 1/5] ci(docs): add link-check automation and tighten Docusaurus link strictness Closes the gap reported in #5672 (broken/404 links on tunit.dev/docs): - Add .github/workflows/link-check.yml with a `markdown-links` job that runs lychee against docs/docs/**/*.{md,mdx} on PRs touching docs. - Extend deploy-pages-test.yml with a lychee step that scans the built HTML after `yarn build`, reusing the existing build (no duplicate install/build per PR). - Add docs/lychee.toml with shared config: caching, retries, accepted status codes, and exclusions for bot-hostile sites (NuGet, SO, etc.). - Tighten docusaurus.config.ts: onBrokenAnchors and onBrokenMarkdownLinks now `throw` instead of `warn`, matching the existing onBrokenLinks policy. Build passes locally with no pre-existing breakage. --- .github/workflows/deploy-pages-test.yml | 19 +++++++++- .github/workflows/link-check.yml | 49 +++++++++++++++++++++++++ docs/docusaurus.config.ts | 3 +- docs/lychee.toml | 32 ++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/link-check.yml create mode 100644 docs/lychee.toml diff --git a/.github/workflows/deploy-pages-test.yml b/.github/workflows/deploy-pages-test.yml index 31977319c2..b0c3a654f7 100644 --- a/.github/workflows/deploy-pages-test.yml +++ b/.github/workflows/deploy-pages-test.yml @@ -29,4 +29,21 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - name: Test build website - run: yarn build \ No newline at end of file + run: yarn build + + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: docs/.lycheecache + key: lychee-html-${{ hashFiles('docs/docs/**/*.md', 'docs/docs/**/*.mdx', 'docs/lychee.toml') }} + restore-keys: lychee-html- + + - name: Check links in built HTML + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --config ./docs/lychee.toml + --no-progress + 'docs/build/**/*.html' + fail: true + workingDirectory: ${{ github.workspace }} \ No newline at end of file diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000000..480277b4f3 --- /dev/null +++ b/.github/workflows/link-check.yml @@ -0,0 +1,49 @@ +name: Documentation Link Check + +on: + pull_request: + branches: + - main + paths: + - 'docs/docs/**' + - 'docs/lychee.toml' + - '.github/workflows/link-check.yml' + push: + branches: + - main + paths: + - 'docs/docs/**' + - 'docs/lychee.toml' + - '.github/workflows/link-check.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + markdown-links: + name: Check links in Markdown sources + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: lychee-markdown-${{ hashFiles('docs/docs/**/*.md', 'docs/docs/**/*.mdx', 'docs/lychee.toml') }} + restore-keys: lychee-markdown- + + - name: Run lychee on Markdown sources + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --config ./docs/lychee.toml + --no-progress + 'docs/docs/**/*.md' + 'docs/docs/**/*.mdx' + fail: true diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index bac64c53ad..1921b4e22b 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -21,6 +21,7 @@ const config: Config = { deploymentBranch: 'gh-pages', onBrokenLinks: 'throw', + onBrokenAnchors: 'throw', // Even if you don't use internationalization, you can use this field to set // useful metadata like html lang. For example, if your site is Chinese, you @@ -85,7 +86,7 @@ const config: Config = { markdown: { mermaid: true, hooks: { - onBrokenMarkdownLinks: 'warn', + onBrokenMarkdownLinks: 'throw', }, }, themes: ['@docusaurus/theme-mermaid'], diff --git a/docs/lychee.toml b/docs/lychee.toml new file mode 100644 index 0000000000..f1e3678be1 --- /dev/null +++ b/docs/lychee.toml @@ -0,0 +1,32 @@ +# Lychee link-checker configuration. Used by .github/workflows/link-check.yml +# and the link-check step in .github/workflows/deploy-pages-test.yml. +# Docs: https://lychee.cli.rs/usage/config/ + +max_concurrency = 8 +max_retries = 3 +retry_wait_time = 5 +timeout = 20 +user_agent = "Mozilla/5.0 (compatible; lychee-tunit-docs)" + +# 429 = rate-limited; the link is fine, just throttled. 200/206 are accepted by default. +accept = [429] + +cache = true +max_cache_age = "1d" + +# Sites that consistently 4xx/5xx automated checks even though the link is correct, +# plus localhost/loopback. +exclude = [ + "^https://github\\.com/thomhurst/TUnit/edit/", # Docusaurus "edit this page" — generated, may 404 on new docs + "^https://www\\.nuget\\.org/packages/", # NuGet returns 429/403 to bot UAs + "^https://stackoverflow\\.com/", # Cloudflare bot challenge + "^https://github\\.com/sponsors/", # often 403 to bots + "^https://tluma\\.ai/", # third-party widget script, not a doc link + "^http://localhost", + "^https?://127\\.0\\.0\\.1", +] + +exclude_path = [ + "docs/node_modules", + "docs/.docusaurus", +] From aabd13645bf42b44230b3732d74e5def4d4e9f5a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:09:54 +0100 Subject: [PATCH 2/5] ci(docs): scope lychee to external links only and resolve root-relative paths First CI run surfaced three issues with the lychee config: 1. `accept = [429]` REPLACED defaults, so 200 OK was being rejected. Restore explicit `[200, 206, 429]`. 2. Lychee fails to parse root-relative paths (`/docs/...`, `/img/...`) without a base. Pass `--base https://tunit.dev` so they resolve as URLs. 3. Bare relative paths (`mocking/advanced` -> `mocking/advanced.md` via Docusaurus rewrite) confused lychee, which tried the literal filename. Restrict lychee to `scheme = ["https", "http"]` so all file:// URIs are skipped, and exclude `^https?://tunit\.dev/` so we do not re-check our own site (Docusaurus build already validates internal links via onBrokenLinks/Anchors/MarkdownLinks). Net effect: lychee now checks only external URLs, exactly the gap the Docusaurus build does not cover. --- .github/workflows/deploy-pages-test.yml | 1 + .github/workflows/link-check.yml | 1 + docs/lychee.toml | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-pages-test.yml b/.github/workflows/deploy-pages-test.yml index b0c3a654f7..46c1491c09 100644 --- a/.github/workflows/deploy-pages-test.yml +++ b/.github/workflows/deploy-pages-test.yml @@ -43,6 +43,7 @@ jobs: with: args: >- --config ./docs/lychee.toml + --base https://tunit.dev --no-progress 'docs/build/**/*.html' fail: true diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 480277b4f3..02610a0e62 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -43,6 +43,7 @@ jobs: with: args: >- --config ./docs/lychee.toml + --base https://tunit.dev --no-progress 'docs/docs/**/*.md' 'docs/docs/**/*.mdx' diff --git a/docs/lychee.toml b/docs/lychee.toml index f1e3678be1..9c769d799d 100644 --- a/docs/lychee.toml +++ b/docs/lychee.toml @@ -8,15 +8,25 @@ retry_wait_time = 5 timeout = 20 user_agent = "Mozilla/5.0 (compatible; lychee-tunit-docs)" -# 429 = rate-limited; the link is fine, just throttled. 200/206 are accepted by default. -accept = [429] +# Lychee's `accept` REPLACES defaults rather than extending — must list 200/206 explicitly. +# 429 = rate-limited; the link is fine, just throttled. +accept = [200, 206, 429] cache = true max_cache_age = "1d" -# Sites that consistently 4xx/5xx automated checks even though the link is correct, -# plus localhost/loopback. +# Only check HTTP(S) URLs. File / mailto / etc. are skipped — internal Docusaurus +# links are validated at build time by `onBrokenLinks: 'throw'` and friends in +# docusaurus.config.ts, so re-checking them here is duplicate work and produces +# false positives (e.g. bare paths `mocking/advanced` that Docusaurus rewrites +# to `.md` but lychee can't). +scheme = ["https", "http"] + exclude = [ + # Self-links — validated at build time; new pages in a PR are not yet live and would 404. + "^https?://tunit\\.dev/", + + # Bot-hostile sites that 4xx/5xx automated checks even when the link is fine. "^https://github\\.com/thomhurst/TUnit/edit/", # Docusaurus "edit this page" — generated, may 404 on new docs "^https://www\\.nuget\\.org/packages/", # NuGet returns 429/403 to bot UAs "^https://stackoverflow\\.com/", # Cloudflare bot challenge From 13c917ed1d38bd96a0483c0232b7470d3715caa6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:13:21 +0100 Subject: [PATCH 3/5] fix(docs): update broken Aspire link and use lychee --base-url - docs/docs/examples/aspire.md: update Aspire overview link from learn.microsoft.com (404) to aspire.dev/get-started/what-is-aspire/ where Microsoft now redirects. This is the first real bug surfaced by the new link checker. - link-check.yml + deploy-pages-test.yml: rename --base to --base-url (lychee 0.23 deprecated --base). --- .github/workflows/deploy-pages-test.yml | 2 +- .github/workflows/link-check.yml | 2 +- docs/docs/examples/aspire.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-pages-test.yml b/.github/workflows/deploy-pages-test.yml index 46c1491c09..2726c67ede 100644 --- a/.github/workflows/deploy-pages-test.yml +++ b/.github/workflows/deploy-pages-test.yml @@ -43,7 +43,7 @@ jobs: with: args: >- --config ./docs/lychee.toml - --base https://tunit.dev + --base-url https://tunit.dev --no-progress 'docs/build/**/*.html' fail: true diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 02610a0e62..01acd068fb 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -43,7 +43,7 @@ jobs: with: args: >- --config ./docs/lychee.toml - --base https://tunit.dev + --base-url https://tunit.dev --no-progress 'docs/docs/**/*.md' 'docs/docs/**/*.mdx' diff --git a/docs/docs/examples/aspire.md b/docs/docs/examples/aspire.md index 27c366afb1..17741d06e1 100644 --- a/docs/docs/examples/aspire.md +++ b/docs/docs/examples/aspire.md @@ -1,6 +1,6 @@ # Aspire Integration Testing -TUnit provides first-class support for [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/overview) integration testing through the `TUnit.Aspire` package. This package eliminates the boilerplate of managing an Aspire distributed application in tests, handling the full lifecycle (build, start, wait for resources, stop, dispose) automatically. +TUnit provides first-class support for [.NET Aspire](https://aspire.dev/get-started/what-is-aspire/) integration testing through the `TUnit.Aspire` package. This package eliminates the boilerplate of managing an Aspire distributed application in tests, handling the full lifecycle (build, start, wait for resources, stop, dispose) automatically. ## Installation From 93db7ad9cbf285a0f079c000a6c8191a419a3f87 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:17:49 +0100 Subject: [PATCH 4/5] ci(docs): fix lychee cache path and broaden link-check trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on PR #5779: - deploy-pages-test.yml: lychee runs from `${{ github.workspace }}`, so its on-disk cache lives at /.lycheecache, not docs/.lycheecache. The cache step was looking in the wrong place and never hit. Match the path to where lychee actually writes. - deploy-pages-test.yml: add the missing trailing newline. - link-check.yml: trigger on docusaurus.config.ts and sidebars.ts too — sidebar/config changes can introduce broken internal links without touching any individual .md file. --- .github/workflows/deploy-pages-test.yml | 4 ++-- .github/workflows/link-check.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-pages-test.yml b/.github/workflows/deploy-pages-test.yml index 2726c67ede..936624e22c 100644 --- a/.github/workflows/deploy-pages-test.yml +++ b/.github/workflows/deploy-pages-test.yml @@ -34,7 +34,7 @@ jobs: - name: Restore lychee cache uses: actions/cache@v4 with: - path: docs/.lycheecache + path: .lycheecache key: lychee-html-${{ hashFiles('docs/docs/**/*.md', 'docs/docs/**/*.mdx', 'docs/lychee.toml') }} restore-keys: lychee-html- @@ -47,4 +47,4 @@ jobs: --no-progress 'docs/build/**/*.html' fail: true - workingDirectory: ${{ github.workspace }} \ No newline at end of file + workingDirectory: ${{ github.workspace }} diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 01acd068fb..ea205349de 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -6,6 +6,8 @@ on: - main paths: - 'docs/docs/**' + - 'docs/docusaurus.config.ts' + - 'docs/sidebars.ts' - 'docs/lychee.toml' - '.github/workflows/link-check.yml' push: @@ -13,6 +15,8 @@ on: - main paths: - 'docs/docs/**' + - 'docs/docusaurus.config.ts' + - 'docs/sidebars.ts' - 'docs/lychee.toml' - '.github/workflows/link-check.yml' workflow_dispatch: From aa38e43da2a89eb0a64048958e31e587b3c4f9a6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:23:08 +0100 Subject: [PATCH 5/5] ci(docs): only run docs deploy test on docs-touching PRs Add a `paths:` filter so the Docusaurus build + lychee scan only fires when something under docs/ (or the workflow itself) actually changes. Pure C# PRs no longer pay the ~90s build cost for a deploy test that has nothing to validate. Mirrors the filter pattern already used by link-check.yml. --- .github/workflows/deploy-pages-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy-pages-test.yml b/.github/workflows/deploy-pages-test.yml index 936624e22c..7306054db6 100644 --- a/.github/workflows/deploy-pages-test.yml +++ b/.github/workflows/deploy-pages-test.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - main + paths: + - 'docs/**' + - '.github/workflows/deploy-pages-test.yml' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}