mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-18 12:08:35 +00:00
b1cfe6edb1
The docs have no link checking at all, so external links rot silently: a trial run already found `docs/src/python/python.md` pointing at `lancedb.github.io/lance-namespace`, which returns 404 since the repository moved to the lance-format org. Checking external links on the blocking path would be the wrong trade: third-party hosts rate-limit automated clients, reject non-browser user agents, and go down temporarily, so any of them having a bad minute would turn unrelated PRs red. Following lance-format/lance#8315, this adds a daily `lychee` run that reports broken links into a single tracking issue, rewritten in place on each run and closed automatically once every link resolves. The scan job runs the downloaded lychee binary with a read-only token; everything that writes lives in a separate report job, and a non-verdict lychee exit fails the run instead of publishing a bogus report. The check is restricted to http(s) links because much of `docs/src` is generated API reference (the `js/` tree comes from `npm run docs`) and the hand-written pages use mkdocstrings cross-references and nav-relative paths that only resolve in the site mkdocs builds, so relative links would be reported as broken on every run. The one broken link the trial run surfaced is fixed here; after the fix, a local run over all 154 files reports 0 errors across 216 unique links.
223 lines
9.9 KiB
YAML
223 lines
9.9 KiB
YAML
name: Check doc links
|
|
|
|
# Checking external links is inherently noisy: third-party sites rate-limit
|
|
# automated clients, reject non-browser user agents, and go down temporarily.
|
|
# Blocking pull requests on that trades a lot of false failures for very little
|
|
# signal, so this runs on a schedule and reports findings in a single tracking
|
|
# issue instead of failing anyone's build.
|
|
on:
|
|
schedule:
|
|
- cron: "0 7 * * *"
|
|
workflow_dispatch:
|
|
|
|
# The report lives in one repository-global issue, so runs must not overlap: a
|
|
# lookup racing a create produces duplicate issues, and a healthy run closing
|
|
# the issue while a failing run only rewrites its body would leave a broken
|
|
# report closed. The group is deliberately ref-independent so that a manual
|
|
# dispatch serializes against the scheduled run.
|
|
concurrency:
|
|
group: docs-link-check
|
|
cancel-in-progress: false
|
|
|
|
permissions: {}
|
|
|
|
env:
|
|
REPORT_TITLE: "Docs link checker report"
|
|
|
|
jobs:
|
|
scan:
|
|
name: Scan links
|
|
runs-on: ubuntu-24.04
|
|
# lychee-action is pinned by SHA, but its wrapper downloads the lychee
|
|
# release tarball at run time without verifying a digest, and hands the
|
|
# resulting binary a GitHub token. Release assets remain replaceable, so
|
|
# that binary is confined to a job whose token can only read public
|
|
# content; everything that writes runs in the report job below.
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
exit_code: ${{ steps.lychee.outputs.exit_code }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# workflow_dispatch can run from any ref, but the report is
|
|
# repository-global. Always measure the default branch so a manual
|
|
# run from a topic branch cannot close a report that main warrants,
|
|
# or overwrite it with branch-only findings.
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
persist-credentials: false
|
|
|
|
- name: Check links
|
|
id: lychee
|
|
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
|
|
with:
|
|
# Restricted to http(s) on purpose. Much of docs/src is generated
|
|
# API reference (the js/ tree comes from `npm run docs` in nodejs)
|
|
# and the hand-written pages use mkdocstrings cross-references and
|
|
# nav-relative paths that only resolve in the site mkdocs builds,
|
|
# not in this checkout, so relative links would be reported as
|
|
# broken on every run.
|
|
args: >-
|
|
--scheme https
|
|
--scheme http
|
|
--no-progress
|
|
--max-retries 3
|
|
--timeout 20
|
|
'docs/src/**/*.md'
|
|
format: json
|
|
output: ./lychee/out.json
|
|
jobSummary: false
|
|
# The report, not a red build, is the signal for broken links. The
|
|
# validation step below still fails the run if the check itself
|
|
# breaks.
|
|
fail: false
|
|
|
|
- name: Validate report
|
|
# lychee does not reserve exit code 2 for broken links: its CLI
|
|
# parser also exits 2 on an invalid option, before any link was
|
|
# checked or any report written. Only a parseable report whose
|
|
# counts agree with the exit code counts as a link verdict; anything
|
|
# else fails here, and the report job below is skipped entirely, so
|
|
# the tracking issue is never touched. Exit 2 covers timeouts as
|
|
# well as errors, and a timed-out host is exactly the transient
|
|
# unavailability this report exists to surface, so both count as
|
|
# findings. Requiring total > 0 also catches a glob that silently
|
|
# stopped matching any file.
|
|
if: steps.lychee.outputs.exit_code == 0 || steps.lychee.outputs.exit_code == 2
|
|
env:
|
|
EXIT_CODE: ${{ steps.lychee.outputs.exit_code }}
|
|
run: |
|
|
jq -e --argjson code "$EXIT_CODE" '
|
|
(.total > 0) and
|
|
(if $code == 0
|
|
then .errors == 0 and .timeouts == 0
|
|
and (.error_map | length == 0) and (.timeout_map | length == 0)
|
|
else (.errors + .timeouts) > 0
|
|
and ((.error_map | length) + (.timeout_map | length)) > 0
|
|
end)
|
|
' ./lychee/out.json
|
|
|
|
- name: Upload report
|
|
if: steps.lychee.outputs.exit_code == 2
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: link-report
|
|
path: ./lychee/out.json
|
|
retention-days: 7
|
|
|
|
report:
|
|
name: Update report issue
|
|
needs: scan
|
|
runs-on: ubuntu-24.04
|
|
# Deliberately no checkout: this job needs the report artifact and the
|
|
# issues API, not the repository contents.
|
|
permissions:
|
|
issues: write
|
|
env:
|
|
EXIT_CODE: ${{ needs.scan.outputs.exit_code }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
steps:
|
|
- name: Classify checker result
|
|
# lychee exits 0 when every link resolves and 2 when links fail,
|
|
# both already cross-checked against the report by the scan job's
|
|
# validation step. Anything else (1 runtime, 3 bad config) means the
|
|
# check never produced a link verdict, which must surface as a failed
|
|
# run rather than be published as "broken documentation links".
|
|
run: |
|
|
case "$EXIT_CODE" in
|
|
0|2)
|
|
echo "lychee exit code $EXIT_CODE"
|
|
;;
|
|
*)
|
|
echo "::error::lychee exited with '$EXIT_CODE': the link check did not complete. Leaving the report issue untouched."
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
- name: Find existing report issue
|
|
id: report
|
|
# Matched on title alone, and through search rather than a listing:
|
|
# the issue action applies labels in a separate call after creating the
|
|
# issue, so a label filter misses a half-created report, and this
|
|
# repository has far more open issues than one listing page holds.
|
|
# Closed issues are included because a healthy run closes the report:
|
|
# an open-only lookup would forget that identity and the next failing
|
|
# run would open a duplicate. The oldest match stays the canonical
|
|
# report and is reopened below when links break again.
|
|
run: |
|
|
match=$(gh issue list --repo "$GITHUB_REPOSITORY" --state all \
|
|
--search "in:title \"$REPORT_TITLE\" author:app/github-actions" \
|
|
--limit 50 --json number,title,state \
|
|
--jq "[.[] | select(.title == \"$REPORT_TITLE\")] | sort_by(.number) | first // empty")
|
|
echo "number=$(jq -r '.number // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
|
|
echo "state=$(jq -r '.state // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Download report
|
|
if: env.EXIT_CODE == 2
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: link-report
|
|
path: ./lychee
|
|
|
|
- name: Compose report
|
|
if: env.EXIT_CODE == 2
|
|
run: |
|
|
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
{
|
|
echo "Broken documentation links found by [\`$GITHUB_WORKFLOW\`]($run_url)."
|
|
echo
|
|
echo "This issue is rewritten by every scheduled run and closed automatically once all links resolve."
|
|
echo
|
|
echo "Entries can be false positives: some sites rate-limit or block automated clients while working fine in a browser. Confirm before editing the docs, and add persistent offenders to \`--exclude\` in \`.github/workflows/docs-link-check.yml\`."
|
|
echo
|
|
# Timeouts are reported alongside errors: entries land in
|
|
# timeout_map with a status text instead of an HTTP code.
|
|
jq -r '
|
|
"\(.errors) of \(.total) links failed, \(.timeouts) timed out.",
|
|
"",
|
|
([(.error_map | to_entries[]), (.timeout_map | to_entries[])]
|
|
| group_by(.key)[] |
|
|
"### Errors in \(.[0].key)",
|
|
"",
|
|
(map(.value[])[] | "* [\(.status.code // .status.text // "ERR")] <\(.url)> — \(.status.details // .status.text // "unknown error")"),
|
|
"")
|
|
' ./lychee/out.json
|
|
} > ./lychee/issue.md
|
|
|
|
- name: Reopen report issue
|
|
# A healthy run closes the report, and the issue action below only
|
|
# rewrites the body of whatever number it is given. Without an
|
|
# explicit reopen, the 2 -> 0 -> 2 sequence would keep rewriting a
|
|
# closed issue while links are broken. A CLOSED state implies the
|
|
# lookup found a canonical issue, so no separate emptiness check.
|
|
if: env.EXIT_CODE == 2 && steps.report.outputs.state == 'CLOSED'
|
|
env:
|
|
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
|
|
run: |
|
|
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
gh issue reopen "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
|
|
--comment "Broken documentation links found again in [the latest run]($run_url)."
|
|
|
|
- name: Report broken links
|
|
if: env.EXIT_CODE == 2
|
|
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
|
|
with:
|
|
# Empty on the first failing run, which creates the issue; afterwards
|
|
# the same issue is updated in place.
|
|
issue-number: ${{ steps.report.outputs.number }}
|
|
title: ${{ env.REPORT_TITLE }}
|
|
content-filepath: ./lychee/issue.md
|
|
labels: documentation
|
|
|
|
- name: Close report issue once links are healthy
|
|
# An OPEN state implies the lookup found a canonical issue; a report
|
|
# that is already closed needs nothing.
|
|
if: env.EXIT_CODE == 0 && steps.report.outputs.state == 'OPEN'
|
|
env:
|
|
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
|
|
run: |
|
|
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
gh issue close "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
|
|
--comment "All documentation links resolved in [the latest run]($run_url)."
|