From 95928868500921e6f882c35af14c5075059edb0f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 14:47:16 +0100 Subject: [PATCH 01/35] Add lychee configuration for external link checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configures lychee link checker with: - Rate limiting and retry settings - Custom user agent to avoid bot blocking - Cache settings to reduce load on external sites - Ignore patterns for placeholder URLs, localhost, and sites that block automated checkers (Twitter, LinkedIn, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .lychee.toml | 31 +++++++++++++++++++++++++++++++ .lycheeignore | 27 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .lychee.toml create mode 100644 .lycheeignore diff --git a/.lychee.toml b/.lychee.toml new file mode 100644 index 00000000000000..85c34fb49e0bdb --- /dev/null +++ b/.lychee.toml @@ -0,0 +1,31 @@ +# Lychee configuration for external link checking +# Documentation: https://github.com/lycheeverse/lychee + +# Maximum number of concurrent requests +max_concurrency = 32 + +# Maximum number of retries per request +max_retries = 3 + +# Request timeout in seconds +timeout = 30 + +# Retry wait time in seconds +retry_wait_time = 2 + +# User agent (some sites block default user agents) +user_agent = "Mozilla/5.0 (compatible; Sentry-Docs-Link-Checker; +https://github.com/getsentry/sentry-docs)" + +# Accept common status codes that indicate the link works +accept = [200, 201, 202, 203, 204, 206, 301, 302, 308] + +# Only check external links (our internal check handles internal ones) +include_mail = false +include_verbatim = false + +# Follow redirects +max_redirects = 10 + +# Cache settings (reduce load on external sites) +cache = true +max_cache_age = "1d" diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 00000000000000..b09ceeb0d1f5fe --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,27 @@ +# URLs to ignore during external link checking +# Supports regex patterns - lines starting with # are comments + +# Example/placeholder URLs +https?://example\.com.* +https?://your-.* +https?://.*\.example\..* +https?://___.*___.* + +# Localhost and local dev URLs +https?://.*localhost.* +https?://127\.0\.0\.1.* +https?://0\.0\.0\.0.* + +# Sites known to block automated checkers +https?://twitter\.com.* +https?://x\.com.* +https?://linkedin\.com.* +https?://www\.linkedin\.com.* + +# Interactive demos that may not respond to HEAD requests +https?://demo\.arcade\.software.* + +# Placeholder domains commonly used in docs +https?://api\.example\.com.* +https?://your-api-host.* +https?://empowerplant\.io.* From f1b1df366981b20f50bada23442667313d6e1127 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 14:48:01 +0100 Subject: [PATCH 02/35] Add GitHub workflow for external link checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses lychee to validate external links in documentation. Triggers: - Weekly cron (Sunday 2 AM UTC): Creates/updates GitHub issue - Manual dispatch: Optionally fails on broken links - Pull requests: Adds non-blocking comment with report The workflow caches results to reduce load on external sites and does not block PRs (external link failures are often transient or false positives). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/lint-external-links.yml | 114 ++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .github/workflows/lint-external-links.yml diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml new file mode 100644 index 00000000000000..c91fbbf5544f5a --- /dev/null +++ b/.github/workflows/lint-external-links.yml @@ -0,0 +1,114 @@ +name: Check External Links + +on: + # Run weekly on Sundays at 2 AM UTC + schedule: + - cron: '0 2 * * 0' + + # Allow manual triggering + workflow_dispatch: + inputs: + fail_on_errors: + description: 'Fail workflow if broken links found' + required: false + default: false + type: boolean + + # Run on PRs that modify docs (non-blocking) + pull_request: + branches: [master] + paths: + - 'docs/**' + - 'develop-docs/**' + - '.lychee.toml' + - '.lycheeignore' + +jobs: + check-external-links: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + # Restore lychee cache to reduce load on external sites + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + - name: Check external links + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --cache + --max-cache-age 1d + --config .lychee.toml + --exclude-path node_modules + --exclude-path .next + --exclude-path public + "./docs/**/*.md" + "./docs/**/*.mdx" + "./develop-docs/**/*.md" + "./develop-docs/**/*.mdx" + # Don't fail on scheduled runs or PRs - only on manual dispatch if requested + fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} + output: ./lychee-report.md + format: markdown + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # For scheduled runs: Create/update an issue with broken links + - name: Create/Update Issue for Broken Links + if: github.event_name == 'schedule' && steps.lychee.outputs.exit_code != 0 + uses: peter-evans/create-issue-from-file@v5 + with: + title: 'Weekly External Link Check: Broken Links Found' + content-filepath: ./lychee-report.md + labels: | + automated + documentation + broken-links + update-existing: true + + # For PRs: Add a comment with the report (non-blocking) + - name: Comment on PR with Link Report + if: github.event_name == 'pull_request' && steps.lychee.outputs.exit_code != 0 + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const report = fs.readFileSync('./lychee-report.md', 'utf8'); + + // Truncate if too long + const maxLength = 60000; + const truncatedReport = report.length > maxLength + ? report.substring(0, maxLength) + '\n\n... (report truncated)' + : report; + + const body = `## External Link Check Report + + **Note:** This check is informational and does not block the PR. + +
+ Click to expand report + + ${truncatedReport} + +
+ + --- + *This comment was generated by the external link checker workflow.*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); From 082da398733219c004856eda0a7a66b9c3cc4fe5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 14:53:25 +0100 Subject: [PATCH 03/35] Document external link checking in lint-404s README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add section explaining the relationship between internal link checking (this script) and external link checking (lychee). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/lint-404s/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md index 6db24d2b8955df..698b37b5c891d7 100644 --- a/scripts/lint-404s/README.md +++ b/scripts/lint-404s/README.md @@ -63,3 +63,19 @@ The `ignore-list.txt` file contains paths that should be skipped during checking - `0` - No 404s found - `1` - 404s were detected + +## External Link Checking + +This script only checks **internal links**. External links (to third-party sites) are validated by a separate workflow using [lychee](https://github.com/lycheeverse/lychee). + +See: + +- `.github/workflows/lint-external-links.yml` - The external link check workflow +- `.lychee.toml` - Lychee configuration +- `.lycheeignore` - URLs to ignore during external link checking + +### Why Separate? + +1. **Performance**: External link checking is slower and shouldn't block PRs +2. **False positives**: Many external sites block automated checkers +3. **Different schedules**: External checks run weekly; internal checks run on every PR From bb7232063b4104dbfd654d69e52e744a9994ee1f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 15:19:43 +0100 Subject: [PATCH 04/35] Add pre-commit hook for external link checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a warn-only pre-commit hook that checks external links in changed markdown files using lychee. The hook: - Only runs on docs/ and develop-docs/ markdown files - Shows warnings but doesn't block commits - Gracefully handles missing lychee installation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .pre-commit-config.yaml | 9 +++++++++ scripts/lint-external-links.sh | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100755 scripts/lint-external-links.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b378cd172d02ec..046d49b8c82bd2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,3 +34,12 @@ repos: rev: v1.39.0 hooks: - id: typos + - repo: local + hooks: + - id: lychee + name: Check external links (warn only) + entry: ./scripts/lint-external-links.sh + language: script + types_or: [markdown] + files: ^(docs|develop-docs)/ + verbose: true diff --git a/scripts/lint-external-links.sh b/scripts/lint-external-links.sh new file mode 100755 index 00000000000000..f3be9aa02d9708 --- /dev/null +++ b/scripts/lint-external-links.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Wrapper script for lychee that warns but doesn't block commits +# Used by pre-commit hook + +if ! command -v lychee &> /dev/null; then + echo "Warning: lychee not installed. Skipping external link check." + echo "Install with: brew install lychee" + exit 0 +fi + +# Run lychee on the provided files +lychee --config .lychee.toml --no-progress "$@" +exit_code=$? + +if [ $exit_code -ne 0 ]; then + echo "" + echo "⚠️ External link issues found (commit not blocked)" + echo " Run 'lychee --config .lychee.toml ' for details" +fi + +# Always exit 0 so commit proceeds +exit 0 From 12c24da916f00ca402a2bb4c482395a8c14b2698 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 15:20:04 +0100 Subject: [PATCH 05/35] Document local usage and pre-commit hook in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add instructions for running lychee locally and document the pre-commit hook behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/lint-404s/README.md | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md index 698b37b5c891d7..644c56716bc579 100644 --- a/scripts/lint-404s/README.md +++ b/scripts/lint-404s/README.md @@ -66,15 +66,38 @@ The `ignore-list.txt` file contains paths that should be skipped during checking ## External Link Checking -This script only checks **internal links**. External links (to third-party sites) are validated by a separate workflow using [lychee](https://github.com/lycheeverse/lychee). +This script only checks **internal links**. External links (to third-party sites) are validated separately using [lychee](https://github.com/lycheeverse/lychee). -See: +### Running Locally + +```bash +# Install lychee +brew install lychee + +# Check all docs +lychee --config .lychee.toml "./docs/**/*.md" "./docs/**/*.mdx" + +# Check a specific file +lychee --config .lychee.toml docs/platforms/javascript/index.mdx +``` + +### Pre-commit Hook + +A pre-commit hook checks external links in changed files (warn-only, won't block commits). Requires lychee to be installed locally. + +### CI Workflow + +The GitHub workflow (`.github/workflows/lint-external-links.yml`) runs: +- Weekly on a schedule (creates/updates issue with broken links) +- On PRs (adds non-blocking comment) +- Manually via workflow dispatch + +### Configuration Files -- `.github/workflows/lint-external-links.yml` - The external link check workflow - `.lychee.toml` - Lychee configuration -- `.lycheeignore` - URLs to ignore during external link checking +- `.lycheeignore` - URLs to ignore during checking -### Why Separate? +### Why Separate from Internal Link Checking? 1. **Performance**: External link checking is slower and shouldn't block PRs 2. **False positives**: Many external sites block automated checkers From 061980b7b93668a4509997f6f8ff628770bbcf43 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 15:21:25 +0100 Subject: [PATCH 06/35] Simplify pre-commit hook by inlining command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove separate shell script and use inline bash command with || true to achieve warn-only behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .pre-commit-config.yaml | 4 ++-- scripts/lint-external-links.sh | 22 ---------------------- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100755 scripts/lint-external-links.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 046d49b8c82bd2..5d467349060c0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,8 +38,8 @@ repos: hooks: - id: lychee name: Check external links (warn only) - entry: ./scripts/lint-external-links.sh - language: script + entry: bash -c 'command -v lychee >/dev/null && lychee --config .lychee.toml --no-progress "$@" || true' -- + language: system types_or: [markdown] files: ^(docs|develop-docs)/ verbose: true diff --git a/scripts/lint-external-links.sh b/scripts/lint-external-links.sh deleted file mode 100755 index f3be9aa02d9708..00000000000000 --- a/scripts/lint-external-links.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Wrapper script for lychee that warns but doesn't block commits -# Used by pre-commit hook - -if ! command -v lychee &> /dev/null; then - echo "Warning: lychee not installed. Skipping external link check." - echo "Install with: brew install lychee" - exit 0 -fi - -# Run lychee on the provided files -lychee --config .lychee.toml --no-progress "$@" -exit_code=$? - -if [ $exit_code -ne 0 ]; then - echo "" - echo "⚠️ External link issues found (commit not blocked)" - echo " Run 'lychee --config .lychee.toml ' for details" -fi - -# Always exit 0 so commit proceeds -exit 0 From 9a514bdc1600352e8a79072d4cf5b5a149734597 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 15:24:11 +0100 Subject: [PATCH 07/35] Add Lychee cache to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d9228e6660908a..37df37dde887f0 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,6 @@ public/og-images/* yalc.lock /public/doctree.json /public/doctree-dev.json + +# Lychee cache +.lycheecache From 197ca55446b5edfca002651c697228bad19b80e7 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 15:25:38 +0100 Subject: [PATCH 08/35] Use TypeScript for pre-commit hook (cross-platform) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace bash one-liner with TypeScript script for Windows compatibility. Uses bun like other scripts in the repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .pre-commit-config.yaml | 2 +- scripts/lint-external-links.ts | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 scripts/lint-external-links.ts diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d467349060c0c..2b3b2582fcc548 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: lychee name: Check external links (warn only) - entry: bash -c 'command -v lychee >/dev/null && lychee --config .lychee.toml --no-progress "$@" || true' -- + entry: bun scripts/lint-external-links.ts language: system types_or: [markdown] files: ^(docs|develop-docs)/ diff --git a/scripts/lint-external-links.ts b/scripts/lint-external-links.ts new file mode 100644 index 00000000000000..ca6de30f95dda5 --- /dev/null +++ b/scripts/lint-external-links.ts @@ -0,0 +1,35 @@ +/** + * Pre-commit hook wrapper for lychee external link checker. + * Runs lychee on provided files and warns on broken links without blocking commits. + * + * Usage: bun scripts/lint-external-links.ts [files...] + */ + +import {spawnSync} from 'child_process'; + +// Check if lychee is installed +const whichResult = spawnSync('which', ['lychee'], {encoding: 'utf-8'}); +if (whichResult.status !== 0) { + console.log('Warning: lychee not installed. Skipping external link check.'); + console.log('Install with: brew install lychee'); + process.exit(0); +} + +const files = process.argv.slice(2); +if (files.length === 0) { + process.exit(0); +} + +// Run lychee on the provided files +const result = spawnSync('lychee', ['--config', '.lychee.toml', '--no-progress', ...files], { + stdio: 'inherit', + encoding: 'utf-8', +}); + +if (result.status !== 0) { + console.log(''); + console.log('⚠️ External link issues found (commit not blocked)'); +} + +// Always exit 0 so commit proceeds +process.exit(0); From d8f00ec4b8a20fb474f79d7d1a980b0fd0fe1e2a Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 15:29:43 +0100 Subject: [PATCH 09/35] Only check changed files in PR workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use git diff to get list of changed markdown files for PRs, making the check faster. Full scans still run on schedule and manual dispatch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/lint-external-links.yml | 41 ++++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index c91fbbf5544f5a..8a6f00ddced37d 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -32,6 +32,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Restore lychee cache to reduce load on external sites - name: Restore lychee cache @@ -41,8 +43,38 @@ jobs: key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- - - name: Check external links - id: lychee + # For PRs: only check changed markdown files + - name: Get changed files + id: changed + if: github.event_name == 'pull_request' + run: | + FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}...HEAD -- '*.md' '*.mdx' | grep -E '^(docs|develop-docs)/' || true) + if [ -z "$FILES" ]; then + echo "files=" >> $GITHUB_OUTPUT + echo "No markdown files changed in docs/" + else + echo "files<> $GITHUB_OUTPUT + echo "$FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "Changed files:" + echo "$FILES" + fi + + - name: Check external links (changed files only) + id: lychee-pr + if: github.event_name == 'pull_request' && steps.changed.outputs.files != '' + uses: lycheeverse/lychee-action@v2 + with: + args: --cache --max-cache-age 1d --config .lychee.toml --no-progress ${{ steps.changed.outputs.files }} + fail: false + output: ./lychee-report.md + format: markdown + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check external links (all files) + id: lychee-full + if: github.event_name != 'pull_request' uses: lycheeverse/lychee-action@v2 with: args: >- @@ -56,7 +88,6 @@ jobs: "./docs/**/*.mdx" "./develop-docs/**/*.md" "./develop-docs/**/*.mdx" - # Don't fail on scheduled runs or PRs - only on manual dispatch if requested fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} output: ./lychee-report.md format: markdown @@ -65,7 +96,7 @@ jobs: # For scheduled runs: Create/update an issue with broken links - name: Create/Update Issue for Broken Links - if: github.event_name == 'schedule' && steps.lychee.outputs.exit_code != 0 + if: github.event_name == 'schedule' && steps.lychee-full.outputs.exit_code != 0 uses: peter-evans/create-issue-from-file@v5 with: title: 'Weekly External Link Check: Broken Links Found' @@ -78,7 +109,7 @@ jobs: # For PRs: Add a comment with the report (non-blocking) - name: Comment on PR with Link Report - if: github.event_name == 'pull_request' && steps.lychee.outputs.exit_code != 0 + if: github.event_name == 'pull_request' && steps.lychee-pr.outputs.exit_code != 0 uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 0b9475315331fb2a75fcdc29cf72c88a9a661740 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:31:58 +0000 Subject: [PATCH 10/35] [getsentry/action-github-commit] Auto commit --- scripts/lint-404s/README.md | 1 + scripts/lint-external-links.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md index 644c56716bc579..045a3246d22dd1 100644 --- a/scripts/lint-404s/README.md +++ b/scripts/lint-404s/README.md @@ -88,6 +88,7 @@ A pre-commit hook checks external links in changed files (warn-only, won't block ### CI Workflow The GitHub workflow (`.github/workflows/lint-external-links.yml`) runs: + - Weekly on a schedule (creates/updates issue with broken links) - On PRs (adds non-blocking comment) - Manually via workflow dispatch diff --git a/scripts/lint-external-links.ts b/scripts/lint-external-links.ts index ca6de30f95dda5..66aeeadff829c8 100644 --- a/scripts/lint-external-links.ts +++ b/scripts/lint-external-links.ts @@ -21,10 +21,14 @@ if (files.length === 0) { } // Run lychee on the provided files -const result = spawnSync('lychee', ['--config', '.lychee.toml', '--no-progress', ...files], { - stdio: 'inherit', - encoding: 'utf-8', -}); +const result = spawnSync( + 'lychee', + ['--config', '.lychee.toml', '--no-progress', ...files], + { + stdio: 'inherit', + encoding: 'utf-8', + } +); if (result.status !== 0) { console.log(''); From e71be850fb3c3e3a575725f7b9e806001fbb94a2 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 16:13:26 +0100 Subject: [PATCH 11/35] Fix lychee config to reduce false positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scheme filter to only check http/https (skip root-relative links) - Accept 403/418 status codes (bot blocking, freedesktop teapot) - Add ignore patterns for: - Bot-blocking sites (npmjs, maven, medium, gitlab, epicgames) - Private resources (Notion, private GitHub repos, Zendesk) - Unstable docs (freedesktop) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .lychee.toml | 6 +++++- .lycheeignore | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.lychee.toml b/.lychee.toml index 85c34fb49e0bdb..01926578e0ff7c 100644 --- a/.lychee.toml +++ b/.lychee.toml @@ -1,6 +1,9 @@ # Lychee configuration for external link checking # Documentation: https://github.com/lycheeverse/lychee +# Only check http/https links (ignore root-relative internal links) +scheme = ["https", "http"] + # Maximum number of concurrent requests max_concurrency = 32 @@ -17,7 +20,8 @@ retry_wait_time = 2 user_agent = "Mozilla/5.0 (compatible; Sentry-Docs-Link-Checker; +https://github.com/getsentry/sentry-docs)" # Accept common status codes that indicate the link works -accept = [200, 201, 202, 203, 204, 206, 301, 302, 308] +# Include 403 (bot blocking) and 418 (freedesktop teapot) to reduce noise +accept = [200, 201, 202, 203, 204, 206, 301, 302, 308, 403, 418] # Only check external links (our internal check handles internal ones) include_mail = false diff --git a/.lycheeignore b/.lycheeignore index b09ceeb0d1f5fe..64eedfc5466906 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -17,10 +17,29 @@ https?://twitter\.com.* https?://x\.com.* https?://linkedin\.com.* https?://www\.linkedin\.com.* +https?://www\.npmjs\.com.* +https?://search\.maven\.org.* +https?://medium\.com.* +https?://.*\.medium\.com.* +https?://gitlab\.com/oauth/.* +https?://docs\.gitlab\.com.* +https?://dev\.epicgames\.com.* +https?://docs\.unrealengine\.com.* +https?://cursor\.com.* +https?://dash\.cloudflare\.com.* +https?://www\.freedesktop\.org.* # Interactive demos that may not respond to HEAD requests https?://demo\.arcade\.software.* +# Private/internal resources +https?://.*\.notion\.so.* +https?://www\.notion\.so.* +https?://github\.com/getsentry/getsentry.* +https?://github\.com/getsentry/sentry-options-automator.* +https?://github\.com/getsentry/etl.* +https?://sentry\.zendesk\.com.* + # Placeholder domains commonly used in docs https?://api\.example\.com.* https?://your-api-host.* From 0cb8b709f9862f7b3445d9c5761c6ef6fe871f0f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 16:26:31 +0100 Subject: [PATCH 12/35] Use base_url to resolve root-relative links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set base_url to docs.sentry.io so lychee can resolve root-relative links, then exclude docs.sentry.io from checking (internal links are already covered by lint-404s). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .lychee.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.lychee.toml b/.lychee.toml index 01926578e0ff7c..4ee78469d11db1 100644 --- a/.lychee.toml +++ b/.lychee.toml @@ -1,8 +1,11 @@ # Lychee configuration for external link checking # Documentation: https://github.com/lycheeverse/lychee -# Only check http/https links (ignore root-relative internal links) -scheme = ["https", "http"] +# Base URL to resolve root-relative links +base_url = "https://docs.sentry.io" + +# Exclude internal links (already handled by lint-404s script) +exclude = ['^https://docs\.sentry\.io'] # Maximum number of concurrent requests max_concurrency = 32 From 6b13baf590ccc45851224c50b1e3077e0160158f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 19:04:04 +0100 Subject: [PATCH 13/35] Add ignore patterns for TLS-incompatible and internal sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After manually testing ERROR entries from lychee.log: - bottlepy.org: TLS 1.3 only, incompatible with lychee's native-tls - help.revise.dev: Cloudflare ECH required, fails even with curl - dev.getsentry.net: Internal development URLs - sentry-content-dashboard: Internal dashboard (401) - godoc.org/pkg.go.dev: Rate-limited (429) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .lycheeignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.lycheeignore b/.lycheeignore index 64eedfc5466906..b694d751cced03 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -11,6 +11,11 @@ https?://___.*___.* https?://.*localhost.* https?://127\.0\.0\.1.* https?://0\.0\.0\.0.* +https?://10\.0\.2\.2.* + +# Internal Sentry development URLs +https?://.*\.getsentry\.net.* +https?://sentry-content-dashboard\.sentry\.dev.* # Sites known to block automated checkers https?://twitter\.com.* @@ -29,6 +34,18 @@ https?://cursor\.com.* https?://dash\.cloudflare\.com.* https?://www\.freedesktop\.org.* +# TLS compatibility issues (sites work in browser but fail in lychee due to native-tls) +# bottlepy.org only supports TLS 1.3, incompatible with lychee's TLS backend +https?://bottlepy\.org.* + +# Cloudflare ECH (Encrypted Client Hello) required - fails even with curl/openssl +https?://help\.revise\.dev.* +https?://.*\.intercomhelpcenter\.com.* + +# Rate-limited sites (may fail intermittently with 429) +https?://godoc\.org.* +https?://pkg\.go\.dev.* + # Interactive demos that may not respond to HEAD requests https?://demo\.arcade\.software.* From 86594d3eb65b5fa7ecbbdbb130f2f3da63e3a8b1 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 20:39:25 +0100 Subject: [PATCH 14/35] Use optional credentials pattern for private IPs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed from separate entries to using regex optional group (.+@)? to match private IPs with or without credentials (e.g., token@10.0.2.2). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .lycheeignore | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.lycheeignore b/.lycheeignore index b694d751cced03..4a1fb4e7f2d625 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -7,11 +7,13 @@ https?://your-.* https?://.*\.example\..* https?://___.*___.* -# Localhost and local dev URLs -https?://.*localhost.* -https?://127\.0\.0\.1.* -https?://0\.0\.0\.0.* -https?://10\.0\.2\.2.* +# Localhost and local dev URLs (with optional credentials) +https?://(.+@)?.*localhost.* +https?://(.+@)?127\.0\.0\.1.* +https?://(.+@)?0\.0\.0\.0.* +https?://(.+@)?10\..* +https?://(.+@)?172\.(1[6-9]|2[0-9]|3[0-1])\..* +https?://(.+@)?192\.168\..* # Internal Sentry development URLs https?://.*\.getsentry\.net.* From 4a963adfafd26f360f94ba50bcc23b4e8c61987f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 20:42:17 +0100 Subject: [PATCH 15/35] Refactor workflow: separate PR and full-scan jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split into two jobs for clarity: - check-pr: PRs only, changed files, adds comment - check-full: Schedule/manual, all files, creates issue Removed caching (wasn't working with per-commit keys). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/lint-external-links.yml | 105 +++++++++++----------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 8a6f00ddced37d..49ca59eac7d66b 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -24,10 +24,11 @@ on: - '.lycheeignore' jobs: - check-external-links: + # Job for PRs: check only changed files, add comment + check-pr: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest permissions: - issues: write pull-requests: write steps: @@ -35,18 +36,8 @@ jobs: with: fetch-depth: 0 - # Restore lychee cache to reduce load on external sites - - name: Restore lychee cache - uses: actions/cache@v4 - with: - path: .lycheecache - key: cache-lychee-${{ github.sha }} - restore-keys: cache-lychee- - - # For PRs: only check changed markdown files - name: Get changed files id: changed - if: github.event_name == 'pull_request' run: | FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}...HEAD -- '*.md' '*.mdx' | grep -E '^(docs|develop-docs)/' || true) if [ -z "$FILES" ]; then @@ -60,56 +51,20 @@ jobs: echo "$FILES" fi - - name: Check external links (changed files only) - id: lychee-pr - if: github.event_name == 'pull_request' && steps.changed.outputs.files != '' + - name: Check external links + id: lychee + if: steps.changed.outputs.files != '' uses: lycheeverse/lychee-action@v2 with: - args: --cache --max-cache-age 1d --config .lychee.toml --no-progress ${{ steps.changed.outputs.files }} + args: --config .lychee.toml --no-progress ${{ steps.changed.outputs.files }} fail: false output: ./lychee-report.md format: markdown env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Check external links (all files) - id: lychee-full - if: github.event_name != 'pull_request' - uses: lycheeverse/lychee-action@v2 - with: - args: >- - --cache - --max-cache-age 1d - --config .lychee.toml - --exclude-path node_modules - --exclude-path .next - --exclude-path public - "./docs/**/*.md" - "./docs/**/*.mdx" - "./develop-docs/**/*.md" - "./develop-docs/**/*.mdx" - fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} - output: ./lychee-report.md - format: markdown - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # For scheduled runs: Create/update an issue with broken links - - name: Create/Update Issue for Broken Links - if: github.event_name == 'schedule' && steps.lychee-full.outputs.exit_code != 0 - uses: peter-evans/create-issue-from-file@v5 - with: - title: 'Weekly External Link Check: Broken Links Found' - content-filepath: ./lychee-report.md - labels: | - automated - documentation - broken-links - update-existing: true - - # For PRs: Add a comment with the report (non-blocking) - - name: Comment on PR with Link Report - if: github.event_name == 'pull_request' && steps.lychee-pr.outputs.exit_code != 0 + - name: Comment on PR + if: steps.lychee.outputs.exit_code != 0 uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -117,7 +72,6 @@ jobs: const fs = require('fs'); const report = fs.readFileSync('./lychee-report.md', 'utf8'); - // Truncate if too long const maxLength = 60000; const truncatedReport = report.length > maxLength ? report.substring(0, maxLength) + '\n\n... (report truncated)' @@ -143,3 +97,44 @@ jobs: repo: context.repo.repo, body: body }); + + # Job for scheduled/manual runs: check all files, create issue + check-full: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/checkout@v4 + + - name: Check external links + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --config .lychee.toml + --exclude-path node_modules + --exclude-path .next + --exclude-path public + "./docs/**/*.md" + "./docs/**/*.mdx" + "./develop-docs/**/*.md" + "./develop-docs/**/*.mdx" + fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} + output: ./lychee-report.md + format: markdown + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create/Update Issue + if: steps.lychee.outputs.exit_code != 0 + uses: peter-evans/create-issue-from-file@v5 + with: + title: 'Weekly External Link Check: Broken Links Found' + content-filepath: ./lychee-report.md + labels: | + automated + documentation + broken-links + update-existing: true From 7419a695811c7569deb3129c924bacb4b2041aca Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 21:07:41 +0100 Subject: [PATCH 16/35] Refine external link checker workflow and config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename .lychee.toml to lychee.toml (default config name) - Remove --config args since lychee.toml is auto-detected - Simplify workflow: use '.' instead of listing directories - Split workflow into separate PR and full-scan jobs - Update PR job to update existing comment instead of creating new ones - Update full-scan job to update existing issue instead of creating duplicates - Add file existence checks before reading reports - Use appropriate GitHub labels (Bug, Team: Docs, Product Area: Docs) - Add proper permissions scoping per job 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/lint-external-links.yml | 100 ++++++++++++++++------ .lychee.toml => lychee.toml | 3 + scripts/lint-404s/README.md | 8 +- scripts/lint-external-links.ts | 2 +- 4 files changed, 80 insertions(+), 33 deletions(-) rename .lychee.toml => lychee.toml (95%) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 49ca59eac7d66b..a621258f3a5e45 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -17,11 +17,6 @@ on: # Run on PRs that modify docs (non-blocking) pull_request: branches: [master] - paths: - - 'docs/**' - - 'develop-docs/**' - - '.lychee.toml' - - '.lycheeignore' jobs: # Job for PRs: check only changed files, add comment @@ -56,7 +51,7 @@ jobs: if: steps.changed.outputs.files != '' uses: lycheeverse/lychee-action@v2 with: - args: --config .lychee.toml --no-progress ${{ steps.changed.outputs.files }} + args: --no-progress ${{ steps.changed.outputs.files }} fail: false output: ./lychee-report.md format: markdown @@ -70,6 +65,13 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); + + // Check if report file exists + if (!fs.existsSync('./lychee-report.md')) { + console.log('No report file found, skipping comment'); + return; + } + const report = fs.readFileSync('./lychee-report.md', 'utf8'); const maxLength = 60000; @@ -91,13 +93,36 @@ jobs: --- *This comment was generated by the external link checker workflow.*`; - github.rest.issues.createComment({ - issue_number: context.issue.number, + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - body: body + issue_number: context.issue.number, }); + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('External Link Check Report') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + # Job for scheduled/manual runs: check all files, create issue check-full: if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' @@ -112,29 +137,48 @@ jobs: id: lychee uses: lycheeverse/lychee-action@v2 with: - args: >- - --config .lychee.toml - --exclude-path node_modules - --exclude-path .next - --exclude-path public - "./docs/**/*.md" - "./docs/**/*.mdx" - "./develop-docs/**/*.md" - "./develop-docs/**/*.mdx" + args: . fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} output: ./lychee-report.md format: markdown env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create/Update Issue + - name: Find existing issue if: steps.lychee.outputs.exit_code != 0 - uses: peter-evans/create-issue-from-file@v5 - with: - title: 'Weekly External Link Check: Broken Links Found' - content-filepath: ./lychee-report.md - labels: | - automated - documentation - broken-links - update-existing: true + id: find-issue + run: | + ISSUE_NUMBER=$(gh issue list \ + --state open \ + --search "Weekly External Link Check: Broken Links Found in:title" \ + --json number \ + --jq '.[0].number // ""') + echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create or update issue + if: steps.lychee.outputs.exit_code != 0 + run: | + # Check if report file exists + if [ ! -f ./lychee-report.md ]; then + echo "No report file found, skipping issue creation" + exit 0 + fi + + ISSUE_BODY=$(cat ./lychee-report.md) + if [ -n "${{ steps.find-issue.outputs.number }}" ]; then + gh issue edit "${{ steps.find-issue.outputs.number }}" \ + --body "$ISSUE_BODY" + echo "Updated issue #${{ steps.find-issue.outputs.number }}" + else + gh issue create \ + --title "Weekly External Link Check: Broken Links Found" \ + --body "$ISSUE_BODY" \ + --label "Bug" \ + --label "Team: Docs" \ + --label "Product Area: Docs" + echo "Created new issue" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.lychee.toml b/lychee.toml similarity index 95% rename from .lychee.toml rename to lychee.toml index 4ee78469d11db1..1338e31cca8406 100644 --- a/.lychee.toml +++ b/lychee.toml @@ -4,6 +4,9 @@ # Base URL to resolve root-relative links base_url = "https://docs.sentry.io" +# Only check markdown files +include = ['\\.mdx?$'] + # Exclude internal links (already handled by lint-404s script) exclude = ['^https://docs\.sentry\.io'] diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md index 045a3246d22dd1..1bf7ad27d34150 100644 --- a/scripts/lint-404s/README.md +++ b/scripts/lint-404s/README.md @@ -74,11 +74,11 @@ This script only checks **internal links**. External links (to third-party sites # Install lychee brew install lychee -# Check all docs -lychee --config .lychee.toml "./docs/**/*.md" "./docs/**/*.mdx" +# Check all markdown files in the repo +lychee . # Check a specific file -lychee --config .lychee.toml docs/platforms/javascript/index.mdx +lychee docs/platforms/javascript/index.mdx ``` ### Pre-commit Hook @@ -95,7 +95,7 @@ The GitHub workflow (`.github/workflows/lint-external-links.yml`) runs: ### Configuration Files -- `.lychee.toml` - Lychee configuration +- `lychee.toml` - Lychee configuration - `.lycheeignore` - URLs to ignore during checking ### Why Separate from Internal Link Checking? diff --git a/scripts/lint-external-links.ts b/scripts/lint-external-links.ts index 66aeeadff829c8..cd4964f4387f59 100644 --- a/scripts/lint-external-links.ts +++ b/scripts/lint-external-links.ts @@ -23,7 +23,7 @@ if (files.length === 0) { // Run lychee on the provided files const result = spawnSync( 'lychee', - ['--config', '.lychee.toml', '--no-progress', ...files], + ['--no-progress', ...files], { stdio: 'inherit', encoding: 'utf-8', From 43ebc4d9ac8d6efdb557e99e4aca45fc15d11344 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:15:06 +0000 Subject: [PATCH 17/35] [getsentry/action-github-commit] Auto commit --- scripts/lint-external-links.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/lint-external-links.ts b/scripts/lint-external-links.ts index cd4964f4387f59..53f99f168b4206 100644 --- a/scripts/lint-external-links.ts +++ b/scripts/lint-external-links.ts @@ -21,14 +21,10 @@ if (files.length === 0) { } // Run lychee on the provided files -const result = spawnSync( - 'lychee', - ['--no-progress', ...files], - { - stdio: 'inherit', - encoding: 'utf-8', - } -); +const result = spawnSync('lychee', ['--no-progress', ...files], { + stdio: 'inherit', + encoding: 'utf-8', +}); if (result.status !== 0) { console.log(''); From 7534a9ea8f7a701d62e1b1a39cff49accdc5d057 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 21:54:48 +0100 Subject: [PATCH 18/35] Improve lychee config: add exclude_all_private and remove directory filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scheme, exclude_all_private, and include_fragments settings to lychee.toml - Remove redundant private IP patterns from .lycheeignore (now handled by config) - Remove docs/develop-docs directory filters to check all markdown files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/lint-external-links.yml | 4 ++-- .lycheeignore | 9 +-------- .pre-commit-config.yaml | 1 - lychee.toml | 11 +++++++++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index a621258f3a5e45..59962cde80de1b 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -34,10 +34,10 @@ jobs: - name: Get changed files id: changed run: | - FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}...HEAD -- '*.md' '*.mdx' | grep -E '^(docs|develop-docs)/' || true) + FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}...HEAD -- '*.md' '*.mdx' || true) if [ -z "$FILES" ]; then echo "files=" >> $GITHUB_OUTPUT - echo "No markdown files changed in docs/" + echo "No markdown files changed" else echo "files<> $GITHUB_OUTPUT echo "$FILES" >> $GITHUB_OUTPUT diff --git a/.lycheeignore b/.lycheeignore index 4a1fb4e7f2d625..6416d8fc381fab 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -1,5 +1,6 @@ # URLs to ignore during external link checking # Supports regex patterns - lines starting with # are comments +# Note: Private IPs (localhost, 10.x, 172.16-31.x, 192.168.x) are handled by exclude_all_private in lychee.toml # Example/placeholder URLs https?://example\.com.* @@ -7,14 +8,6 @@ https?://your-.* https?://.*\.example\..* https?://___.*___.* -# Localhost and local dev URLs (with optional credentials) -https?://(.+@)?.*localhost.* -https?://(.+@)?127\.0\.0\.1.* -https?://(.+@)?0\.0\.0\.0.* -https?://(.+@)?10\..* -https?://(.+@)?172\.(1[6-9]|2[0-9]|3[0-1])\..* -https?://(.+@)?192\.168\..* - # Internal Sentry development URLs https?://.*\.getsentry\.net.* https?://sentry-content-dashboard\.sentry\.dev.* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b3b2582fcc548..c2b5d0ab03897c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,5 +41,4 @@ repos: entry: bun scripts/lint-external-links.ts language: system types_or: [markdown] - files: ^(docs|develop-docs)/ verbose: true diff --git a/lychee.toml b/lychee.toml index 1338e31cca8406..2f92d381f38eec 100644 --- a/lychee.toml +++ b/lychee.toml @@ -4,8 +4,11 @@ # Base URL to resolve root-relative links base_url = "https://docs.sentry.io" -# Only check markdown files -include = ['\\.mdx?$'] +# Only check HTTP and HTTPS links +scheme = ["https", "http"] + +# Exclude all private IP addresses automatically (localhost, 10.x, 172.16-31.x, 192.168.x, etc.) +exclude_all_private = true # Exclude internal links (already handled by lint-404s script) exclude = ['^https://docs\.sentry\.io'] @@ -29,6 +32,10 @@ user_agent = "Mozilla/5.0 (compatible; Sentry-Docs-Link-Checker; +https://github # Include 403 (bot blocking) and 418 (freedesktop teapot) to reduce noise accept = [200, 201, 202, 203, 204, 206, 301, 302, 308, 403, 418] +# Don't validate URL fragments/anchors (e.g., #section-name) +# Fragment checking is unreliable: JS-rendered anchors appear broken, and many sites don't validate them +include_fragments = false + # Only check external links (our internal check handles internal ones) include_mail = false include_verbatim = false From 76cf9a19de181f5799033a2e3e9e3ef157c14863 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 23:44:24 +0100 Subject: [PATCH 19/35] Refactor PR link check workflow: remove comment step and unnecessary permissions --- .github/workflows/lint-external-links.yml | 77 +---------------------- 1 file changed, 3 insertions(+), 74 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 59962cde80de1b..00a134c111345d 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -19,12 +19,10 @@ on: branches: [master] jobs: - # Job for PRs: check only changed files, add comment + # Job for PRs: check only changed files check-pr: if: github.event_name == 'pull_request' runs-on: ubuntu-latest - permissions: - pull-requests: write steps: - uses: actions/checkout@v4 @@ -47,82 +45,13 @@ jobs: fi - name: Check external links - id: lychee if: steps.changed.outputs.files != '' uses: lycheeverse/lychee-action@v2 with: - args: --no-progress ${{ steps.changed.outputs.files }} - fail: false - output: ./lychee-report.md - format: markdown + args: --verbose --no-progress ${{ steps.changed.outputs.files }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Comment on PR - if: steps.lychee.outputs.exit_code != 0 - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - - // Check if report file exists - if (!fs.existsSync('./lychee-report.md')) { - console.log('No report file found, skipping comment'); - return; - } - - const report = fs.readFileSync('./lychee-report.md', 'utf8'); - - const maxLength = 60000; - const truncatedReport = report.length > maxLength - ? report.substring(0, maxLength) + '\n\n... (report truncated)' - : report; - - const body = `## External Link Check Report - - **Note:** This check is informational and does not block the PR. - -
- Click to expand report - - ${truncatedReport} - -
- - --- - *This comment was generated by the external link checker workflow.*`; - - // Find existing comment - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('External Link Check Report') - ); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: body - }); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } - # Job for scheduled/manual runs: check all files, create issue check-full: if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' @@ -137,7 +66,7 @@ jobs: id: lychee uses: lycheeverse/lychee-action@v2 with: - args: . + args: --verbose . fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} output: ./lychee-report.md format: markdown From f0ea8a64e97ff4e09b6447512a6af0905d452774 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 23:56:10 +0100 Subject: [PATCH 20/35] Update README.md: clarify external link checking behavior in PRs and configuration details --- scripts/lint-404s/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md index 1bf7ad27d34150..b7e23c1aba2849 100644 --- a/scripts/lint-404s/README.md +++ b/scripts/lint-404s/README.md @@ -90,7 +90,7 @@ A pre-commit hook checks external links in changed files (warn-only, won't block The GitHub workflow (`.github/workflows/lint-external-links.yml`) runs: - Weekly on a schedule (creates/updates issue with broken links) -- On PRs (adds non-blocking comment) +- On PRs (checks changed files only) - Manually via workflow dispatch ### Configuration Files @@ -102,4 +102,4 @@ The GitHub workflow (`.github/workflows/lint-external-links.yml`) runs: 1. **Performance**: External link checking is slower and shouldn't block PRs 2. **False positives**: Many external sites block automated checkers -3. **Different schedules**: External checks run weekly; internal checks run on every PR +3. **Different scope**: External checks only run on changed files in PRs; internal checks validate all pages From de36c8ec31d6a773a2204398336e02a8f4ae495e Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 30 Dec 2025 23:58:09 +0100 Subject: [PATCH 21/35] Use cross-platform lychee detection in pre-commit hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Unix-specific `which` command with `lychee --version` check that works on Windows, macOS, and Linux. Also add cargo install option to the help message. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/lint-external-links.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/lint-external-links.ts b/scripts/lint-external-links.ts index 53f99f168b4206..48b5d8cb450df2 100644 --- a/scripts/lint-external-links.ts +++ b/scripts/lint-external-links.ts @@ -8,10 +8,10 @@ import {spawnSync} from 'child_process'; // Check if lychee is installed -const whichResult = spawnSync('which', ['lychee'], {encoding: 'utf-8'}); -if (whichResult.status !== 0) { +const versionCheck = spawnSync('lychee', ['--version'], {encoding: 'utf-8', stdio: 'pipe'}); +if (versionCheck.error || versionCheck.status !== 0) { console.log('Warning: lychee not installed. Skipping external link check.'); - console.log('Install with: brew install lychee'); + console.log('Install with: brew install lychee (macOS) or cargo install lychee (cross-platform)'); process.exit(0); } From 2c7df02048d1ebf7a12e014f2519435313c8d08b Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:45:13 +0000 Subject: [PATCH 22/35] [getsentry/action-github-commit] Auto commit --- scripts/lint-external-links.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint-external-links.ts b/scripts/lint-external-links.ts index 48b5d8cb450df2..baa947f3e4e1a2 100644 --- a/scripts/lint-external-links.ts +++ b/scripts/lint-external-links.ts @@ -8,10 +8,15 @@ import {spawnSync} from 'child_process'; // Check if lychee is installed -const versionCheck = spawnSync('lychee', ['--version'], {encoding: 'utf-8', stdio: 'pipe'}); +const versionCheck = spawnSync('lychee', ['--version'], { + encoding: 'utf-8', + stdio: 'pipe', +}); if (versionCheck.error || versionCheck.status !== 0) { console.log('Warning: lychee not installed. Skipping external link check.'); - console.log('Install with: brew install lychee (macOS) or cargo install lychee (cross-platform)'); + console.log( + 'Install with: brew install lychee (macOS) or cargo install lychee (cross-platform)' + ); process.exit(0); } From eb1055564466229159d8a60cc1f251adac0763a3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 31 Dec 2025 09:59:44 +0100 Subject: [PATCH 23/35] cleanup --- .github/workflows/lint-external-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 00a134c111345d..18ca4e68bdc47b 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -67,7 +67,7 @@ jobs: uses: lycheeverse/lychee-action@v2 with: args: --verbose . - fail: ${{ github.event_name == 'workflow_dispatch' && inputs.fail_on_errors == true }} + fail: ${{ inputs.fail_on_errors == true }} output: ./lychee-report.md format: markdown env: From b88debc883da09853f410478b184fa373c46793c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 31 Dec 2025 10:27:50 +0100 Subject: [PATCH 24/35] Add GitHub Actions cache for lychee link checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weekly scheduled runs save the cache, PR checks restore it. This reduces load on external sites and speeds up PR checks. Cache lifetime is 2 weeks to ensure it survives between weekly runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/lint-external-links.yml | 14 ++++++++++++++ lychee.toml | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 18ca4e68bdc47b..b3a2400f783928 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -44,6 +44,14 @@ jobs: echo "$FILES" fi + - name: Restore lychee cache + if: steps.changed.outputs.files != '' + uses: actions/cache/restore@v4 + with: + path: .lycheecache + key: lychee-cache- + restore-keys: lychee-cache- + - name: Check external links if: steps.changed.outputs.files != '' uses: lycheeverse/lychee-action@v2 @@ -73,6 +81,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Save lychee cache + uses: actions/cache/save@v4 + with: + path: .lycheecache + key: lychee-cache-${{ github.run_id }} + - name: Find existing issue if: steps.lychee.outputs.exit_code != 0 id: find-issue diff --git a/lychee.toml b/lychee.toml index 2f92d381f38eec..a70d0775c3c3b4 100644 --- a/lychee.toml +++ b/lychee.toml @@ -44,5 +44,6 @@ include_verbatim = false max_redirects = 10 # Cache settings (reduce load on external sites) +# Cache is populated weekly by scheduled job and used by PR checks cache = true -max_cache_age = "1d" +max_cache_age = "2w" From 06ff59dcdab43d7e4d1e11defa9eaa4a8ebecd3d Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 31 Dec 2025 11:01:10 +0100 Subject: [PATCH 25/35] Exclude transient errors from lychee cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skip caching 429 (rate limit) and 5xx (server error) responses so they get retried on subsequent runs. This ensures transient failures don't persist in the cache while stable results are still reused. Also restore cache on scheduled runs so successful checks from the previous week are skipped. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/lint-external-links.yml | 11 +++++++++++ lychee.toml | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index b3a2400f783928..e6bdfa09471ecd 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -70,6 +70,17 @@ jobs: steps: - uses: actions/checkout@v4 + # Cache strategy: see lychee.toml for details + # - Restore previous cache so successful checks are skipped + # - Transient errors (429, 5xx) are excluded from cache and retried + # - Save updated cache for next run + - name: Restore lychee cache + uses: actions/cache/restore@v4 + with: + path: .lycheecache + key: lychee-cache- + restore-keys: lychee-cache- + - name: Check external links id: lychee uses: lycheeverse/lychee-action@v2 diff --git a/lychee.toml b/lychee.toml index a70d0775c3c3b4..400fcd20daaaf1 100644 --- a/lychee.toml +++ b/lychee.toml @@ -43,7 +43,16 @@ include_verbatim = false # Follow redirects max_redirects = 10 -# Cache settings (reduce load on external sites) -# Cache is populated weekly by scheduled job and used by PR checks +# Cache settings +# +# Strategy: Weekly scheduled runs populate the cache, PR checks consume it. +# - Successful responses (200, 301, 403, 404) are cached and skipped on subsequent runs +# - Transient errors (429 rate limits, 5xx server errors) are NOT cached, so they get retried +# - Cache lifetime is just under 2 weeks so it survives between weekly runs +# +# This means each weekly run only re-checks: +# 1. Links that failed with transient errors last time +# 2. New links not yet in cache cache = true -max_cache_age = "2w" +max_cache_age = "335h" +cache_exclude_status = ["429", "500.."] From 2135419b6ae50179fe9cad79175e82f55feff5d4 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 10:15:52 +0100 Subject: [PATCH 26/35] Refactor external link checker workflow to enforce failure on broken links and simplify issue handling --- .github/workflows/lint-external-links.yml | 87 +++++++++++------------ 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index e6bdfa09471ecd..a8850eaf2d7d22 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -7,12 +7,6 @@ on: # Allow manual triggering workflow_dispatch: - inputs: - fail_on_errors: - description: 'Fail workflow if broken links found' - required: false - default: false - type: boolean # Run on PRs that modify docs (non-blocking) pull_request: @@ -57,6 +51,8 @@ jobs: uses: lycheeverse/lychee-action@v2 with: args: --verbose --no-progress ${{ steps.changed.outputs.files }} + fail: true + jobSummary: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -86,9 +82,10 @@ jobs: uses: lycheeverse/lychee-action@v2 with: args: --verbose . - fail: ${{ inputs.fail_on_errors == true }} + fail: true output: ./lychee-report.md format: markdown + jobSummary: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -98,41 +95,41 @@ jobs: path: .lycheecache key: lychee-cache-${{ github.run_id }} - - name: Find existing issue - if: steps.lychee.outputs.exit_code != 0 - id: find-issue - run: | - ISSUE_NUMBER=$(gh issue list \ - --state open \ - --search "Weekly External Link Check: Broken Links Found in:title" \ - --json number \ - --jq '.[0].number // ""') - echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create or update issue - if: steps.lychee.outputs.exit_code != 0 - run: | - # Check if report file exists - if [ ! -f ./lychee-report.md ]; then - echo "No report file found, skipping issue creation" - exit 0 - fi - - ISSUE_BODY=$(cat ./lychee-report.md) - if [ -n "${{ steps.find-issue.outputs.number }}" ]; then - gh issue edit "${{ steps.find-issue.outputs.number }}" \ - --body "$ISSUE_BODY" - echo "Updated issue #${{ steps.find-issue.outputs.number }}" - else - gh issue create \ - --title "Weekly External Link Check: Broken Links Found" \ - --body "$ISSUE_BODY" \ - --label "Bug" \ - --label "Team: Docs" \ - --label "Product Area: Docs" - echo "Created new issue" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # - name: Find existing issue + # if: steps.lychee.outputs.exit_code != 0 + # id: find-issue + # run: | + # ISSUE_NUMBER=$(gh issue list \ + # --state open \ + # --search "Weekly External Link Check: Broken Links Found in:title" \ + # --json number \ + # --jq '.[0].number // ""') + # echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT + # env: + # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Create or update issue + # if: steps.lychee.outputs.exit_code != 0 + # run: | + # # Check if report file exists + # if [ ! -f ./lychee-report.md ]; then + # echo "No report file found, skipping issue creation" + # exit 0 + # fi + + # ISSUE_BODY=$(cat ./lychee-report.md) + # if [ -n "${{ steps.find-issue.outputs.number }}" ]; then + # gh issue edit "${{ steps.find-issue.outputs.number }}" \ + # --body "$ISSUE_BODY" + # echo "Updated issue #${{ steps.find-issue.outputs.number }}" + # else + # gh issue create \ + # --title "Weekly External Link Check: Broken Links Found" \ + # --body "$ISSUE_BODY" \ + # --label "Bug" \ + # --label "Team: Docs" \ + # --label "Product Area: Docs" + # echo "Created new issue" + # fi + # env: + # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 3fb2cab3f4a9783646a2ac9af54c8e63da89b10b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 10:42:46 +0100 Subject: [PATCH 27/35] Enable failure on broken links in external link checker --- .github/workflows/lint-external-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index a8850eaf2d7d22..5306c85ab6ac68 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -82,9 +82,9 @@ jobs: uses: lycheeverse/lychee-action@v2 with: args: --verbose . - fail: true output: ./lychee-report.md format: markdown + fail: true jobSummary: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 1879d32c330cb2a8d8b84bb34ddb08d5986fe3d8 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 10:42:55 +0100 Subject: [PATCH 28/35] tmp --- .github/workflows/lint-external-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 5306c85ab6ac68..3cd0a20dec1de5 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -58,7 +58,7 @@ jobs: # Job for scheduled/manual runs: check all files, create issue check-full: - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + # if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: issues: write From e245022096007a56b029e0a9334c62ee8b1e50c4 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 10:58:20 +0100 Subject: [PATCH 29/35] save cache even on failure --- .github/workflows/lint-external-links.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 3cd0a20dec1de5..3956734f654bfe 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -91,6 +91,7 @@ jobs: - name: Save lychee cache uses: actions/cache/save@v4 + if: always() with: path: .lycheecache key: lychee-cache-${{ github.run_id }} From 876f2bf764927a110e7d7a4fcf9f5263c58b26e5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 11:03:57 +0100 Subject: [PATCH 30/35] config tuning --- lychee.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lychee.toml b/lychee.toml index 400fcd20daaaf1..6c5a7cda2d0bf0 100644 --- a/lychee.toml +++ b/lychee.toml @@ -17,7 +17,7 @@ exclude = ['^https://docs\.sentry\.io'] max_concurrency = 32 # Maximum number of retries per request -max_retries = 3 +max_retries = 2 # Request timeout in seconds timeout = 30 @@ -29,7 +29,7 @@ retry_wait_time = 2 user_agent = "Mozilla/5.0 (compatible; Sentry-Docs-Link-Checker; +https://github.com/getsentry/sentry-docs)" # Accept common status codes that indicate the link works -# Include 403 (bot blocking) and 418 (freedesktop teapot) to reduce noise +# Include 403 (possibly bot blocking) and 418 (freedesktop teapot) to reduce noise accept = [200, 201, 202, 203, 204, 206, 301, 302, 308, 403, 418] # Don't validate URL fragments/anchors (e.g., #section-name) From 0df8be42ae36c0cfb0f01c739f2884e0ac8ff3ee Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 11:05:55 +0100 Subject: [PATCH 31/35] cleanup --- .github/workflows/lint-external-links.yml | 39 ----------------------- 1 file changed, 39 deletions(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 3956734f654bfe..8aaec7bfa2fc9a 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -95,42 +95,3 @@ jobs: with: path: .lycheecache key: lychee-cache-${{ github.run_id }} - - # - name: Find existing issue - # if: steps.lychee.outputs.exit_code != 0 - # id: find-issue - # run: | - # ISSUE_NUMBER=$(gh issue list \ - # --state open \ - # --search "Weekly External Link Check: Broken Links Found in:title" \ - # --json number \ - # --jq '.[0].number // ""') - # echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - # env: - # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # - name: Create or update issue - # if: steps.lychee.outputs.exit_code != 0 - # run: | - # # Check if report file exists - # if [ ! -f ./lychee-report.md ]; then - # echo "No report file found, skipping issue creation" - # exit 0 - # fi - - # ISSUE_BODY=$(cat ./lychee-report.md) - # if [ -n "${{ steps.find-issue.outputs.number }}" ]; then - # gh issue edit "${{ steps.find-issue.outputs.number }}" \ - # --body "$ISSUE_BODY" - # echo "Updated issue #${{ steps.find-issue.outputs.number }}" - # else - # gh issue create \ - # --title "Weekly External Link Check: Broken Links Found" \ - # --body "$ISSUE_BODY" \ - # --label "Bug" \ - # --label "Team: Docs" \ - # --label "Product Area: Docs" - # echo "Created new issue" - # fi - # env: - # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5e23c12abbd0560e24d517addcc26182b5c3ccb8 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 11:50:08 +0100 Subject: [PATCH 32/35] disable temp full run --- .github/workflows/lint-external-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-external-links.yml b/.github/workflows/lint-external-links.yml index 8aaec7bfa2fc9a..5dfe3d75ebf11d 100644 --- a/.github/workflows/lint-external-links.yml +++ b/.github/workflows/lint-external-links.yml @@ -58,7 +58,7 @@ jobs: # Job for scheduled/manual runs: check all files, create issue check-full: - # if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: issues: write From 726582e484e522ade09d00062f18eab8e9052513 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 14:17:06 +0100 Subject: [PATCH 33/35] Include .mdx files in pre-commit link check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change from types_or: [markdown] to files pattern so both .md and .mdx files are checked locally, matching the CI workflow behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2b5d0ab03897c..214f4e5198a41e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,5 +40,5 @@ repos: name: Check external links (warn only) entry: bun scripts/lint-external-links.ts language: system - types_or: [markdown] + files: \.(md|mdx)$ verbose: true From 4e14ee7e258d05356510af2d298a6abaa057a2ac Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 2 Jan 2026 14:32:43 +0100 Subject: [PATCH 34/35] fix: update cache_exclude_status format in lychee.toml --- lychee.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lychee.toml b/lychee.toml index 6c5a7cda2d0bf0..e8691b8643e303 100644 --- a/lychee.toml +++ b/lychee.toml @@ -55,4 +55,4 @@ max_redirects = 10 # 2. New links not yet in cache cache = true max_cache_age = "335h" -cache_exclude_status = ["429", "500.."] +cache_exclude_status = "429, 500.." From 149dc210473f2ee0d383ddef5a83ffbc5e3e178d Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:33:35 +0100 Subject: [PATCH 35/35] Update README.md --- scripts/lint-404s/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md index b7e23c1aba2849..ed637b535ad49f 100644 --- a/scripts/lint-404s/README.md +++ b/scripts/lint-404s/README.md @@ -100,6 +100,5 @@ The GitHub workflow (`.github/workflows/lint-external-links.yml`) runs: ### Why Separate from Internal Link Checking? -1. **Performance**: External link checking is slower and shouldn't block PRs -2. **False positives**: Many external sites block automated checkers -3. **Different scope**: External checks only run on changed files in PRs; internal checks validate all pages +1. **False positives**: Many external sites block automated checkers +2. **Different scope**: External checks only run on changed files in PRs; internal checks validate all pages