Skip to main content

GitHub Actions Link Checker Guide

Purpose

This guide explains how to set up automated broken-link detection for markdown documentation repositories using GitHub Actions. When enabled, every push and pull request is checked for broken internal links (cross-references between docs) and optionally broken external URLs.

Why this matters: Documentation repos rely on relative markdown links ([Pipeline SOP](sales-pipeline/ghl-prospecting-pipeline-sop.md)). When files are moved, renamed, or archived, those links break silently. A CI link checker catches them before they reach readers.


markdown-link-check is a Node.js tool that parses markdown files and verifies every link resolves. The GitHub Action wrapper (gaurav-nelson/github-action-markdown-link-check) runs it on every push/PR with zero configuration required.

Why This Tool

Criteriamarkdown-link-checkAlternatives
Markdown-nativeYes -- understands [text](url) and reference linkslychee (Rust, faster but less markdown-aware)
Relative link supportYes -- resolves paths relative to the fileMost alternatives also support this
Config file.markdown-link-check.json in repo rootVaries
Free for public/private reposYes (GitHub Actions minutes apply)Same
Active maintenanceYeslychee is also actively maintained

Setup

Step 1: Create the Workflow File

Create .github/workflows/check-links.yml:

name: Check Markdown Links

on:
push:
branches: [master]
paths: ['**/*.md']
pull_request:
branches: [master]
paths: ['**/*.md']

jobs:
check-links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'yes'
use-verbose-mode: 'yes'
config-file: '.markdown-link-check.json'

Key settings:

  • paths: ['**/*.md'] -- only runs when markdown files change (saves CI minutes)
  • use-quiet-mode -- suppresses passing links, only shows failures
  • use-verbose-mode -- shows which file each failure came from

Step 2: Create the Configuration File

Create .markdown-link-check.json in the repo root:

{
"ignorePatterns": [
{ "pattern": "^https://app.symphonycore.com" },
{ "pattern": "^https://storage.googleapis.com" },
{ "pattern": "^mailto:" }
],
"replacementPatterns": [],
"httpHeaders": [],
"timeout": "10s",
"retryOn429": true,
"retryCount": 3,
"fallbackRetryDelay": "5s",
"aliveStatusCodes": [200, 206]
}

Configuration explained:

SettingPurpose
ignorePatternsSkip URLs that are known-good or require auth to resolve
timeoutHow long to wait for external URLs (10s is reasonable)
retryOn429Retry when rate-limited (common with GitHub, LinkedIn, etc.)
retryCountNumber of retries before marking a link as dead
aliveStatusCodesHTTP status codes that count as "alive"

Step 3: Tune Ignore Patterns

Add patterns for URLs that will always fail in CI (require login, block bots, or are dynamically rendered):

{
"ignorePatterns": [
{ "pattern": "^https://app.symphonycore.com" },
{ "pattern": "^https://storage.googleapis.com" },
{ "pattern": "^https://app.gohighlevel.com" },
{ "pattern": "^https://www.linkedin.com" },
{ "pattern": "^https://symphonycore.com" },
{ "pattern": "^mailto:" },
{ "pattern": "^#" }
]
}

Why these are ignored:

  • app.symphonycore.com / app.gohighlevel.com -- require GHL auth
  • linkedin.com -- blocks automated requests (returns 999)
  • symphonycore.com -- JS-rendered, returns 200 but link checker can't verify anchors
  • storage.googleapis.com -- signed URLs or CDN assets, may require auth
  • mailto: -- not HTTP links
  • # -- in-page anchors (these are checked by default but can produce false positives with heading ID mismatches)

What It Catches

ScenarioDetected?Example
Renamed file, old link remainsYes[SOP](old-name.md) when file is now new-name.md
Moved file to different directoryYes[SOP](/sops/file) when file moved to sales-pipeline/
Deleted/archived fileYesLink to a file that no longer exists in the repo
Typo in relative pathYes[SOP](sales-pipline/file.md) (misspelled directory)
Broken external URL (404)YesLink to a webpage that's been taken down
External URL behind authNoCovered by ignorePatterns
Anchor link to wrong headingPartialDetects missing files but not all heading-ID mismatches

Reading the Output

Passing Run

FILE: 04-operations/sops/sales-pipeline/prospect-email-discovery-sop.md
[OK] ../ghl-prospecting-audit-accuracy-sop.md
[OK] ghl-prospecting-pipeline-sop.md
[OK] ../../06-team-training/platform-training/kb-015-ghl-prospecting-tool.md

7 links checked. 0 broken.

Failing Run

FILE: 04-operations/sops/sales-pipeline/prospect-email-discovery-sop.md
[OK] ../ghl-prospecting-audit-accuracy-sop.md
[FAIL] ghl-prospecting-pipline-sop.md --> Status: 400
[OK] ../../06-team-training/platform-training/kb-015-ghl-prospecting-tool.md

7 links checked. 1 broken.
ERROR: 1 dead link found!

The [FAIL] line shows the broken path. Fix the link in the source file, commit, and push.


Running Locally

To check links without pushing to GitHub:

# Install globally
npm install -g markdown-link-check

# Check a single file
markdown-link-check 04-operations/sops/sales-pipeline/prospect-email-discovery-sop.md

# Check all markdown files in the repo
find . -name '*.md' -not -path './node_modules/*' -exec markdown-link-check --config .markdown-link-check.json {} \;

For faster local checks, lychee is a Rust alternative:

# Install (Windows via scoop or cargo)
scoop install lychee
# or
cargo install lychee

# Check all markdown files
lychee "**/*.md" --exclude-path node_modules

Maintenance

When to Update ignore Patterns

  • When you add a new external service URL that blocks bots (add to ignorePatterns)
  • When a previously-ignored service becomes publicly accessible (remove from ignorePatterns)
  • When false positives appear in CI for specific URLs

When to Update the Workflow

  • When the default branch changes (update branches: [master])
  • When you want to add scheduled checks (e.g., weekly external URL validation):
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9:00 UTC

Cost

FactorImpact
GitHub Actions minutes~30-60 seconds per run for a repo this size
Free tier (public repos)Unlimited
Free tier (private repos)2,000 minutes/month (this uses < 1% of that)
External request rate limitsMitigated by retryOn429 and ignorePatterns


Document History

VersionDateChanges
1.02026-03-02Initial version -- setup, configuration, ignore patterns, local usage

Document Information

  • Version: 1.0
  • Created: 2026-03-02
  • Updated: 2026-03-02
  • Location: 11-engineering/github-actions-link-checker-guide.md