Skip to content
Latchkey

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.

C

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.

Grade your own workflow free or run it on Latchkey →
Source: jeremylongshore/claude-code-plugins-plus-skills.github/workflows/check-links.ymlLicense MITView source

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

workflow (.yml)
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

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.

Actions used in this workflow