broken-links #30
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: broken-links | |
| on: | |
| schedule: | |
| # Nightly at 09:00 UTC (around 03:00 MT / 04:00 CT). | |
| - cron: '0 9 * * *' | |
| workflow_dispatch: {} | |
| permissions: | |
| issues: write | |
| contents: read | |
| jobs: | |
| crawl: | |
| name: Crawl site for broken links | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Pick target URL | |
| id: target | |
| # We feed Lychee the sitemap rather than the homepage. Lychee's | |
| # CLI doesn't support recursive crawling — it checks every link | |
| # on every input. Pointing it at /sitemap.xml gives it the full | |
| # URL list (33 static pages + 162 blog posts) and it then checks | |
| # every link on every one of those pages. That's the "test all | |
| # internal pages too" coverage the homepage-only run lacked. | |
| run: | | |
| if [ -n "${{ secrets.STAGING_URL }}" ]; then | |
| base="${{ secrets.STAGING_URL }}" | |
| else | |
| base="https://beta.ontargetaba.com/" | |
| fi | |
| # Trim trailing slash, append sitemap.xml | |
| base="${base%/}" | |
| echo "url=${base}/sitemap.xml" >> "$GITHUB_OUTPUT" | |
| - name: Run Lychee | |
| id: lychee | |
| uses: lycheeverse/lychee-action@v2 | |
| with: | |
| args: >- | |
| --verbose | |
| --no-progress | |
| --max-retries 2 | |
| --retry-wait-time 5 | |
| --timeout 20 | |
| --accept 200,206,429 | |
| --include-verbatim | |
| --exclude '^https://ontargetaba\.com/' | |
| --exclude '^https://static\.hotjar\.com/c/hotjar-$' | |
| --exclude 'search_term_string' | |
| --exclude '^https://(www\.)?linkedin\.com/' | |
| --exclude '^https://(www\.)?instagram\.com/' | |
| --exclude '^https://(www\.)?facebook\.com/' | |
| ${{ steps.target.outputs.url }} | |
| fail: false | |
| output: ./lychee-report.md | |
| - name: Verify Lychee actually ran | |
| # `fail: false` above lets the step continue when broken links | |
| # are found, but it also masks the case where Lychee itself | |
| # blew up (bad CLI args, sitemap unreachable, etc.) — in which | |
| # case no report is produced and we'd silently open a misleading | |
| # tracking issue. Treat "no report or empty report" as a hard | |
| # workflow failure so red CI flags the real problem. | |
| run: | | |
| if [ ! -s ./lychee-report.md ]; then | |
| echo "::error::Lychee produced no report — it likely failed to run. Check the 'Run Lychee' step logs above for CLI arg errors or network failures." | |
| exit 1 | |
| fi | |
| - name: Find existing tracking issue | |
| id: existing | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| num=$(gh issue list --state open --label broken-links \ | |
| --search "Broken links found" \ | |
| --json number --jq '.[0].number // empty') | |
| echo "number=$num" >> "$GITHUB_OUTPUT" | |
| - name: Open or update tracking issue | |
| if: steps.lychee.outputs.exit_code != '0' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TARGET_URL: ${{ steps.target.outputs.url }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| date_str=$(date -u +%Y-%m-%d) | |
| title="Broken links found ${date_str}" | |
| # Build the body in a file. We previously stitched it with | |
| # bash $(...) + +=, but command substitution strips trailing | |
| # newlines, so when lychee-report.md didn't end with one the | |
| # closing ``` ended up smushed against the last content line. | |
| # Writing through printf to a real file gives us deterministic | |
| # line endings — closing fence is always on its own line. | |
| { | |
| printf '## Broken links\n\n' | |
| printf 'Target: %s\n' "$TARGET_URL" | |
| printf 'Run: %s\n\n' "$RUN_URL" | |
| printf '```\n' | |
| if [ -s ./lychee-report.md ]; then | |
| cat ./lychee-report.md | |
| else | |
| printf 'See run logs for details.\n' | |
| fi | |
| # Force a final newline before the closing fence regardless | |
| # of whether the report ended with one. | |
| printf '\n```\n' | |
| } > /tmp/issue-body.md | |
| if [ -n "${{ steps.existing.outputs.number }}" ]; then | |
| gh issue edit "${{ steps.existing.outputs.number }}" \ | |
| --title "$title" \ | |
| --body-file /tmp/issue-body.md | |
| else | |
| gh issue create \ | |
| --title "$title" \ | |
| --body-file /tmp/issue-body.md \ | |
| --label broken-links | |
| fi | |
| - name: Close tracking issue when clean | |
| if: steps.lychee.outputs.exit_code == '0' && steps.existing.outputs.number != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh issue close "${{ steps.existing.outputs.number }}" \ | |
| --comment "All previously broken links are now resolving. Closing." |