fix(links): repair rotted citations and harden the daily link sweep #102
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Links | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| - cron: "0 6 * * *" # Daily at 06:00 UTC — catches rotted external links | |
| workflow_dispatch: # manual trigger for the external-link sweep | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Blocking: broken internal links are a content bug we can always fix. | |
| # Crawls the built site served locally so directory URLs (/spec/x/) resolve | |
| # to their index.html exactly as in production. External links are skipped. | |
| internal: | |
| name: Internal links | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build | |
| run: npm run build | |
| env: | |
| NODE_ENV: production | |
| - name: Check internal links | |
| run: | | |
| npx astro preview --port 4321 & | |
| up= | |
| for i in $(seq 1 30); do | |
| curl -sf http://localhost:4321/ >/dev/null && { up=1; break; } | |
| sleep 1 | |
| done | |
| [ -n "$up" ] || { echo "::error::preview server did not start"; exit 1; } | |
| npx --yes linkinator@6 http://localhost:4321 \ | |
| --recurse --skip "^https?://(?!localhost)" | |
| # Blocking: runs daily and on demand. A failed run signals rotted external | |
| # links that need fixing. Kept off PRs so a flaky upstream (IETF / W3C / MDN | |
| # rate-limiting, upstream outages) can't block merges — only the daily sweep. | |
| # | |
| # Noise control lives in linkinator.config.json: | |
| # - retry / retryErrors: re-attempt 429s (honouring retry-after) and 5xx, | |
| # so transient rate-limits and blips don't fail the run. | |
| # - concurrency 25: gentler crawl, fewer self-inflicted 429s. | |
| # - skip[]: hosts that hard-block any headless checker and can never pass — | |
| # w3.org/validator.w3.org/securityheaders.com sit behind a Cloudflare JS | |
| # challenge (403 "Just a moment…"); developers.facebook.com 400s bots; | |
| # github.com /edit/ links and our own jdevalk/specification.website repo | |
| # self-links are per-page chrome that 429 under GitHub's burst limit (not | |
| # citations — third-party github repos stay checked); the a2a endpoint is | |
| # POST-only (405); | |
| # developer.android.com connection-resets headless clients ([0]); and | |
| # example.com is the reserved illustration domain used in prose samples. | |
| # These are excluded so a red run means real rot, not bot-blocking. Genuine | |
| # 404s on these hosts won't be auto-caught — verify them by hand when citing. | |
| external: | |
| name: External links | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build | |
| run: npm run build | |
| env: | |
| NODE_ENV: production | |
| - name: Check all links (internal + external) | |
| run: | | |
| npx astro preview --port 4321 & | |
| up= | |
| for i in $(seq 1 30); do | |
| curl -sf http://localhost:4321/ >/dev/null && { up=1; break; } | |
| sleep 1 | |
| done | |
| [ -n "$up" ] || { echo "::error::preview server did not start"; exit 1; } | |
| npx --yes linkinator@6 http://localhost:4321 \ | |
| --config linkinator.config.json |