Check Markdown Links workflow (jeremylongshore/claude-code-plugins-plus-skills)
The Check Markdown Links workflow from jeremylongshore/claude-code-plugins-plus-skills, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Check Markdown Links workflow from the jeremylongshore/claude-code-plugins-plus-skills repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Check Markdown Links
# Runs lychee against every .md file in the repo. Catches broken intra-repo
# relative links AND broken external links. Required on PRs touching .md;
# also runs weekly on a schedule so external-host rot gets surfaced.
#
# Gap this closes: prior to 2026-05-30 the repo had no broken-link checker.
# A link in issue #789 (and later in PR #805's pre-merge body) pointed at a
# gitignored file path; no CI gate caught it because no link checker existed.
# This workflow + .lychee.toml at the repo root close the repo-side half of
# that gap. The live-issue-body half is closed by check-issue-body-links.yml.
on:
pull_request:
paths:
- '**/*.md'
- '.lychee.toml'
- '.github/workflows/check-links.yml'
schedule:
# Mondays 05:00 UTC - catch external-host rot weekly.
- cron: '0 5 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore lychee cache
uses: actions/cache@v4
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}
restore-keys: cache-lychee-
# Two lychee invocations because the marketplace blog content uses
# root-relative URLs (`/posts/foo`, `/explore`) that Astro renders
# against https://tonsofskills.com at build time. Lychee needs --base
# for those to resolve as live URLs. Repo-side .md files (READMEs,
# 000-docs, skill bodies) don't use root-relative paths so they scan
# without a base.
- name: Run lychee (repo-side markdown)
id: lychee_repo
uses: lycheeverse/lychee-action@v2
with:
# lycheeVersion stays on the action's default (currently v0.23.0).
# Attempted v0.24.2 pin to fix the exclude_path regression but the
# action's tarball extraction doesn't handle v0.24's restructured
# archive (nested binary). Workaround: pass exclude paths via CLI
# --exclude-path flags, which v0.23.0 honors. The .lychee.toml
# exclude_path is kept as the source of truth for local runs and
# for whichever future action version handles v0.24+ packaging.
args: >-
--config .lychee.toml
--root-dir ${{ github.workspace }}
--no-progress
--cache
--max-cache-age 1d
--exclude-path 'marketplace/src/content'
--exclude-path 'plugins/skill-enhancers/axiom/docs'
--exclude-path 'tests/fixtures'
--exclude-path 'node_modules'
--exclude-path 'marketplace/dist'
--exclude-path 'marketplace/.astro'
--exclude-path 'marketplace/.cache'
--exclude-path 'marketplace/public/downloads'
--exclude-path 'plugins/[^/]+/dist'
--exclude-path 'plugins/[^/]+/node_modules'
--exclude-path 'freshie/cache'
--exclude-path '.audit-harness'
--exclude-path 'coverage'
--exclude-path '.pnpm-store'
'./**/*.md'
'./**/*.mdx'
# Never red-X a PR on link results - external-host flakiness (conn
# resets, rate-limits) produced ~200 false failures that read as
# repo noise. Link rot is surfaced by the weekly scheduled run +
# the report artifact instead, not by blocking contributor PRs.
fail: false
output: ./lychee-repo-report.md
- name: Run lychee (marketplace content with site base)
id: lychee_marketplace
uses: lycheeverse/lychee-action@v2
with:
args: >-
--config .lychee.toml
--base https://tonsofskills.com
--no-progress
--cache
--max-cache-age 1d
'marketplace/src/content/**/*.md'
'marketplace/src/content/**/*.mdx'
# See note above - never block a PR on link results.
fail: false
output: ./lychee-marketplace-report.md
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lychee-reports-${{ github.run_id }}
path: |
./lychee-repo-report.md
./lychee-marketplace-report.md
if-no-files-found: ignore
retention-days: 30
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Check Markdown Links # Runs lychee against every .md file in the repo. Catches broken intra-repo # relative links AND broken external links. Required on PRs touching .md; # also runs weekly on a schedule so external-host rot gets surfaced. # # Gap this closes: prior to 2026-05-30 the repo had no broken-link checker. # A link in issue #789 (and later in PR #805's pre-merge body) pointed at a # gitignored file path; no CI gate caught it because no link checker existed. # This workflow + .lychee.toml at the repo root close the repo-side half of # that gap. The live-issue-body half is closed by check-issue-body-links.yml. on: pull_request: paths: - '**/*.md' - '.lychee.toml' - '.github/workflows/check-links.yml' schedule: # Mondays 05:00 UTC - catch external-host rot weekly. - cron: '0 5 * * 1' workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: link-check: timeout-minutes: 30 runs-on: latchkey-small steps: - uses: actions/checkout@v4 - name: Restore lychee cache uses: actions/cache@v4 with: path: .lycheecache key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- # Two lychee invocations because the marketplace blog content uses # root-relative URLs (`/posts/foo`, `/explore`) that Astro renders # against https://tonsofskills.com at build time. Lychee needs --base # for those to resolve as live URLs. Repo-side .md files (READMEs, # 000-docs, skill bodies) don't use root-relative paths so they scan # without a base. - name: Run lychee (repo-side markdown) id: lychee_repo uses: lycheeverse/lychee-action@v2 with: # lycheeVersion stays on the action's default (currently v0.23.0). # Attempted v0.24.2 pin to fix the exclude_path regression but the # action's tarball extraction doesn't handle v0.24's restructured # archive (nested binary). Workaround: pass exclude paths via CLI # --exclude-path flags, which v0.23.0 honors. The .lychee.toml # exclude_path is kept as the source of truth for local runs and # for whichever future action version handles v0.24+ packaging. args: >- --config .lychee.toml --root-dir ${{ github.workspace }} --no-progress --cache --max-cache-age 1d --exclude-path 'marketplace/src/content' --exclude-path 'plugins/skill-enhancers/axiom/docs' --exclude-path 'tests/fixtures' --exclude-path 'node_modules' --exclude-path 'marketplace/dist' --exclude-path 'marketplace/.astro' --exclude-path 'marketplace/.cache' --exclude-path 'marketplace/public/downloads' --exclude-path 'plugins/[^/]+/dist' --exclude-path 'plugins/[^/]+/node_modules' --exclude-path 'freshie/cache' --exclude-path '.audit-harness' --exclude-path 'coverage' --exclude-path '.pnpm-store' './**/*.md' './**/*.mdx' # Never red-X a PR on link results - external-host flakiness (conn # resets, rate-limits) produced ~200 false failures that read as # repo noise. Link rot is surfaced by the weekly scheduled run + # the report artifact instead, not by blocking contributor PRs. fail: false output: ./lychee-repo-report.md - name: Run lychee (marketplace content with site base) id: lychee_marketplace uses: lycheeverse/lychee-action@v2 with: args: >- --config .lychee.toml --base https://tonsofskills.com --no-progress --cache --max-cache-age 1d 'marketplace/src/content/**/*.md' 'marketplace/src/content/**/*.mdx' # See note above - never block a PR on link results. fail: false output: ./lychee-marketplace-report.md - name: Upload reports if: always() uses: actions/upload-artifact@v4 with: name: lychee-reports-${{ github.run_id }} path: | ./lychee-repo-report.md ./lychee-marketplace-report.md if-no-files-found: ignore retention-days: 30
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.