From cf977815e77177761554bf773410d76252cffb56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:40:00 +0000 Subject: [PATCH 01/18] Initial plan From 581abfe16ce3db43a9d9cae0c57f5d4eff419e44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:44:59 +0000 Subject: [PATCH 02/18] Add check-links.yml workflow with all PR #424 feedback applied Co-authored-by: TimHess <3947063+TimHess@users.noreply.github.com> --- .github/workflows/check-links.yml | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/check-links.yml diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 00000000..0a79f6ae --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,64 @@ +name: Check broken links + +on: + workflow_dispatch: + pull_request: + paths: + - '**.md' + - 'docs/**' + +permissions: + contents: read + pull-requests: write + +jobs: + check-links: + name: Check links in documentation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + - name: Check links with lychee + uses: lycheeverse/lychee-action@v2 + with: + args: | + --verbose + --no-progress + --cache + --max-cache-age 1d + --accept 100..=103,200..=299,403,429 + --exclude 'localhost' + --exclude 'fortuneservice' + --exclude '\.internal' + --exclude 'consul-register-example' + --exclude '^docker://' + --exclude-path '.git' + --exclude-path 'docs/api' + --max-concurrency 5 + --max-retries 6 + --retry-wait-time 15 + --timeout 30 + '**/*.md' + fail: true + jobSummary: false + + - name: Write link checking results to job summary + if: always() + run: | + sed -i 's/^# Summary$/# Link checking results/' ./lychee/out.md + cat ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" + + - name: Comment on PR with link check results + if: always() && github.event_name == 'pull_request' + uses: mshick/add-pr-comment@v2 + with: + message-id: broken-links-check + message-path: ./lychee/out.md From d5168bda9be27dfa33fb499527ba4b037215b2de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:26:21 +0000 Subject: [PATCH 03/18] Address all bart-vmware review feedback on check-links.yml and build-and-stage.yml Co-authored-by: TimHess <3947063+TimHess@users.noreply.github.com> --- .github/workflows/build-and-stage.yml | 3 ++- .github/workflows/check-links.yml | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-stage.yml b/.github/workflows/build-and-stage.yml index 8be42a05..1e6bf263 100644 --- a/.github/workflows/build-and-stage.yml +++ b/.github/workflows/build-and-stage.yml @@ -25,7 +25,8 @@ jobs: name: Build and publish app runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - name: Git checkout + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 0a79f6ae..8d8b58a6 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -3,9 +3,12 @@ name: Check broken links on: workflow_dispatch: pull_request: + branches: + - main paths: - '**.md' - 'docs/**' + - '.github/workflows/check-links.yml' permissions: contents: read @@ -16,8 +19,10 @@ jobs: name: Check links in documentation runs-on: ubuntu-latest steps: - - name: Checkout repository + - name: Git checkout uses: actions/checkout@v6 + with: + persist-credentials: false - name: Restore lychee cache uses: actions/cache@v4 @@ -51,13 +56,13 @@ jobs: jobSummary: false - name: Write link checking results to job summary - if: always() + if: ${{ !cancelled() }} run: | sed -i 's/^# Summary$/# Link checking results/' ./lychee/out.md cat ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" - name: Comment on PR with link check results - if: always() && github.event_name == 'pull_request' + if: ${{ !cancelled() && github.event_name == 'pull_request' }} uses: mshick/add-pr-comment@v2 with: message-id: broken-links-check From 68fb4ea7acbb87f2e130d706196e9d2fc3801eae Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Mon, 23 Feb 2026 13:02:37 -0600 Subject: [PATCH 04/18] add link-checking of internal links of built site content --- .github/workflows/build-and-stage.yml | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-stage.yml b/.github/workflows/build-and-stage.yml index 1e6bf263..60179b02 100644 --- a/.github/workflows/build-and-stage.yml +++ b/.github/workflows/build-and-stage.yml @@ -19,6 +19,7 @@ env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: true DOCFX_SOURCE_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + PUBLISH_DIR: ${{ github.workspace }}/mainsite jobs: build: @@ -46,13 +47,39 @@ jobs: run: dotnet build src/Steeltoe.io --configuration Release - name: dotnet publish - run: dotnet publish src/Steeltoe.io --configuration Release --no-build --output ${{ env.DOTNET_ROOT }}/mainsite + run: dotnet publish src/Steeltoe.io --configuration Release --no-build --output ${{ env.PUBLISH_DIR }} + + - name: Check internal links in built site + if: ${{ !cancelled() }} + uses: lycheeverse/lychee-action@v2 + with: + args: | + --verbose + --no-progress + --root-dir ${{ env.PUBLISH_DIR }} + --exclude '^https?://' + --exclude-path '.git' + --max-concurrency 5 + --max-retries 6 + --retry-wait-time 15 + --timeout 30 + '${{ env.PUBLISH_DIR }}/**/*.html' + fail: true + jobSummary: false + + - name: Add internal link check results to job summary + if: ${{ !cancelled() }} + run: | + if [ -f ./lychee/out.md ]; then + sed -i 's/^# Summary$/# Internal link check (built site)/' ./lychee/out.md + cat ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" + fi - name: Upload artifact for deployment job uses: actions/upload-artifact@v6 with: name: Steeltoe.io - path: ${{ env.DOTNET_ROOT }}/mainsite + path: ${{ env.PUBLISH_DIR }} deploy: name: Deploy From d1f19abf0cf4c69d6d743c04391bcfe493ebffa9 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Mon, 23 Feb 2026 17:52:22 -0600 Subject: [PATCH 05/18] process improvements, add local script for checking links from all docfx files - ignore razor routes from static file checking - alter root-dir to avoid errors with asset links --- .github/workflows/build-and-stage.yml | 10 +++--- .github/workflows/check-links.yml | 10 +++--- build/README.md | 10 ++++++ build/check-internal-links.ps1 | 52 +++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 build/check-internal-links.ps1 diff --git a/.github/workflows/build-and-stage.yml b/.github/workflows/build-and-stage.yml index 60179b02..9da70c3e 100644 --- a/.github/workflows/build-and-stage.yml +++ b/.github/workflows/build-and-stage.yml @@ -56,23 +56,23 @@ jobs: args: | --verbose --no-progress - --root-dir ${{ env.PUBLISH_DIR }} + --root-dir ${{ env.PUBLISH_DIR }}/wwwroot --exclude '^https?://' + --exclude '^file:///[^#?]*/[^.#?/]+$' --exclude-path '.git' --max-concurrency 5 --max-retries 6 --retry-wait-time 15 --timeout 30 - '${{ env.PUBLISH_DIR }}/**/*.html' + '${{ env.PUBLISH_DIR }}/wwwroot/**/*.html' fail: true jobSummary: false - - name: Add internal link check results to job summary + - name: Add internal link checking results to job summary if: ${{ !cancelled() }} run: | if [ -f ./lychee/out.md ]; then - sed -i 's/^# Summary$/# Internal link check (built site)/' ./lychee/out.md - cat ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" + sed 's/^# Summary$/# Internal link checking results/' ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" fi - name: Upload artifact for deployment job diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 8d8b58a6..e5533b00 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -46,7 +46,6 @@ jobs: --exclude 'consul-register-example' --exclude '^docker://' --exclude-path '.git' - --exclude-path 'docs/api' --max-concurrency 5 --max-retries 6 --retry-wait-time 15 @@ -55,15 +54,16 @@ jobs: fail: true jobSummary: false - - name: Write link checking results to job summary + - name: Add link checking results to job summary if: ${{ !cancelled() }} run: | - sed -i 's/^# Summary$/# Link checking results/' ./lychee/out.md - cat ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" + if [ -f ./lychee/out.md ]; then + sed 's/^# Summary$/# Link checking results/' ./lychee/out.md | tee -a "$GITHUB_STEP_SUMMARY" > ./lychee/results.md + fi - name: Comment on PR with link check results if: ${{ !cancelled() && github.event_name == 'pull_request' }} uses: mshick/add-pr-comment@v2 with: message-id: broken-links-check - message-path: ./lychee/out.md + message-path: ./lychee/results.md diff --git a/build/README.md b/build/README.md index bcf86d17..35ce4187 100644 --- a/build/README.md +++ b/build/README.md @@ -28,3 +28,13 @@ This process can take a while to complete, will display many warnings and may in 1. This command will produce many yaml files and place them in [docs](../docs), alongside the index pages for each API version 1. The script offers a parameter to target a single version (for example: `build-metadata.ps1 3`) 1. Run `docfx build` with [docfx-all.json](../docs/docfx-all.json), which will copy the html files into `wwwroot` of the Steeltoe.io project + +## Checking internal links locally + +After running the docfx build, check internal links in the generated HTML with the same tools CI uses: + +```pwsh +pwsh build/check-internal-links.ps1 +``` + +Requires [lychee](https://lychee.cli.rs/installation/). diff --git a/build/check-internal-links.ps1 b/build/check-internal-links.ps1 new file mode 100644 index 00000000..a7bccbf9 --- /dev/null +++ b/build/check-internal-links.ps1 @@ -0,0 +1,52 @@ +#!/usr/bin/env pwsh +# Check internal links in the built site output, matching what build-and-stage CI does. +# Run from repo root after docfx build. Requires lychee: https://lychee.cli.rs/installation/ + +param( + [string] $SitePath = 'src/Steeltoe.io/wwwroot' +) + +$ErrorActionPreference = 'Stop' +$repoRoot = (Get-Item $PSScriptRoot).Parent.FullName +Push-Location $repoRoot + +try { + if (-not (Get-Command lychee -ErrorAction SilentlyContinue)) { + Write-Error 'lychee not found. Install: cargo install lychee, scoop install lychee, or winget install lycheeverse.lychee' + } + + $resolvedPath = (Resolve-Path -LiteralPath $SitePath -ErrorAction Stop).Path + if (-not (Test-Path $resolvedPath)) { + Write-Error "Site path not found: $SitePath. Run the docfx build first." + } + + $htmlFiles = Get-ChildItem -Path $resolvedPath -Filter '*.html' -Recurse -File + if ($htmlFiles.Count -eq 0) { + Write-Error "No .html files found under $SitePath. Run the docfx build first." + } + + Write-Host "Checking internal links under $resolvedPath ($($htmlFiles.Count) HTML files) ..." -ForegroundColor Cyan + + $fileList = New-TemporaryFile + $htmlFiles | ForEach-Object { "./$($_.FullName.Substring($repoRoot.Length + 1) -replace '\\','/')" } | + Set-Content -LiteralPath $fileList -Encoding utf8NoBOM + + & lychee ` + --no-progress ` + --root-dir $resolvedPath ` + --exclude '^https?://' ` + --exclude '^file:///[^#?]*/[^.#?/]+$' ` + --exclude-path '.git' ` + --max-concurrency 5 ` + --max-retries 6 ` + --retry-wait-time 15 ` + --timeout 30 ` + --files-from $fileList + + $exitCode = $LASTEXITCODE + Remove-Item -LiteralPath $fileList -ErrorAction SilentlyContinue + exit $exitCode +} +finally { + Pop-Location +} From 5631660ef2c891e01f867f61bbdd5d5bbb6bf202 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Tue, 24 Feb 2026 14:53:37 -0600 Subject: [PATCH 06/18] focus on external link checking in static content - drop changes to build-and-stage - replace .ps1 with equivalent of check-links.yml --- .github/workflows/build-and-stage.yml | 34 ++---------------- .github/workflows/check-links.yml | 10 +++--- .gitignore | 2 ++ build/README.md | 6 ++-- build/check-external-links.ps1 | 48 +++++++++++++++++++++++++ build/check-internal-links.ps1 | 52 --------------------------- 6 files changed, 61 insertions(+), 91 deletions(-) create mode 100644 build/check-external-links.ps1 delete mode 100644 build/check-internal-links.ps1 diff --git a/.github/workflows/build-and-stage.yml b/.github/workflows/build-and-stage.yml index 9da70c3e..8be42a05 100644 --- a/.github/workflows/build-and-stage.yml +++ b/.github/workflows/build-and-stage.yml @@ -19,15 +19,13 @@ env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: true DOCFX_SOURCE_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - PUBLISH_DIR: ${{ github.workspace }}/mainsite jobs: build: name: Build and publish app runs-on: ubuntu-latest steps: - - name: Git checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: persist-credentials: false @@ -47,39 +45,13 @@ jobs: run: dotnet build src/Steeltoe.io --configuration Release - name: dotnet publish - run: dotnet publish src/Steeltoe.io --configuration Release --no-build --output ${{ env.PUBLISH_DIR }} - - - name: Check internal links in built site - if: ${{ !cancelled() }} - uses: lycheeverse/lychee-action@v2 - with: - args: | - --verbose - --no-progress - --root-dir ${{ env.PUBLISH_DIR }}/wwwroot - --exclude '^https?://' - --exclude '^file:///[^#?]*/[^.#?/]+$' - --exclude-path '.git' - --max-concurrency 5 - --max-retries 6 - --retry-wait-time 15 - --timeout 30 - '${{ env.PUBLISH_DIR }}/wwwroot/**/*.html' - fail: true - jobSummary: false - - - name: Add internal link checking results to job summary - if: ${{ !cancelled() }} - run: | - if [ -f ./lychee/out.md ]; then - sed 's/^# Summary$/# Internal link checking results/' ./lychee/out.md >> "$GITHUB_STEP_SUMMARY" - fi + run: dotnet publish src/Steeltoe.io --configuration Release --no-build --output ${{ env.DOTNET_ROOT }}/mainsite - name: Upload artifact for deployment job uses: actions/upload-artifact@v6 with: name: Steeltoe.io - path: ${{ env.PUBLISH_DIR }} + path: ${{ env.DOTNET_ROOT }}/mainsite deploy: name: Deploy diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index e5533b00..4aaf1fb2 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -1,4 +1,4 @@ -name: Check broken links +name: Check external links on: workflow_dispatch: @@ -16,7 +16,7 @@ permissions: jobs: check-links: - name: Check links in documentation + name: Check external links in documentation runs-on: ubuntu-latest steps: - name: Git checkout @@ -31,7 +31,7 @@ jobs: key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- - - name: Check links with lychee + - name: Check external links with lychee uses: lycheeverse/lychee-action@v2 with: args: | @@ -58,12 +58,12 @@ jobs: if: ${{ !cancelled() }} run: | if [ -f ./lychee/out.md ]; then - sed 's/^# Summary$/# Link checking results/' ./lychee/out.md | tee -a "$GITHUB_STEP_SUMMARY" > ./lychee/results.md + sed 's/^# Summary$/# External link checking results/' ./lychee/out.md | tee -a "$GITHUB_STEP_SUMMARY" > ./lychee/results.md fi - name: Comment on PR with link check results if: ${{ !cancelled() && github.event_name == 'pull_request' }} uses: mshick/add-pr-comment@v2 with: - message-id: broken-links-check + message-id: external-links-check message-path: ./lychee/results.md diff --git a/.gitignore b/.gitignore index 2bb691c8..8ecdc210 100644 --- a/.gitignore +++ b/.gitignore @@ -459,3 +459,5 @@ src/Steeltoe.io/wwwroot/*xrefmap.yml # Temporary workaround until a proper DocFX build supporting .NET 10 is available. docfx-net10-binaries/ + +.lycheecache diff --git a/build/README.md b/build/README.md index 35ce4187..f9a683b9 100644 --- a/build/README.md +++ b/build/README.md @@ -29,12 +29,12 @@ This process can take a while to complete, will display many warnings and may in 1. The script offers a parameter to target a single version (for example: `build-metadata.ps1 3`) 1. Run `docfx build` with [docfx-all.json](../docs/docfx-all.json), which will copy the html files into `wwwroot` of the Steeltoe.io project -## Checking internal links locally +## Checking external links locally -After running the docfx build, check internal links in the generated HTML with the same tools CI uses: +Check external links in markdown files with the same tools CI uses (no build required): ```pwsh -pwsh build/check-internal-links.ps1 +pwsh build/check-external-links.ps1 ``` Requires [lychee](https://lychee.cli.rs/installation/). diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 new file mode 100644 index 00000000..396659f4 --- /dev/null +++ b/build/check-external-links.ps1 @@ -0,0 +1,48 @@ +#!/usr/bin/env pwsh +# Check external links in markdown files, matching what the check-links CI job does. +# Run from anywhere in the repo. Requires lychee: https://lychee.cli.rs/installation/ + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$baseDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = (Get-Item $baseDir).Parent.FullName +Push-Location $repoRoot + +try { + if (-not (Get-Command lychee -ErrorAction SilentlyContinue)) { + throw 'lychee not found. Install: cargo install lychee, scoop install lychee, or winget install lycheeverse.lychee' + } + + $mdFiles = Get-ChildItem -Path . -Filter '*.md' -Recurse -File + if ($mdFiles.Count -eq 0) { + throw 'No .md files found in the repository.' + } + + Write-Output "Checking external links in $($mdFiles.Count) markdown files ..." + + & lychee ` + --verbose ` + --no-progress ` + --cache ` + --max-cache-age 1d ` + --accept '100..=103,200..=299,403,429' ` + --exclude 'localhost' ` + --exclude 'fortuneservice' ` + --exclude '\.internal' ` + --exclude 'consul-register-example' ` + --exclude '^docker://' ` + --exclude-path '.git' ` + --max-concurrency 5 ` + --max-retries 6 ` + --retry-wait-time 15 ` + --timeout 30 ` + '**/*.md' + + if ($LASTEXITCODE -ne 0) { + throw "lychee exited with code $LASTEXITCODE" + } +} +finally { + Pop-Location +} diff --git a/build/check-internal-links.ps1 b/build/check-internal-links.ps1 deleted file mode 100644 index a7bccbf9..00000000 --- a/build/check-internal-links.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env pwsh -# Check internal links in the built site output, matching what build-and-stage CI does. -# Run from repo root after docfx build. Requires lychee: https://lychee.cli.rs/installation/ - -param( - [string] $SitePath = 'src/Steeltoe.io/wwwroot' -) - -$ErrorActionPreference = 'Stop' -$repoRoot = (Get-Item $PSScriptRoot).Parent.FullName -Push-Location $repoRoot - -try { - if (-not (Get-Command lychee -ErrorAction SilentlyContinue)) { - Write-Error 'lychee not found. Install: cargo install lychee, scoop install lychee, or winget install lycheeverse.lychee' - } - - $resolvedPath = (Resolve-Path -LiteralPath $SitePath -ErrorAction Stop).Path - if (-not (Test-Path $resolvedPath)) { - Write-Error "Site path not found: $SitePath. Run the docfx build first." - } - - $htmlFiles = Get-ChildItem -Path $resolvedPath -Filter '*.html' -Recurse -File - if ($htmlFiles.Count -eq 0) { - Write-Error "No .html files found under $SitePath. Run the docfx build first." - } - - Write-Host "Checking internal links under $resolvedPath ($($htmlFiles.Count) HTML files) ..." -ForegroundColor Cyan - - $fileList = New-TemporaryFile - $htmlFiles | ForEach-Object { "./$($_.FullName.Substring($repoRoot.Length + 1) -replace '\\','/')" } | - Set-Content -LiteralPath $fileList -Encoding utf8NoBOM - - & lychee ` - --no-progress ` - --root-dir $resolvedPath ` - --exclude '^https?://' ` - --exclude '^file:///[^#?]*/[^.#?/]+$' ` - --exclude-path '.git' ` - --max-concurrency 5 ` - --max-retries 6 ` - --retry-wait-time 15 ` - --timeout 30 ` - --files-from $fileList - - $exitCode = $LASTEXITCODE - Remove-Item -LiteralPath $fileList -ErrorAction SilentlyContinue - exit $exitCode -} -finally { - Pop-Location -} From 6af1af4ab7349d782b9ff04b9f7e4c88e1bcba5c Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 25 Feb 2026 09:24:09 -0600 Subject: [PATCH 07/18] drop branch filter from PR trigger --- .github/workflows/check-links.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 4aaf1fb2..6c7807f9 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -3,8 +3,6 @@ name: Check external links on: workflow_dispatch: pull_request: - branches: - - main paths: - '**.md' - 'docs/**' From cc825feb694694cfe228fcf0e11bdf9726a091e5 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 25 Feb 2026 10:44:10 -0600 Subject: [PATCH 08/18] fix lychee install link --- build/README.md | 2 +- build/check-external-links.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/README.md b/build/README.md index f9a683b9..e1b2f73c 100644 --- a/build/README.md +++ b/build/README.md @@ -37,4 +37,4 @@ Check external links in markdown files with the same tools CI uses (no build req pwsh build/check-external-links.ps1 ``` -Requires [lychee](https://lychee.cli.rs/installation/). +Requires [lychee](https://lychee.cli.rs/guides/getting-started/). diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index 396659f4..97de19e4 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -1,6 +1,6 @@ #!/usr/bin/env pwsh # Check external links in markdown files, matching what the check-links CI job does. -# Run from anywhere in the repo. Requires lychee: https://lychee.cli.rs/installation/ +# Run from anywhere in the repo. Requires lychee: https://lychee.cli.rs/guides/getting-started/ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' From 82e6e9f8ad6ce6389d2ab730a88e57919ce3784e Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 25 Feb 2026 14:30:18 -0600 Subject: [PATCH 09/18] focus check-links job on external links, exclude vendored sources Co-authored-by: Cursor Made-with: Cursor --- .github/workflows/check-links.yml | 5 +++-- build/check-external-links.ps1 | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 6c7807f9..99565d7b 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -35,6 +35,8 @@ jobs: args: | --verbose --no-progress + --scheme https + --scheme http --cache --max-cache-age 1d --accept 100..=103,200..=299,403,429 @@ -42,8 +44,7 @@ jobs: --exclude 'fortuneservice' --exclude '\.internal' --exclude 'consul-register-example' - --exclude '^docker://' - --exclude-path '.git' + --exclude-path 'build.sources' --max-concurrency 5 --max-retries 6 --retry-wait-time 15 diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index 97de19e4..a388c23a 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -24,6 +24,8 @@ try { & lychee ` --verbose ` --no-progress ` + --scheme https ` + --scheme http ` --cache ` --max-cache-age 1d ` --accept '100..=103,200..=299,403,429' ` @@ -31,8 +33,7 @@ try { --exclude 'fortuneservice' ` --exclude '\.internal' ` --exclude 'consul-register-example' ` - --exclude '^docker://' ` - --exclude-path '.git' ` + --exclude-path 'build.sources' ` --max-concurrency 5 ` --max-retries 6 ` --retry-wait-time 15 ` From ec140f6cee06afa6fe9e48266cf42e9c6cf42beb Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 25 Feb 2026 14:53:57 -0600 Subject: [PATCH 10/18] add --root-dir to resolve root-relative image paths without error Made-with: Cursor --- .github/workflows/check-links.yml | 1 + build/check-external-links.ps1 | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 99565d7b..9b1061d5 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -37,6 +37,7 @@ jobs: --no-progress --scheme https --scheme http + --root-dir '.' --cache --max-cache-age 1d --accept 100..=103,200..=299,403,429 diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index a388c23a..98b629bd 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -26,6 +26,7 @@ try { --no-progress ` --scheme https ` --scheme http ` + --root-dir '.' ` --cache ` --max-cache-age 1d ` --accept '100..=103,200..=299,403,429' ` From 02374e256d4b35569eb402135b40c663fa5e5065 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Thu, 26 Feb 2026 08:18:33 -0600 Subject: [PATCH 11/18] Update .github/workflows/check-links.yml Co-authored-by: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> --- .github/workflows/check-links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 9b1061d5..97734a75 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -8,6 +8,10 @@ on: - 'docs/**' - '.github/workflows/check-links.yml' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read pull-requests: write From 9016ef2949fb05a16cd250a40f2cdd23a25a58cf Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Thu, 26 Feb 2026 10:29:36 -0600 Subject: [PATCH 12/18] PR suggests and alphabetize lychee params - only use --no-progress in CI - use more defaults - change settings per-host instead of globally - check for installed lychee first --- .github/workflows/build-and-stage.yml | 3 ++- .github/workflows/check-links.yml | 25 +++++++++++------------- .gitignore | 1 + build/check-external-links.ps1 | 28 ++++++++++++--------------- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-and-stage.yml b/.github/workflows/build-and-stage.yml index 8be42a05..1e6bf263 100644 --- a/.github/workflows/build-and-stage.yml +++ b/.github/workflows/build-and-stage.yml @@ -25,7 +25,8 @@ jobs: name: Build and publish app runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - name: Git checkout + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 97734a75..c08bbde1 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -37,36 +37,33 @@ jobs: uses: lycheeverse/lychee-action@v2 with: args: | - --verbose --no-progress - --scheme https - --scheme http - --root-dir '.' --cache - --max-cache-age 1d - --accept 100..=103,200..=299,403,429 - --exclude 'localhost' --exclude 'fortuneservice' --exclude '\.internal' --exclude 'consul-register-example' + --exclude-loopback --exclude-path 'build.sources' - --max-concurrency 5 - --max-retries 6 - --retry-wait-time 15 - --timeout 30 + --host-concurrency 5 + --host-request-interval 100ms + --include-fragments + --require-https + --retry-wait-time 3 + --scheme http + --scheme https '**/*.md' - fail: true + failIfEmpty: true jobSummary: false - name: Add link checking results to job summary - if: ${{ !cancelled() }} + if: always() run: | if [ -f ./lychee/out.md ]; then sed 's/^# Summary$/# External link checking results/' ./lychee/out.md | tee -a "$GITHUB_STEP_SUMMARY" > ./lychee/results.md fi - name: Comment on PR with link check results - if: ${{ !cancelled() && github.event_name == 'pull_request' }} + if: always() && github.event_name == 'pull_request' uses: mshick/add-pr-comment@v2 with: message-id: external-links-check diff --git a/.gitignore b/.gitignore index 8ecdc210..6fd66bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -460,4 +460,5 @@ src/Steeltoe.io/wwwroot/*xrefmap.yml # Temporary workaround until a proper DocFX build supporting .NET 10 is available. docfx-net10-binaries/ +# lychee link-checker cache .lycheecache diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index 98b629bd..6b6490fc 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -5,15 +5,15 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +if (-not (Get-Command lychee -ErrorAction SilentlyContinue)) { + throw 'lychee not found. Install: cargo install lychee, scoop install lychee, or winget install lycheeverse.lychee' +} + $baseDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = (Get-Item $baseDir).Parent.FullName Push-Location $repoRoot try { - if (-not (Get-Command lychee -ErrorAction SilentlyContinue)) { - throw 'lychee not found. Install: cargo install lychee, scoop install lychee, or winget install lycheeverse.lychee' - } - $mdFiles = Get-ChildItem -Path . -Filter '*.md' -Recurse -File if ($mdFiles.Count -eq 0) { throw 'No .md files found in the repository.' @@ -22,23 +22,19 @@ try { Write-Output "Checking external links in $($mdFiles.Count) markdown files ..." & lychee ` - --verbose ` - --no-progress ` - --scheme https ` - --scheme http ` - --root-dir '.' ` --cache ` - --max-cache-age 1d ` - --accept '100..=103,200..=299,403,429' ` - --exclude 'localhost' ` --exclude 'fortuneservice' ` --exclude '\.internal' ` --exclude 'consul-register-example' ` + --exclude-loopback ` --exclude-path 'build.sources' ` - --max-concurrency 5 ` - --max-retries 6 ` - --retry-wait-time 15 ` - --timeout 30 ` + --host-concurrency 5 ` + --host-request-interval 100ms ` + --include-fragments ` + --require-https ` + --retry-wait-time 3 ` + --scheme http ` + --scheme https ` '**/*.md' if ($LASTEXITCODE -ne 0) { From dd15c022d64bea573fe1649279a1132d54dc01fc Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Thu, 26 Feb 2026 10:33:59 -0600 Subject: [PATCH 13/18] re-add root-dir, primarily for running in CI --- .github/workflows/check-links.yml | 1 + build/check-external-links.ps1 | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index c08bbde1..7e31ed07 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -49,6 +49,7 @@ jobs: --include-fragments --require-https --retry-wait-time 3 + --root-dir '.' --scheme http --scheme https '**/*.md' diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index 6b6490fc..2ceb17c2 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -33,6 +33,7 @@ try { --include-fragments ` --require-https ` --retry-wait-time 3 ` + --root-dir '.' ` --scheme http ` --scheme https ` '**/*.md' From ec3c124e3b93b4df4a75240194839a88a59c7037 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Thu, 26 Feb 2026 14:04:10 -0600 Subject: [PATCH 14/18] re-add --verbose, local script enhancements --verbose is needed to see redirect details - locally: - set user-agent locally to avoid 403 on mysql links (doesn't seem to affect CI) - ignore build/docfx-net10-binaries (like playwright readme) - longer host-request-interval and retry-wait-time --- .github/workflows/check-links.yml | 5 +++-- build/check-external-links.ps1 | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 7e31ed07..43dc4aef 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -45,13 +45,14 @@ jobs: --exclude-loopback --exclude-path 'build.sources' --host-concurrency 5 - --host-request-interval 100ms + --host-request-interval 250ms --include-fragments --require-https - --retry-wait-time 3 + --retry-wait-time 5 --root-dir '.' --scheme http --scheme https + --verbose '**/*.md' failIfEmpty: true jobSummary: false diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index 2ceb17c2..d073ae96 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -28,14 +28,17 @@ try { --exclude 'consul-register-example' ` --exclude-loopback ` --exclude-path 'build.sources' ` + --exclude-path 'build.docfx-net10-binaries' ` --host-concurrency 5 ` - --host-request-interval 100ms ` + --host-request-interval 300ms ` --include-fragments ` --require-https ` - --retry-wait-time 3 ` + --retry-wait-time 5 ` --root-dir '.' ` - --scheme http ` - --scheme https ` + --scheme 'http' ` + --scheme 'https' ` + --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' ` + --verbose ` '**/*.md' if ($LASTEXITCODE -ne 0) { From b0140cd792fcf8b7d68d0000d53cfbd33722db03 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Fri, 27 Feb 2026 09:13:09 -0600 Subject: [PATCH 15/18] move settings to shared lychee.toml - since redirect fixes are already in place in other PR, change verbosity now --- .github/workflows/check-links.yml | 21 +++----------------- build/README.md | 1 + build/check-external-links.ps1 | 20 +------------------ lychee.toml | 32 +++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 lychee.toml diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 43dc4aef..faf285ed 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -7,6 +7,7 @@ on: - '**.md' - 'docs/**' - '.github/workflows/check-links.yml' + - 'lychee.toml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -36,24 +37,8 @@ jobs: - name: Check external links with lychee uses: lycheeverse/lychee-action@v2 with: - args: | - --no-progress - --cache - --exclude 'fortuneservice' - --exclude '\.internal' - --exclude 'consul-register-example' - --exclude-loopback - --exclude-path 'build.sources' - --host-concurrency 5 - --host-request-interval 250ms - --include-fragments - --require-https - --retry-wait-time 5 - --root-dir '.' - --scheme http - --scheme https - --verbose - '**/*.md' + args: --no-progress '**/*.md' + fail: false failIfEmpty: true jobSummary: false diff --git a/build/README.md b/build/README.md index e1b2f73c..63f6f4be 100644 --- a/build/README.md +++ b/build/README.md @@ -38,3 +38,4 @@ pwsh build/check-external-links.ps1 ``` Requires [lychee](https://lychee.cli.rs/guides/getting-started/). +Settings are shared between CI and the local script via [`lychee.toml`](../lychee.toml) at the repo root. diff --git a/build/check-external-links.ps1 b/build/check-external-links.ps1 index d073ae96..737b5b32 100644 --- a/build/check-external-links.ps1 +++ b/build/check-external-links.ps1 @@ -21,25 +21,7 @@ try { Write-Output "Checking external links in $($mdFiles.Count) markdown files ..." - & lychee ` - --cache ` - --exclude 'fortuneservice' ` - --exclude '\.internal' ` - --exclude 'consul-register-example' ` - --exclude-loopback ` - --exclude-path 'build.sources' ` - --exclude-path 'build.docfx-net10-binaries' ` - --host-concurrency 5 ` - --host-request-interval 300ms ` - --include-fragments ` - --require-https ` - --retry-wait-time 5 ` - --root-dir '.' ` - --scheme 'http' ` - --scheme 'https' ` - --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' ` - --verbose ` - '**/*.md' + & lychee '**/*.md' if ($LASTEXITCODE -ne 0) { throw "lychee exited with code $LASTEXITCODE" diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 00000000..5a29609f --- /dev/null +++ b/lychee.toml @@ -0,0 +1,32 @@ +# Shared lychee config for CI and local script (build/check-external-links.ps1). + +verbose = "info" +cache = true +scheme = ["http", "https"] +require_https = true +include_fragments = true +retry_wait_time = 5 +root_dir = "." +host_concurrency = 5 +host_request_interval = "250ms" +exclude_loopback = true + +# Browser-like UA so sites that block bots (e.g. mysql.com) respond normally. +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" + +exclude = [ + # Placeholder hostnames in code samples. + "fortuneservice", + "\\.internal", + "consul-register-example", + # False positives: these sites generate anchor IDs via JS, so fragments + # aren't in the raw HTML. Track https://github.com/lycheeverse/lychee/issues/1729 + "https://learn\\.microsoft\\.com/.*#remarks", + "https://github\\.com/.*#", +] + +exclude_path = [ + # Vendored sources from API browser build. + "build/sources", + "build/docfx-net10-binaries", +] From b26c2cb16c3d5a48f6e36ff3cdc81bdaa1271a4b Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Fri, 27 Feb 2026 10:13:06 -0600 Subject: [PATCH 16/18] fix exclude_paths broken in move to lychee.toml --- lychee.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lychee.toml b/lychee.toml index 5a29609f..577c97b7 100644 --- a/lychee.toml +++ b/lychee.toml @@ -27,6 +27,6 @@ exclude = [ exclude_path = [ # Vendored sources from API browser build. - "build/sources", - "build/docfx-net10-binaries", + "build.sources", + "build.docfx-net10-binaries", ] From 83b2dab47b1e7ec3ae44cf7df97e2ca6b7a864ad Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Fri, 27 Feb 2026 11:57:27 -0600 Subject: [PATCH 17/18] isolate lychee caching to specific workstreams - also include a paste-able snippet to bust the cache for a given PR --- .github/workflows/check-links.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index faf285ed..c9694740 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -27,12 +27,22 @@ jobs: with: persist-credentials: false + # To bust the lychee cache for a PR, replace `` with the PR number and run in pwsh: + # $ids = gh api repos/SteeltoeOSS/Documentation/actions/caches ` + # --paginate ` + # --jq '.actions_caches[] + # | select(.key | startswith("cache-lychee--")) + # | .id' + # $ids | ForEach-Object { + # gh api -X DELETE "repos/SteeltoeOSS/Documentation/actions/caches/$_" + # Write-Host "Deleted cache $_" + # } - name: Restore lychee cache uses: actions/cache@v4 with: path: .lycheecache - key: cache-lychee-${{ github.sha }} - restore-keys: cache-lychee- + key: cache-lychee-${{ github.event.pull_request.number || 'manual' }}-${{ github.sha }} + restore-keys: cache-lychee-${{ github.event.pull_request.number || 'manual' }}- - name: Check external links with lychee uses: lycheeverse/lychee-action@v2 From ef49b7eef9fe1fcb93ffa31a8fd2722fd739ff75 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Mon, 2 Mar 2026 07:44:15 -0600 Subject: [PATCH 18/18] Apply suggestions from code review Co-authored-by: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> --- .github/workflows/check-links.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index c9694740..c4fb2746 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -27,7 +27,7 @@ jobs: with: persist-credentials: false - # To bust the lychee cache for a PR, replace `` with the PR number and run in pwsh: + # To clear the lychee cache for a PR, replace `` with the PR number and run in pwsh: # $ids = gh api repos/SteeltoeOSS/Documentation/actions/caches ` # --paginate ` # --jq '.actions_caches[] @@ -53,14 +53,13 @@ jobs: jobSummary: false - name: Add link checking results to job summary - if: always() run: | if [ -f ./lychee/out.md ]; then sed 's/^# Summary$/# External link checking results/' ./lychee/out.md | tee -a "$GITHUB_STEP_SUMMARY" > ./lychee/results.md fi - name: Comment on PR with link check results - if: always() && github.event_name == 'pull_request' + if: ${{ github.event_name == 'pull_request' }} uses: mshick/add-pr-comment@v2 with: message-id: external-links-check