diff --git a/.github/lychee.toml b/.github/lychee.toml new file mode 100644 index 0000000..0f923d2 --- /dev/null +++ b/.github/lychee.toml @@ -0,0 +1,32 @@ +# Shared lychee configuration. +# +# Used by: +# .github/workflows/validate.yml (--offline, blocks PRs) +# .github/workflows/external-reference-check.yml (online, informational + scheduled) +# +# Both workflows pass `--config .github/lychee.toml` explicitly because lychee +# only auto-discovers `lychee.toml` in the current working directory. + +# Cache successful checks across the run so we don't hammer the same host. +cache = true +max_cache_age = "1d" + +# Be polite when retrying transient failures. +max_retries = 2 +retry_wait_time = 2 + +# A 429 means the host exists but rate-limited us; treat as success rather +# than as a broken reference. +accept = ["200..=206", "429"] + +# URL regex patterns to skip. Use this for hosts that are flaky, auth-gated, +# or routinely block CI runners. Prefer narrow patterns over broad ones so +# real link rot keeps getting caught. +# +# Examples: +# '^https://intranet\.example\.com/', +# '^https://github\.com/[^/]+/[^/]+/pull/', +exclude = [ + # openai.com blocks GitHub-hosted runner IPs with 403; the page is fine + '^https://openai\.com/codex/?$', +] diff --git a/.github/workflows/external-reference-check.yml b/.github/workflows/external-reference-check.yml new file mode 100644 index 0000000..18a0713 --- /dev/null +++ b/.github/workflows/external-reference-check.yml @@ -0,0 +1,93 @@ +name: external-reference-check + +# Reach out to every external URL referenced from our markdown and report +# anything that's unreachable. This is intentionally split from `validate` +# because external checks are flaky (transient 5xx, rate limits, runner IP +# blocks, DNS hiccups). The workflow fails on PR and manual runs so broken +# links are visible; whether they actually block merge is controlled by +# whether this check is marked required in branch protection (recommended: +# leave it not-required so a transient hiccup can't block a merge). +# +# Internal references and anchors are checked by `validate.yml` with +# `lychee --offline`. + +on: + pull_request: + paths: + - "**/*.md" + - ".github/lychee.toml" + - ".github/workflows/external-reference-check.yml" + schedule: + # Mondays at 12:00 UTC. Catches link rot in unchanged content. + - cron: "0 12 * * 1" + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + external-references: + name: Check external references in skills + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Check external references + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + args: --config .github/lychee.toml --no-progress "./**/*.md" + # Fail the workflow on PR and manual runs so broken external + # references show up as a red check (mark this workflow as + # not-required in branch protection if you don't want it to + # block merges). + # + # On the scheduled cron run we deliberately do NOT fail here, + # so the issue-filing step below can still execute. + fail: ${{ github.event_name != 'schedule' }} + + # Scheduled runs file (or update) an issue when something is rotten, + # so unchanged-but-broken links don't silently sit in main. + - name: File or update link-rot issue on scheduled failure + if: github.event_name == 'schedule' && steps.lychee.outputs.exit_code != 0 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + RUN_ID: ${{ github.run_id }} + run: | + set -euo pipefail + + # Create the label on first use; ignore if it already exists. + gh label create external-reference-rot \ + --color ffaa00 \ + --description "Unreachable external references found by scheduled lychee run" \ + 2>/dev/null || true + + run_url="${SERVER_URL}/${REPO}/actions/runs/${RUN_ID}" + + if [ ! -s ./lychee/out.md ]; then + echo "lychee report file is missing or empty; nothing to file." >&2 + exit 0 + fi + + existing=$(gh issue list \ + --label external-reference-rot \ + --state open \ + --json number \ + --jq '.[0].number // empty') + + if [ -n "$existing" ]; then + echo "Updating existing issue #$existing" + gh issue edit "$existing" --body-file ./lychee/out.md + gh issue comment "$existing" \ + --body "Refreshed by scheduled run: $run_url" + else + echo "Filing new issue" + gh issue create \ + --title "External reference rot detected" \ + --label external-reference-rot \ + --body-file ./lychee/out.md + fi diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fd6aef7..8cca0fa 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -6,11 +6,12 @@ on: pull_request: paths: - "scripts/**" - - "**/SKILL.md" + - "**/*.md" - "skills/**" - ".claude-plugin/**" - ".cursor-plugin/**" - ".github/workflows/validate.yml" + - ".github/lychee.toml" workflow_dispatch: jobs: @@ -26,3 +27,13 @@ jobs: - name: Validate skills and generated manifests run: ./scripts/check.sh + + # Deterministic, offline-only reference check: relative paths and + # heading anchors. External URLs are intentionally not checked here + # because their failure modes are flaky; see + # `.github/workflows/external-reference-check.yml` for that. + - name: Check internal references and anchors + uses: lycheeverse/lychee-action@v2 + with: + args: --config .github/lychee.toml --offline --include-fragments --no-progress "./**/*.md" + fail: true diff --git a/README.md b/README.md index dd5823b..3c06e79 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Cursor](https://img.shields.io/badge/Cursor-Compatible-000000?logo=cursor&logoColor=white)](https://cursor.com) [![Claude Code](https://img.shields.io/badge/Claude_Code-Compatible-F07535?logo=claude&logoColor=white)](https://www.anthropic.com/claude-code) [![OpenAI Codex](https://img.shields.io/badge/OpenAI_Codex-Compatible-412991?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0yMi4yODIgOS44MjFhNS45ODUgNS45ODUgMCAwIDAtLjUxNi00LjkxIDYuMDQ2IDYuMDQ2IDAgMCAwLTYuNTEtMi45QTYuMDY1IDYuMDY1IDAgMCAwIDQuOTgxIDQuMThhNS45ODUgNS45ODUgMCAwIDAtMy45OTggMi45IDYuMDQ2IDYuMDQ2IDAgMCAwIC43NDMgNy4wOTcgNS45OCA1Ljk4IDAgMCAwIC41MSA0LjkxMSA2LjA1MSA2LjA1MSAwIDAgMCA2LjUxNSAyLjlBNS45ODUgNS45ODUgMCAwIDAgMTMuMjYgMjRhNi4wNTYgNi4wNTYgMCAwIDAgNS43NzItNC4yMDUgNS45OSA1Ljk5IDAgMCAwIDMuOTk3LTIuOSA2LjA1NiA2LjA1NiAwIDAgMC0uNzQ3LTcuMDc0ek0xMy4yNiAyMi40M2E0LjQ3NiA0LjQ3NiAwIDAgMS0yLjg3Ni0xLjA0bC4xNDEtLjA4MSA0Ljc3OS0yLjc1OGEuNzk1Ljc5NSAwIDAgMCAuMzkyLS42ODF2LTYuNzM3bDIuMDIgMS4xNjhhLjA3MS4wNzEgMCAwIDEgLjAzOC4wNTJ2NS41ODNhNC41MDQgNC41MDQgMCAwIDEtNC40OTQgNC40OTR6TTMuNiAxOC4zMDRhNC40NyA0LjQ3IDAgMCAxLS41MzUtMy4wMTRsLjE0Mi4wODUgNC43ODMgMi43NTlhLjc3MS43NzEgMCAwIDAgLjc4IDBsNS44NDMtMy4zNjl2Mi4zMzJhLjA4LjA4IDAgMCAxLS4wMzMuMDYyTDkuNzQgMTkuOTVhNC41IDQuNSAwIDAgMS02LjE0LTEuNjQ2ek0yLjM0IDcuODk2YTQuNDg1IDQuNDg1IDAgMCAxIDIuMzY2LTEuOTczVjExLjZhLjc2Ni43NjYgMCAwIDAgLjM4OC42NzdsNS44MTUgMy4zNTUtMi4wMiAxLjE2OGEuMDc2LjA3NiAwIDAgMS0uMDcxIDBsLTQuODMtMi43ODZBNC41MDQgNC41MDQgMCAwIDEgMi4zNCA3Ljg3MnptMTYuNTk3IDMuODU1bC01LjgzMy0zLjM4N0wxNS4xMTkgNy4yYS4wNzYuMDc2IDAgMCAxIC4wNzEgMGw0LjgzIDIuNzkxYTQuNDk0IDQuNDk0IDAgMCAxLS42NzYgOC4xMDV2LTUuNjc4YS43OS43OSAwIDAgMC0uNDA3LS42Njd6bTIuMDEtMy4wMjNsLS4xNDEtLjA4NS00Ljc3NC0yLjc4MmEuNzc2Ljc3NiAwIDAgMC0uNzg1IDBMOS40MDkgOS4yM1Y2Ljg5N2EuMDY2LjA2NiAwIDAgMSAuMDI4LS4wNjFsNC44My0yLjc4N2E0LjUgNC41IDAgMCAxIDYuNjggNC42NnptLTEyLjY0IDQuMTM1bC0yLjAyLTEuMTY0YS4wOC4wOCAwIDAgMS0uMDM4LS4wNTdWNi4wNzVhNC41IDQuNSAwIDAgMSA3LjM3NS0zLjQ1M2wtLjE0Mi4wOC00Ljc3OCAyLjc1OGEuNzk1Ljc5NSAwIDAgMC0uMzkzLjY4MXptMS4wOTctMi4zNjVsMi42MDItMS41IDIuNjA3IDEuNXYyLjk5OWwtMi41OTcgMS41LTIuNjA3LTEuNXoiLz48L3N2Zz4=)](https://openai.com/codex/) -[![Gemini CLI](https://img.shields.io/badge/Gemini_CLI-Compatible-4285F4?logo=googlegemini&logoColor=white)](https://ai.google.dev/gemini-api/docs/cli) +[![Gemini CLI](https://img.shields.io/badge/Gemini_CLI-Compatible-4285F4?logo=googlegemini&logoColor=white)](https://ai.google.dev/gemini-api/docs) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) AMD Skills