mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-18 03:58:26 +00:00
244 lines
11 KiB
YAML
244 lines
11 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:
|
|
checker_outcome: ${{ steps.lychee.outcome }}
|
|
exit_code: ${{ steps.lychee.outputs.exit_code }}
|
|
status: ${{ steps.validate.outputs.status }}
|
|
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
|
|
continue-on-error: true
|
|
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 issue, not a red workflow run, is the signal for link
|
|
# findings and checker failures alike.
|
|
fail: false
|
|
|
|
- name: Validate report
|
|
id: validate
|
|
# 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 a completed exit code (0 or 2) counts as a link
|
|
# verdict. Everything else becomes a checker-error report instead of
|
|
# failing the workflow. 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: always()
|
|
env:
|
|
CHECKER_OUTCOME: ${{ steps.lychee.outcome }}
|
|
EXIT_CODE: ${{ steps.lychee.outputs.exit_code }}
|
|
run: |
|
|
status=checker-error
|
|
if [[ "$CHECKER_OUTCOME" == success ]] &&
|
|
[[ "$EXIT_CODE" == 0 || "$EXIT_CODE" == 2 ]] &&
|
|
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
|
|
then
|
|
if [[ "$EXIT_CODE" == 0 ]]; then
|
|
status=healthy
|
|
else
|
|
status=findings
|
|
fi
|
|
fi
|
|
echo "status=$status" >> "$GITHUB_OUTPUT"
|
|
echo "Validated link check as $status"
|
|
|
|
- name: Upload report
|
|
if: steps.validate.outputs.status == 'findings'
|
|
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:
|
|
CHECKER_OUTCOME: ${{ needs.scan.outputs.checker_outcome }}
|
|
EXIT_CODE: ${{ needs.scan.outputs.exit_code }}
|
|
STATUS: ${{ needs.scan.outputs.status }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
steps:
|
|
- 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 a problem recurs.
|
|
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.STATUS == 'findings'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: link-report
|
|
path: ./lychee
|
|
|
|
- name: Compose report
|
|
if: env.STATUS == 'findings'
|
|
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: Compose checker error report
|
|
if: env.STATUS == 'checker-error'
|
|
run: |
|
|
mkdir -p ./lychee
|
|
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
{
|
|
echo "The documentation link check did not complete in [the latest run]($run_url)."
|
|
echo
|
|
echo "This issue is rewritten by every scheduled run and closed automatically once a trustworthy run finds that all links resolve."
|
|
echo
|
|
echo "The checker did not produce a trustworthy link verdict. Treat the previous result, if any, as stale until a later run completes."
|
|
echo
|
|
echo "* Action outcome: \`$CHECKER_OUTCOME\`"
|
|
echo "* Exit code: \`${EXIT_CODE:-not reported}\`"
|
|
echo "* Verdict validation: \`failed\`"
|
|
} > ./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, a later finding or checker error would rewrite a
|
|
# closed issue. A CLOSED state implies the lookup found a canonical
|
|
# issue, so no separate emptiness check.
|
|
if: >-
|
|
env.STATUS != 'healthy' &&
|
|
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 "The documentation link checker reported a problem again in [the latest run]($run_url)."
|
|
|
|
- name: Report link-check problem
|
|
if: env.STATUS != 'healthy'
|
|
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.STATUS == 'healthy' &&
|
|
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)."
|