fix(scripts): preserve complete HTML src/href URLs in README link checks (#34817)

Only srcset is a comma-separated candidate list; src and href hold one URL.
Splitting them invented missing targets for paths containing commas or
spaces and could pass a missing target whose fragments were tracked.

Fixes #34815
This commit is contained in:
Blue Daydreaming
2026-10-28 07:07:46 -07:00
committed by GitHub
parent a6fdeda541
commit 0b604956ec
2 changed files with 45 additions and 6 deletions
+9 -5
View File
@@ -7,7 +7,7 @@ import { pathToFileURL } from 'node:url'
// Docs-only CI skips some media owners; check index paths because GitHub cannot render uncommitted docs-site/feature-wall assets.
const TRANSLATED_README_DIR = path.join('docs', 'readme')
const EXTERNAL_TARGET = /^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i
const HTML_ATTRIBUTE = /\b(?:src|srcset|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
const HTML_ATTRIBUTE = /\b(src|srcset|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
const MARKDOWN_LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
function readmeFiles(root) {
@@ -32,10 +32,14 @@ function trackedFiles(root, candidates) {
}
function* localTargets(markdown) {
for (const match of markdown.matchAll(HTML_ATTRIBUTE)) {
// srcset lists candidates; each begins with its URL.
for (const candidate of (match[1] ?? match[2]).split(',')) {
const target = candidate.trim().split(/\s+/)[0]
for (const [, attribute, doubleQuoted, singleQuoted] of markdown.matchAll(HTML_ATTRIBUTE)) {
const value = doubleQuoted ?? singleQuoted
// Only srcset lists candidates; each begins with its URL. src/href hold one whole URL.
const targets =
attribute === 'srcset'
? value.split(',').map((candidate) => candidate.trim().split(/\s+/)[0])
: [value.trim()]
for (const target of targets) {
if (target) {
yield target
}
@@ -44,7 +44,9 @@ const validReadmes = {
"<img src='resources/build/icon.png' />",
'<img src="https://img.shields.io/badge/x-y-z" />',
'[Contributing](.github/CONTRIBUTING.md) [Docs](https://example.com/docs) [Top](#top)',
'![hero](docs/assets/hero%20image.jpg "Hero")'
'![hero](docs/assets/hero%20image.jpg "Hero")',
// src/href hold one URL; only srcset is a comma-separated list.
'<img src="docs/assets/hero image,wide.jpg" /> <a href=\'https://example.com/docs?pages=one,two\'>Docs</a>'
].join('\n'),
'docs/readme/README.ja.md': [
'<img src="../../resources/build/icon.png" />',
@@ -56,6 +58,7 @@ const validReadmes = {
'resources/onboarding/feature-wall/tile-01.poster.jpg': 'jpg',
'docs/site/public/docs/tab-split.gif': 'gif',
'docs/assets/hero image.jpg': 'jpg',
'docs/assets/hero image,wide.jpg': 'jpg',
'.github/CONTRIBUTING.md': 'contributing',
LICENSE: 'mit'
}
@@ -77,6 +80,38 @@ describe('README local link check', () => {
expect(findBrokenReadmeLinks(makeFixture(validReadmes))).toEqual([])
})
it('rejects a missing full URL even when its split fragments are tracked', () => {
const root = makeFixture({
...validReadmes,
'README.md': '<img src="docs/assets/missing image,wide.jpg" />',
'docs/assets/missing': 'fragment',
'wide.jpg': 'fragment'
})
expect(findBrokenReadmeLinks(root)).toEqual([
{
readme: 'README.md',
target: 'docs/assets/missing image,wide.jpg',
resolved: 'docs/assets/missing image,wide.jpg'
}
])
})
it('continues checking each srcset candidate without its descriptor', () => {
const root = makeFixture({
...validReadmes,
'README.md': '<source srcset="resources/build/icon.png 1x, docs/assets/missing.png 2x">'
})
expect(findBrokenReadmeLinks(root)).toEqual([
{
readme: 'README.md',
target: 'docs/assets/missing.png',
resolved: 'docs/assets/missing.png'
}
])
})
it('checks tracked media outside the detector sparse checkout', () => {
const root = makeFixture(validReadmes)
const workflow = parse(readFileSync(path.join(projectDir, '.github/workflows/pr.yml'), 'utf8'))