Skip to content

Commit 88b1aa0

Browse files
sharadregoticlaude
andauthored
DX-2361: Add GitHub Actions to validate external URLs and internal anchor fragments (#1865)
* DX-2361: Add GitHub Actions to validate external URLs and internal anchors - Extend validate_mintlify_docs.py with --check-anchors flag that extracts GFM-slugified heading anchors, {#custom-id} syntax, and <a id/name> elements from target MDX files and verifies every internal #fragment resolves - Update validate-docs.yml to run anchor check on every PR - Add check-external-links.yml workflow: weekly scheduled (Mon 07:00 UTC) and manual-trigger run that HEAD-checks all external HTTP/HTTPS URLs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * DX-2361: Run external link check on every pull request Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * DX-2361: Fix exit code not failing on broken external links External link failures were reported but never factored into the exit code, so CI was passing despite 404s. Added has_broken_external check and a summary line for external link results. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * DX-2361: Switch external link checker to lychee Replace sequential Python/requests approach with lycheeverse/lychee-action. Lychee runs checks in parallel, has built-in retry logic, and is purpose-built for link checking — much faster on a repo with 1500+ external URLs. Add lychee.toml to exclude tyk-owned domains, localhost, placeholder URLs, and sites known to block bots (LinkedIn, Facebook). Accept 429 as non-broken to handle rate-limited responses gracefully. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix mirror-pr workflow failing on PR bodies with special characters GitHub Actions expands ${{ }} expressions before the shell runs, so backticks and $ signs in PR bodies were injected raw into the script and interpreted as shell command substitution. Fix: write static content via a single-quoted heredoc (no shell expansion), then append the PR body via printf with an env var ($PR_BODY). Shell variables accessed as "$VAR" are never re-interpreted, making any PR body content safe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix lychee checking internal/relative links Add scheme = ["https", "http"] to lychee.toml so lychee only attempts to check http/https URLs. Without this, lychee tries to resolve root-relative paths (/img/..., /page/...) as file URIs and fails. Internal links are already validated by validate-docs.yml. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix lychee erroring on root-relative paths scheme = ["https", "http"] didn't help because lychee fails to build the URL before the scheme filter runs. Set base = "https://tyk.io" so root-relative paths (/img/..., /page/...) are resolved to https://tyk.io/... and then silently skipped by the existing tyk.io exclude rule, rather than producing "cannot convert path to URI" errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix lychee config: base -> base_url Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7de8894 commit 88b1aa0

5 files changed

Lines changed: 410 additions & 108 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Check External Links
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
external-links:
12+
name: Check External URLs
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
18+
19+
- name: Check external links
20+
uses: lycheeverse/lychee-action@v2
21+
with:
22+
args: --config lychee.toml '**/*.md' '**/*.mdx'
23+
fail: true

.github/workflows/mirror-pr-to-build-deploy.yml

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,28 @@ jobs:
3232
# Create new mirror PR
3333
echo "Creating new mirror PR..."
3434
35-
# Create PR body content using printf to avoid shell parsing issues
36-
printf '%s\n' \
37-
"**🔗 Auto-generated mirror PR for Mintlify preview**" \
38-
"" \
39-
"**Original PR:** #${{ github.event.number }}" \
40-
"**Author:** @${{ github.event.pull_request.user.login }}" \
41-
"" \
42-
"## Purpose" \
43-
"This PR provides a Mintlify preview link for reviewing documentation changes." \
44-
"" \
45-
"## Preview Link" \
46-
"The Mintlify preview will be available once this PR is processed." \
47-
"" \
48-
"## ⚠️ Important Notes" \
49-
"- **Do not merge this PR directly**" \
50-
"- This PR will be auto-merged when the original PR #${{ github.event.number }} is merged" \
51-
"- Make all comments and reviews on the original PR #${{ github.event.number }}" \
52-
"" \
53-
"## Changes" \
54-
"${{ github.event.pull_request.body }}" > pr_body.txt
35+
# Write PR body to file via env var to safely handle special characters
36+
# (backticks, $, quotes) without shell re-interpretation
37+
cat > pr_body.txt << 'ENDBODY'
38+
**🔗 Auto-generated mirror PR for Mintlify preview**
39+
40+
**Original PR:** #${{ github.event.number }}
41+
**Author:** @${{ github.event.pull_request.user.login }}
42+
43+
## Purpose
44+
This PR provides a Mintlify preview link for reviewing documentation changes.
45+
46+
## Preview Link
47+
The Mintlify preview will be available once this PR is processed.
48+
49+
## ⚠️ Important Notes
50+
- **Do not merge this PR directly**
51+
- This PR will be auto-merged when the original PR #${{ github.event.number }} is merged
52+
- Make all comments and reviews on the original PR #${{ github.event.number }}
53+
54+
## Changes
55+
ENDBODY
56+
printf '%s\n' "$PR_BODY" >> pr_body.txt
5557

5658
# Escape the title properly to handle special characters and spaces
5759
ESCAPED_TITLE=$(printf '%s' "🔄 Preview: ${{ github.event.pull_request.title }}" | sed 's/"/\\"/g')
@@ -69,6 +71,7 @@ jobs:
6971
fi
7072
env:
7173
GH_TOKEN: ${{ secrets.ORG_GH_TOKEN }}
74+
PR_BODY: ${{ github.event.pull_request.body }}
7275

7376
cleanup-mirror-pr:
7477
runs-on: ubuntu-latest

.github/workflows/validate-docs.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ jobs:
2828
run: |
2929
echo "🔍 Running documentation validation..."
3030
python scripts/validate_mintlify_docs.py . --validate-redirects --verbose
31-
31+
32+
- name: Check anchor fragments
33+
run: |
34+
echo "⚓ Checking internal anchor fragments..."
35+
python scripts/validate_mintlify_docs.py . --check-anchors --links-only
36+
3237
- name: Validation complete
3338
if: success()
3439
run: |
3540
echo "✅ Documentation validation passed!"
36-
echo "All links, images, navigation, and redirects are valid."
41+
echo "All links, images, navigation, redirects, and anchors are valid."

lychee.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Lychee external link checker configuration
2+
# Docs: https://lychee.cli.rs/usage/configuration/
3+
4+
# Only check http/https URLs — skips mailto:, file://, etc.
5+
scheme = ["https", "http"]
6+
7+
# Resolve root-relative links (e.g. /img/..., /page/...) against the live docs
8+
# base URL. Combined with the tyk.io exclude rule below, these get silently
9+
# skipped rather than erroring as "cannot convert path to URI".
10+
base_url = "https://tyk.io"
11+
12+
# Only check external URLs — internal links are validated by validate-docs.yml
13+
exclude = [
14+
# Tyk-owned domains (covers root-relative paths resolved via base above)
15+
"https?://(.*\\.)?tyk\\.io",
16+
"https?://(.*\\.)?tyktech\\.net",
17+
18+
# Localhost / private addresses
19+
"https?://localhost",
20+
"https?://127\\.0\\.0\\.1",
21+
22+
# Placeholder / example URLs
23+
"https?://example\\.com",
24+
"https?://your-",
25+
"https?://<",
26+
27+
# Known to block bots / return false 403s
28+
"https?://(www\\.)?linkedin\\.com",
29+
"https?://(www\\.)?facebook\\.com",
30+
]
31+
32+
# Treat these status codes as success (429 = rate-limited but link exists)
33+
accept = [200, 201, 202, 203, 204, 206, 301, 302, 303, 307, 308, 429]
34+
35+
# Retry up to 3 times before marking a link as broken
36+
max_retries = 3
37+
38+
# Timeout per request in seconds
39+
timeout = 15
40+
41+
# Number of concurrent requests
42+
max_concurrency = 8

0 commit comments

Comments
 (0)