diff --git a/.github/workflows/docs-pr-check.yml b/.github/workflows/docs-pr-check.yml index 0cf3b3c..19724db 100644 --- a/.github/workflows/docs-pr-check.yml +++ b/.github/workflows/docs-pr-check.yml @@ -23,10 +23,78 @@ jobs: - name: Install dependencies run: npm ci - # The build is the gate. docs/.vuepress/config.ts sets the theme's - # linksCheck plugin to `build: 'error'`, so VuePress throws and exits - # non-zero on a dead internal link. Nothing here greps the log: a - # log-string check passes whenever the message wording changes, and it - # passed for exactly that reason before. - - name: Build docs (fails on broken internal links) + # The build catches ONE class of dead link, not all of them. See the + # coverage map on the next step before concluding anything is redundant. + # + # docs/.vuepress/config.ts sets the theme's linksCheck plugin to + # `build: 'error'`, so VuePress throws and exits non-zero on a dead + # internal link *that is written as a markdown link to a .md target*. The + # plugin filters candidates on /\.md(?:[?#]|$)/, which is about 861 of + # roughly 1331 internal links; it cannot see links in config.ts, in + # frontmatter, in raw HTML, or written with a .html / no extension. + # + # Nothing here greps the log: a log-string check passes whenever the + # message wording changes, and it passed for exactly that reason before. + - name: Build docs (fails on broken internal markdown links) run: npm run docs:build + + # --------------------------------------------------------------------- + # THE ARTIFACT GATE. Every other link gate in this repo reads SOURCE + # MARKDOWN. This one reads the bytes that get deployed. + # + # WHICH GATE COVERS WHICH CLASS — read this before deleting anything. + # The comment above this step used to call the build "the gate" while it + # covered about 65% of internal links, and a maintainer could reasonably + # have deleted link-check.yml believing it redundant. It is not. + # + # link class | build | base-prefix | lychee-source | THIS + # --------------------------------------------|-------|-------------|---------------|----- + # [x](./y.md) dead target | YES | - | YES | YES + # [x](/cli/y) missing /forge_docs/ base | - | YES | YES | YES + # navbar / sidebar entry in config.ts | - | - | - | YES + # docs/README.md frontmatter hero actions | - | - | - | YES + # raw in markdown | - | - | YES | YES + # .html-suffixed and extensionless targets | - | YES | YES | YES + # theme-generated links (prev/next, edit) | - | - | - | YES + # a page that renders but is never emitted | - | - | - | YES + # asset ', + 'must resolve — index', + 'must resolve — pretty', + 'must NOT resolve — missing file', + 'must NOT resolve — missing base prefix', + '
<a href="/canary_base/not-a-real-link.html">in a code sample</a>
', + '', + '', + ].join('\n')) + + const { census, findings } = scan({ distRoot: resolve(dir), base, relativeMode: 'warn' }) + const kinds = findings.map((f) => f.kind).sort() + const problems = [] + + if (census.pages !== 2) problems.push(`walked ${census.pages} canary pages, expected 2`) + if (census.byRule.exact !== 3) problems.push(`exact-rule resolutions ${census.byRule.exact}, expected 3`) + if (census.byRule.index !== 1) problems.push(`index-rule resolutions ${census.byRule.index}, expected 1`) + if (census.byRule.pretty !== 1) problems.push(`pretty-rule resolutions ${census.byRule.pretty}, expected 1`) + if (findings.length !== 3) problems.push(`reported ${findings.length} findings, expected exactly 3`) + if (kinds.join(',') !== 'missing-base,missing-file,missing-file') { + problems.push(`finding kinds were [${kinds.join(', ')}], expected [missing-base, missing-file, missing-file]`) + } + // The code sample and the script-body string must NOT have been collected. + // If either shows up, the extractor is reading page text as markup and + // every "finding" it reports is suspect. + if (findings.some((f) => /not-a-real-link|also-not-a-link/.test(f.ref))) { + problems.push('extractor picked up an attribute-shaped string out of a code sample or a