Files
gstack/lib/pregate/docs-only-lane.ts
T
Garry Tanandcapy-ai[bot] e077053c3c Let pull requests that only change human-facing docs skip the test suites (GST-1951) (#4178)
* Let pull requests that only change human-facing docs skip the test suites (GST-1951)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Quote the docs test list in the docs lane so shellcheck passes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep the Windows smoke check green on docs-only pull requests and update the workflow contract tests

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep the merged test check when the base gained a reader of a changed doc, hold the land policy key until a release reads it, and register two more docs reader tests

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Check links in untracked docs and docs with quoted or spaced names, and report an undecodable anchor instead of crashing

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Point the flaky-exception anchor back at its section and test the link checker's exit codes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-09 01:42:33 +00:00

42 lines
3.6 KiB
TypeScript

/**
* pregate's docs-only route (R81; docs/pregate.md#docs-only). When lib/docs-only.ts classifies the change from the
* merge base with origin/<base> to the working tree as human docs only, the lanes check runs the docs lane here
* instead of the test lanes: links on added lines must resolve, and the tests that read docs by directory
* (DOCS_READER_TESTS, those present in this repository) run locally. It owes no remote gate, so no CI carry is needed;
* pregate's regen (generated-page freshness) and secrets (credential scan) checks run as always.
*/
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { bunIsolationArgs } from '../bun-isolation';
import { addedLines, brokenLinks } from '../doc-links';
import { classifyDocsOnly } from '../docs-only';
import type { CheckResult, PregateContext } from './types';
/** The lanes result for a docs-only change, or null when the change is not docs-only (or cannot be classified). */
export function docsOnlyLane(ctx: PregateContext, base: CheckResult): CheckResult | null {
const ref = `origin/${ctx.base}`;
let r;
try { r = classifyDocsOnly({ root: ctx.repoRoot, base: ref }); } catch { return null; }
if (!r.docsOnly) return null;
ctx.explain.push(`docs-only: ${r.files.length} file(s) (lib/docs-only.ts); the test lanes are not owed`);
const md = r.files.filter(f => /\.md$/i.test(f) && fs.existsSync(path.join(ctx.repoRoot, f)));
let broken;
try {
const mb = spawnSync('git', ['merge-base', ref, 'HEAD'], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 30_000 }).stdout.trim();
broken = brokenLinks(ctx.repoRoot, md, addedLines(ctx.repoRoot, mb || ref, md));
} catch (e) {
return { ...base, status: 'incomplete', code: 'PREGATE_INCOMPLETE', detail: `docs-only: the link check could not read the diff: ${(e as Error).message}`, fix: `git fetch origin ${ctx.base}, then rerun gstack-pregate` };
}
const lines = [...(base.lines ?? []), `docs-only: ${r.files.join(' ')}`, ...broken.map(b => `${b.file}:${b.line}: ${b.target}: ${b.reason}`)];
if (broken.length) return { ...base, lines, status: 'fail', cause: 'diff', code: 'PREGATE_LANE_FAILED', detail: `docs-only: ${broken.length} broken link(s) on added lines`, fix: 'correct each link or add the heading it names, then rerun gstack-pregate' };
const tests = r.tests.filter(t => fs.existsSync(path.join(ctx.repoRoot, t)));
const done = (detail: string): CheckResult => ({ ...base, lines, status: 'pass', remote: [], detail });
if (!tests.length) return done(`docs-only: ${r.files.length} file(s); links on added lines resolve; no docs reader tests; the test suites are not owed`);
if (!ctx.allowRepoCommands) return { ...base, lines, status: 'incomplete', code: 'REPO_COMMANDS_NOT_ALLOWED', detail: `docs-only: ${tests.length} docs reader test file(s) selected but not run (pass --allow-repo-commands)`, fix: 'rerun with --allow-repo-commands (what /ship passes)' };
const run = spawnSync(process.execPath, [...bunIsolationArgs(), 'test', '--timeout=30000', ...tests], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 600_000, maxBuffer: 64 * 1024 * 1024 });
const tail = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim().split('\n').slice(-3).join(' | ');
if (run.status !== 0) return { ...base, lines: [...lines, tail], status: 'fail', cause: 'diff', code: 'PREGATE_LANE_FAILED', detail: `docs-only: docs reader tests failed (${run.error ? run.error.message : `exit ${run.status}`})`, fix: `bun test ${tests.join(' ')}` };
return done(`docs-only: ${r.files.length} file(s); links on added lines resolve; ${tests.length} docs reader test file(s) passed; the test suites are not owed`);
}