-
Notifications
You must be signed in to change notification settings - Fork 136
docs-2923: check links only on PR changes #2841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ctauchen
wants to merge
3
commits into
tigera:main
Choose a base branch
from
ctauchen:docs-2923-pr-scoped-link-check
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| name: PR link check | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'calico/**' | ||
| - 'calico-enterprise/**' | ||
| - 'calico-cloud/**' | ||
| - 'use-cases/**' | ||
| - 'calico_versioned_docs/**' | ||
| - 'calico-enterprise_versioned_docs/**' | ||
| - 'calico-cloud_versioned_docs/**' | ||
|
|
||
| # Cancel an older run when the PR gets a new push. | ||
| concurrency: | ||
| group: pr-link-check-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| link-check: | ||
| name: Check links on changed pages | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Enable Corepack | ||
| run: corepack enable | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '22' | ||
| cache: 'yarn' | ||
|
|
||
| - name: Build the site and the route map | ||
| run: make build | ||
| env: | ||
| LINK_CHECK_ROUTES: 'true' | ||
| NODE_OPTIONS: '--max-old-space-size=6000' | ||
| DOCUSAURUS_IGNORE_SSG_WARNINGS: 'true' | ||
|
|
||
| - name: Find the changed pages | ||
| id: pages | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| run: | | ||
| node scripts/changed-pages.js "$BASE_SHA" build/link-check-routes.json > pages.txt | ||
| COUNT=$(grep -c . pages.txt || true) | ||
| echo "count=$COUNT" >> "$GITHUB_OUTPUT" | ||
| echo "Changed pages ($COUNT):" | ||
| cat pages.txt | ||
|
|
||
| - name: Install Playwright Chrome | ||
| if: steps.pages.outputs.count != '0' | ||
| run: yarn playwright install --with-deps chrome | ||
|
|
||
| - name: Serve the site and check the changed pages | ||
| if: steps.pages.outputs.count != '0' | ||
| env: | ||
| CI: 'true' | ||
| run: | | ||
| # Join with commas so the list survives as one env var; the crawler splits on commas/newlines. | ||
| export PAGES_TO_CHECK="$(tr '\n' ',' < pages.txt)" | ||
| make test 2>&1 | tee link-check.log | ||
|
|
||
| - name: Upload the link-check log | ||
| if: always() && steps.pages.outputs.count != '0' | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: link-check-log | ||
| path: link-check.log | ||
| if-no-files-found: ignore | ||
|
|
||
| - name: Comment on the PR when the check fails | ||
| if: failure() && steps.pages.outputs.count != '0' | ||
| continue-on-error: true | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| run: | | ||
| BODY=comment.md | ||
| { | ||
| echo "## Link check failed" | ||
| echo "" | ||
| echo "The link check found broken links on the pages this PR changes." | ||
| echo "" | ||
| echo "Pages checked:" | ||
| echo '```' | ||
| cat pages.txt | ||
| echo '```' | ||
| echo "" | ||
| echo "Report (last 200 lines, full log is in the run artifact \"link-check-log\"):" | ||
| echo '```' | ||
| tail -n 200 link-check.log 2>/dev/null || echo "(no log captured)" | ||
| echo '```' | ||
| } > "$BODY" | ||
| gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$BODY" |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * changed-pages.js | ||
| * | ||
| * Print the built page URLs for the docs files changed in a PR, one per line. | ||
| * The PR link-check workflow feeds this list to the crawler as PAGES_TO_CHECK. | ||
| * | ||
| * Usage: | ||
| * node scripts/changed-pages.js [baseRef] [manifestPath] | ||
| * | ||
| * Defaults: baseRef = origin/main, manifestPath = build/link-check-routes.json. | ||
| * The manifest is written by the docusaurus-plugin-link-check-routes plugin | ||
| * during a build run with LINK_CHECK_ROUTES=true. | ||
| * | ||
| * Note: only files registered as docs pages map to a URL. Changes to partials, | ||
| * includes, or components are reported as "unmatched" and are not checked here. | ||
| */ | ||
|
|
||
| const { execFileSync } = require('child_process'); | ||
| const fs = require('fs'); | ||
|
|
||
| const base = process.argv[2] || process.env.BASE_REF || 'origin/main'; | ||
| const manifestPath = process.argv[3] || 'build/link-check-routes.json'; | ||
|
|
||
| const DOC_RE = /\.mdx?$/; | ||
|
|
||
| function changedDocFiles(baseRef) { | ||
| // execFile with an argument array: no shell, so baseRef cannot inject commands. | ||
| const out = execFileSync('git', ['diff', '--name-only', `${baseRef}...HEAD`], { encoding: 'utf8' }); | ||
| return out | ||
| .split('\n') | ||
| .map((s) => s.trim()) | ||
| .filter(Boolean) | ||
| .filter((f) => DOC_RE.test(f)); | ||
| } | ||
|
|
||
| let manifest; | ||
| try { | ||
| manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); | ||
| } catch (e) { | ||
| console.error(`[changed-pages] cannot read manifest ${manifestPath}: ${e.message}`); | ||
| process.exit(2); | ||
| } | ||
|
|
||
| const files = changedDocFiles(base); | ||
| const permalinks = new Set(); | ||
| const unmatched = []; | ||
|
|
||
| for (const f of files) { | ||
| const links = manifest[f]; | ||
| if (links && links.length) { | ||
| links.forEach((l) => permalinks.add(l)); | ||
| } else { | ||
| unmatched.push(f); | ||
| } | ||
| } | ||
|
|
||
| // Pages go to stdout (consumed by the workflow); diagnostics go to stderr. | ||
| for (const p of [...permalinks].sort()) { | ||
| console.log(p); | ||
| } | ||
|
|
||
| console.error( | ||
| `[changed-pages] base=${base} changed-docs=${files.length} pages=${permalinks.size} unmatched=${unmatched.length}` | ||
| ); | ||
| if (unmatched.length) { | ||
| console.error( | ||
| `[changed-pages] unmatched (partials/includes/components, not checked): ${unmatched.slice(0, 10).join(', ')}${unmatched.length > 10 ? '...' : ''}` | ||
| ); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * docusaurus-plugin-link-check-routes | ||
| * | ||
| * A Docusaurus postBuild plugin that writes a map of source file to built URL. | ||
| * The PR link check uses this map to turn "files changed in a PR" into "pages to check". | ||
| * | ||
| * Output: <build>/link-check-routes.json, shaped as: | ||
| * { "calico/getting-started/install.mdx": ["/calico/latest/getting-started/install/"], ... } | ||
| * | ||
| * Gated by LINK_CHECK_ROUTES=true — no-op on regular builds, so it does not change normal output. | ||
| */ | ||
|
|
||
| import fs from 'fs/promises'; | ||
| import path from 'path'; | ||
|
|
||
| const PLUGIN_NAME = 'docusaurus-plugin-link-check-routes'; | ||
| const LOG_PREFIX = '[link-check-routes]'; | ||
| const OUTPUT_FILE = 'link-check-routes.json'; | ||
|
|
||
| export default function linkCheckRoutesPlugin() { | ||
| return { | ||
| name: PLUGIN_NAME, | ||
|
|
||
| async postBuild({ outDir, plugins }) { | ||
| if (process.env.LINK_CHECK_ROUTES !== 'true') { | ||
| return; | ||
| } | ||
|
|
||
| // sourcePath (repo-relative) -> array of permalinks (one source can map to more than one). | ||
| const routes = {}; | ||
| const add = (source, permalink) => { | ||
| if (!source || !permalink) return; | ||
| const rel = source.replace(/^@site\//, ''); | ||
| if (!routes[rel]) routes[rel] = []; | ||
| if (!routes[rel].includes(permalink)) routes[rel].push(permalink); | ||
| }; | ||
|
|
||
| const docsPlugins = plugins.filter( | ||
| (p) => p.name === 'docusaurus-plugin-content-docs' | ||
| ); | ||
|
|
||
| for (const dp of docsPlugins) { | ||
| const versions = dp.content?.loadedVersions || []; | ||
| for (const version of versions) { | ||
| for (const doc of version.docs || []) { | ||
| add(doc.source, doc.permalink); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const outPath = path.join(outDir, OUTPUT_FILE); | ||
| await fs.writeFile(outPath, JSON.stringify(routes, null, 2)); | ||
| console.log( | ||
| `${LOG_PREFIX} Wrote ${outPath} (${Object.keys(routes).length} source files).` | ||
| ); | ||
| }, | ||
| }; | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Internal route links are already validated by the build step:
make buildruns withonBrokenLinks: 'throw', so a PR that introduces a broken internal route fails before the crawl even starts. The scoped crawl intentionally covers external and code-block URLs on the changed pages. Broken anchors are a separate track (onBrokenAnchors is 'warn'; see DOCS-2979/2980). I've updated the PR description to state this explicitly.