Make internal link checker blocking on PRs (#62268) · github/docs@54f5ed7 · GitHub
Skip to content

Commit 54f5ed7

Browse files
heiskrCopilot
andauthored
Make internal link checker blocking on PRs (#62268)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 031a122f-ce90-4f05-8641-f98155428219 Copilot-Session: 15ec74e5-6592-4b40-9482-74d62bd15d7c
1 parent f530144 commit 54f5ed7

6 files changed

Lines changed: 106 additions & 18 deletions

File tree

‎.github/workflows/link-check-on-pr.yml‎

Lines changed: 1 addition & 3 deletions
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
1. Create a new custom security configuration, or edit an existing one. See [AUTOTITLE](/code-security/how-tos/secure-at-scale/configure-organization-security/establish-complete-coverage/create-custom-configuration.
2-
1+
1. Create a new custom security configuration, or edit an existing one. See [AUTOTITLE](/code-security/how-tos/secure-at-scale/configure-organization-security/establish-complete-coverage/create-custom-configuration).

‎src/links/lib/extract-links.ts‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import type { Context, Page } from '@/types'
1919
const logger = createLogger(import.meta.url)
2020

2121
// Link patterns for Markdown
22-
const INTERNAL_LINK_PATTERN = /\]\(\/[^)]+\)/g
23-
const AUTOTITLE_LINK_PATTERN = /\[AUTOTITLE\]\(([^)]+)\)/g
22+
const INTERNAL_LINK_PATTERN = /\]\((\/[^()\s]+(?:\([^()]*\)[^()\s]*)*)\)/g
23+
const AUTOTITLE_LINK_PATTERN = /\[AUTOTITLE\]\(([^)\s]+)\)/g
2424
// Handles one level of balanced parentheses in URLs (e.g., Wikipedia links).
2525
// Uses an unrolled loop to avoid catastrophic backtracking on malformed URLs.
2626
const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/[^()\s]*(?:\([^()]*\)[^()\s]*)*)\)/g
@@ -128,13 +128,27 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
128128

129129
// Strip fenced code blocks to avoid checking example/placeholder URLs
130130
// Replaces non-newline characters with spaces to preserve line numbers and positions
131-
const strippedContent = content.replace(
131+
const withoutFences = content.replace(
132132
/^ {0,3}(`{3,})[^\n]*\n[\s\S]*?^ {0,3}\1\s*$/gm,
133133
(match) => {
134134
return match.replace(/[^\n]/g, ' ')
135135
},
136136
)
137137

138+
// Strip inline code spans too, so example or placeholder links written inside
139+
// backticks (e.g. `[AUTOTITLE](/PATH/TO/PAGE)` in the style guide) aren't
140+
// treated as real links. Markdown never renders links inside inline code.
141+
//
142+
// Per CommonMark, a code span opens with a backtick run of length N and closes
143+
// with a run of exactly N backticks; both runs must be maximal, i.e. not
144+
// adjacent to another backtick. The lookarounds enforce that so mismatched
145+
// runs (e.g. `x`` or ``x```) stay literal instead of masking real text (and a
146+
// real link) between them, which would let a broken link evade the check.
147+
// The content is replaced with spaces to preserve line numbers and positions.
148+
const strippedContent = withoutFences.replace(/(?<!`)(`+)(?!`)[^\n]*?(?<!`)\1(?!`)/g, (match) => {
149+
return match.replace(/[^\n]/g, ' ')
150+
})
151+
138152
// Precompute line-start offsets once so every getLineAndColumn call is O(log L).
139153
const lineOffsets = buildLineOffsets(strippedContent)
140154

@@ -160,14 +174,14 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
160174
// Extract regular internal links
161175
while ((match = INTERNAL_LINK_PATTERN.exec(strippedContent)) !== null) {
162176
// Skip if this is an AUTOTITLE link (already captured)
163-
const fullMatch = match[0]
164177
if (strippedContent.substring(match.index - 10, match.index).includes('AUTOTITLE')) {
165178
continue
166179
}
167180

168181
const { line, column } = getLineAndColumn(lineOffsets, match.index)
169-
// Extract href from ](/path) format
170-
const href = fullMatch.substring(2, fullMatch.length - 1).split('#')[0]
182+
// Extract href from ](/path) format. The destination is captured in group 1,
183+
// which handles balanced parentheses (e.g. asset filenames like `(fr).pdf`).
184+
const href = match[1].split('#')[0]
171185
const text = extractLinkText(strippedContent, match.index)
172186

173187
internalLinks.push({

‎src/links/lib/link-report.ts‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,6 @@ ${errors
151151

152152
return `## 🔗 Link Check Results
153153
154-
> [!NOTE]
155-
> **This check is being actively worked on** and may produce false positives. If something looks wrong, you can safely ignore it for now.
156-
157154
${errorSection}${warningSection}${detailsLink}
158155
<!-- link-checker-pr-comment -->`
159156
},

‎src/links/scripts/check-links-pr.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ function filterContentFiles(files: string[]): string[] {
150150
return files.filter((file) => {
151151
// Only check Markdown files in content/ or data/
152152
if (!file.endsWith('.md')) return false
153+
// Skip README.md files. They're developer docs, not published pages, and use
154+
// repo-relative paths (e.g. /src/...) that aren't valid site links.
155+
if (file === 'README.md' || file.endsWith('/README.md')) return false
153156
if (file.startsWith('content/') || file.startsWith('data/')) return true
154157
return false
155158
})
@@ -338,15 +341,24 @@ async function main() {
338341
}
339342
}
340343

341-
// Write artifact for debugging
344+
// Write artifact for debugging. Best-effort: a reporting/API failure must
345+
// never fail the build. Only broken links (below) should fail the PR.
342346
const allFlaws = [...allBrokenLinks, ...allRedirectLinks]
343-
await uploadArtifact('broken-links.json', JSON.stringify(groupBrokenLinks(allFlaws), null, 2))
347+
try {
348+
await uploadArtifact('broken-links.json', JSON.stringify(groupBrokenLinks(allFlaws), null, 2))
349+
} catch (err) {
350+
console.warn('Could not upload broken-links artifact:', err)
351+
}
344352

345-
// Post PR comment if configured
353+
// Post PR comment if configured. Best-effort for the same reason.
346354
const shouldComment = process.env.SHOULD_COMMENT === 'true'
347355
if (shouldComment) {
348356
const actionUrl = process.env.ACTION_RUN_URL
349-
await commentOnPR(allFlaws, actionUrl)
357+
try {
358+
await commentOnPR(allFlaws, actionUrl)
359+
} catch (err) {
360+
console.warn('Could not post PR comment:', err)
361+
}
350362
}
351363

352364
// Exit with error if broken links found

‎src/links/tests/extract-links.ts‎

Lines changed: 68 additions & 0 deletions

0 commit comments

Comments
 (0)