Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e170fc0
ci: comment on PRs that break internal links or anchors
marcleblanc2 Sep 6, 2026
8bfbc1a
test: rename a linked heading to exercise the PR comment (will be rev…
marcleblanc2 Sep 6, 2026
275d1ea
Revert "test: rename a linked heading to exercise the PR comment (wil…
marcleblanc2 Sep 6, 2026
20f17d6
check-links: accept <a name> and id= attributes as anchor targets
marcleblanc2 Sep 6, 2026
b3ce80e
check-links: detect case mismatches in public/ and docs/ asset links too
marcleblanc2 Sep 6, 2026
721b21f
check-links workflow: never comment on a clean PR
marcleblanc2 Sep 6, 2026
74a8f4c
check-links workflow: say what the resolved comment actually means
marcleblanc2 Sep 6, 2026
6391704
check-links: strip fences line by line, first file wins a route, same…
marcleblanc2 Sep 6, 2026
94c37b4
Add verify-links-live: prove a branch's changed links resolve on a de…
marcleblanc2 Sep 7, 2026
0339b3a
check-links: slug the full heading text when a heading contains a lin…
marcleblanc2 Sep 7, 2026
90d9cb6
check-links: state that redirects do not satisfy the PR check
marcleblanc2 Sep 9, 2026
423608e
check-links: reword the inbound-link guidance in the PR comment
marcleblanc2 Sep 10, 2026
66dbee2
check-links: split the inbound-link guidance and say why redirects do…
marcleblanc2 Sep 10, 2026
55e4ee2
check-links: link each file path in the PR comment to the file on the…
marcleblanc2 Sep 10, 2026
66cdca9
check-links: install only github-slugger in CI, link line numbers to …
marcleblanc2 Sep 10, 2026
055ed86
check-links: split the PR comment into outbound and inbound broken links
marcleblanc2 Sep 10, 2026
1cb89f3
check-links: export listFiles and routeFor for check-redirects, cance…
marcleblanc2 Sep 10, 2026
602ba67
check-links: flag absolute self-links with a suggested relative link,…
marcleblanc2 Sep 10, 2026
df94a94
check-links: review comment lists every finding on the line, naming t…
marcleblanc2 Sep 10, 2026
492b425
check-links: report absolute self-links only with --check-self-links,…
marcleblanc2 Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .github/workflows/check-links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Check links

# Reports internal links and #anchors that this PR breaks, compared with the
# merge base, absolute links to this site, and external links on added lines
# that 404. Pre-existing broken links on the base branch are ignored.

on:
pull_request:

# A new push supersedes the run for the previous one
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
check-links:
name: Broken links introduced by this PR
runs-on: ubuntu-latest
steps:
- name: Check out pull request head
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Install github-slugger, the only dependency of dev/check-links.mjs
# Into a scratch prefix, not the repo: `npm install <pkg>` next to
# package.json would install every dependency of the site
run: |
npm install --prefix "$RUNNER_TEMP/deps" --no-package-lock --no-audit --no-fund \
"github-slugger@$(node -p 'require("./package.json").dependencies["github-slugger"]')"
ln -s "$RUNNER_TEMP/deps/node_modules" node_modules

- name: Check out merge base
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
merge_base=$(git merge-base "$BASE_SHA" HEAD)
git worktree add "$RUNNER_TEMP/base" "$merge_base"
git diff -U0 "$merge_base" HEAD > "$RUNNER_TEMP/changes.diff"

- name: Record broken links already present on the base branch
# Exit 1 means findings, which is expected here
run: |
node dev/check-links.mjs --check-anchors --check-self-links --format json \
--root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-links.json" \
|| [ $? -eq 1 ]

- name: Find broken links introduced by this PR
id: check
env:
# File links in the report open the file on the PR branch
LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }}
run: |
if node dev/check-links.mjs --check-anchors --check-self-links --check-external --format markdown \
--baseline "$RUNNER_TEMP/base-links.json" \
--diff "$RUNNER_TEMP/changes.diff" \
--review "$RUNNER_TEMP/review.json" \
--link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then
echo "broken=false" >> "$GITHUB_OUTPUT"
else
echo "broken=true" >> "$GITHUB_OUTPUT"
fi
cat "$RUNNER_TEMP/report.md"

- name: Comment on the pull request
# Fork PRs get a read-only token; the report is still in the job log
if: github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BROKEN: ${{ steps.check.outputs.broken }}
run: |
marker='<!-- check-links-report -->'
existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
--paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)

# Comment only when there is something to report, or an earlier report to resolve
if [ "$BROKEN" = true ]; then
{ echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md"
elif [ -n "$existing_comment" ]; then
printf '%s\n### ✅ The broken links an earlier revision of this PR introduced are fixed\n' \
"$marker" > "$RUNNER_TEMP/comment.md"
else
exit 0
fi

if [ -n "$existing_comment" ]; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \
--field body=@"$RUNNER_TEMP/comment.md"
else
gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md"
fi

- name: Suggest fixes as review comments
# One suggested change per added line with a fix. Suggestions already on
# the PR (same file, line, and text) are not posted again.
if: steps.check.outputs.broken == 'true' && github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/comments" --paginate \
--jq '.[] | {path, line, body}' | jq -s . > "$RUNNER_TEMP/posted.json"
jq --slurpfile posted "$RUNNER_TEMP/posted.json" \
'.comments |= map(select(. as $comment | $posted[0] | index({path: $comment.path, line: $comment.line, body: $comment.body}) | not))' \
"$RUNNER_TEMP/review.json" > "$RUNNER_TEMP/review-new.json"

if [ "$(jq '.comments | length' "$RUNNER_TEMP/review-new.json")" -gt 0 ]; then
gh api --method POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \
--input "$RUNNER_TEMP/review-new.json" > /dev/null \
|| echo "::warning::Could not post the suggested fixes; they are in the report above"
fi

- name: Fail when this PR introduces broken links
if: steps.check.outputs.broken == 'true'
run: exit 1
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- **Build**: `npm run build`
- **Dev**: `npm run dev`
- **Lint**: `npm run lint`
- **Check links**: `npm run check-links -- --check-anchors --check-self-links` (CI comments on PRs that break links; see `dev/check-links.mjs`; `next build` runs it without flags, so only dead page links fail a deploy). When moving a page or renaming a heading, update every link to it; a redirect in `src/data/redirects.ts` does not satisfy the check. Link to this site with relative paths (`/admin/config/site-config`), never `https://sourcegraph.com/docs/…` or `https://docs.sourcegraph.com/…`. To also probe the external links you added: `npm run check-links -- --check-anchors --check-self-links --check-external --diff <(git diff -U0 origin/main)`
- **Prove changed links resolve on a deploy**: `node dev/verify-links-live.mjs --site <vercel-preview-url>` prints a Markdown table for the PR description

## AI Chat Integration

Expand Down
Loading
Loading