Files
callstack__agent-device/test/ci/root-docs-paths-ignore.test.ts
Michał Pierzchała f45228ae71 ci: skip device lanes for root-level docs-only changes (#1781 A9) (#1791)
* ci: skip device lanes for root-level docs-only changes (#1781 A9)

Add AGENTS.md, CHANGELOG.md, CONTEXT.md, CONTRIBUTING.md, LICENSE, and
SECURITY.md to the pull_request paths-ignore block in ios.yml,
android.yml, linux.yml, macos.yml, ci.yml, and size.yml. These
root-level docs files were the only gap left after docs/**,
website/**, and README.md — PRs #1568 (SECURITY.md only), #1697
(CONTEXT.md + docs/adr only), and #1722 (AGENTS.md + docs/) each still
triggered a full 9-15 min macOS iOS run despite touching only prose.

Why each file is safe to ignore for every one of these six workflows:

- None of the four device workflows (ios/android/linux/macos) or their
  composite actions read any of these six files at runtime; the only
  hits from `grep -rln` across scripts/, src/, test/, and
  .github/actions/ are prose comments pointing humans at CONTEXT.md or
  AGENTS.md sections (e.g. scripts/layering/check.ts,
  scripts/wire-compat/run.ts, src/mcp/tool-ref-pins.ts) — never an
  `fs.readFileSync`/`readFile` of the file itself.
- The check-affected selector (scripts/check-affected/model.ts)
  already classifies all six as pure docs: `isDocs()` matches any
  `.md` file plus the literal `LICENSE`, and `docsOwnership()` only
  special-cases `website/docs/docs/commands.md` (unrelated). So these
  files already select zero checks — they only ever produced
  `docsOnlyPaths` entries, never `SelectionReason`s.
- Because they select zero checks, the gate-manifest's path-coverage
  category derivation (`scripts/gate/model.ts` `categories()`, which
  iterates `plan.reasons`) never records a category for them, so
  ci.yml has nothing check-manifest-only that these six files would
  need to keep reachable. `pnpm check:gate-manifest` and
  `pnpm check:gate-manifest:test` both stay green after the change
  (48 checks / 33 lanes, 28/28 gate tests passing).
- size.yml's bundle-size job (scripts/size-report.mjs) measures the
  `pnpm build` dist output and startup timing only — no reference to
  any of these six files. (npm packs LICENSE/README.md into the
  publishable tarball, but that's a `pnpm check:package` node-22.12
  concern in ci.yml's packaged-cli job, which is driven by `dist`
  contents and `package.json`, not by LICENSE/README prose — already
  evidenced by README.md being ignored here since before this change.)

Scope disclosure: `mutation-affected.yml` uses a `paths:` allowlist
(not paths-ignore) so it's structurally unaffected; `test-app-build-cache.yml`
has no path filter at all. Neither was touched.

actionlint and `pnpm check:gate-manifest`/`:test` pass on the changed
workflows.

* test: pin root-doc paths-ignore entries with a regression test

Addresses review feedback on #1791 from thymikee: the docs-only
classifier for AGENTS.md/CHANGELOG.md/CONTEXT.md/CONTRIBUTING.md/
LICENSE/SECURITY.md across ios.yml/android.yml/linux.yml/macos.yml/
ci.yml/size.yml had no regression pin. Neither check:gate-manifest
(only proves a *registered check* is reachable) nor actionlint (only
validates YAML shape) nor generic Markdown coverage would catch a
single dropped entry — e.g. LICENSE reappearing in one workflow's
paths-ignore list but not another's would silently put a full 9-15 min
device run back on prose-only PRs.

test/ci/root-docs-paths-ignore.test.ts parses the six real workflow
files and asserts, using the same matchesGlob the gate-manifest model
uses to decide lane triggering, that each of the six root docs is
ignored by each workflow's pull_request paths-ignore. Registered in
vitest.config.ts's unit-core project next to its sibling
upload-agent-device-artifacts.test.ts (parse-only, no device/subprocess
lane needed).

Verified red on main (all 36 file x doc assertions fail — confirmed via
a throwaway script reading `git show main:.github/workflows/*.yml`)
and green on this branch (6/6). Full unit-core project (873 files /
6641 tests) still passes; check:gate-manifest and
check:gate-manifest:test unchanged (48 checks / 33 lanes, 28/28).
2026-08-18 09:57:09 +02:00

59 lines
2.3 KiB
TypeScript

// Regression pin for #1781 A9: the six root-level docs files
// (AGENTS.md, CHANGELOG.md, CONTEXT.md, CONTRIBUTING.md, LICENSE, SECURITY.md)
// must stay in the `pull_request` `paths-ignore` list of every workflow that
// also ignores `docs/**`/`website/**`/`README.md` — the four device lanes plus
// `ci.yml` and `size.yml`. Nothing else derives this: `check:gate-manifest`
// only asks whether a *registered check* is reachable, generic Markdown
// coverage does not look at workflow trigger config at all, and `actionlint`
// only validates YAML shape, not policy — so a PR that quietly drops one entry
// (e.g. re-adds `LICENSE` to a device workflow while missing it in `size.yml`)
// would pass every other gate and put a full 9-15 min device run back on
// prose-only PRs. This test reads the real workflow files and asserts the
// behavior directly, via the same glob matcher the gate-manifest model uses
// to decide whether a lane triggers for a given path.
import fs from 'node:fs';
import path from 'node:path';
import { expect, test } from 'vitest';
import { parse } from 'yaml';
import { matchesGlob } from '../../scripts/gate/workflows.ts';
const repoRoot = path.resolve(import.meta.dirname, '../..');
const WORKFLOWS = ['ios.yml', 'android.yml', 'linux.yml', 'macos.yml', 'ci.yml', 'size.yml'];
const ROOT_DOCS = [
'AGENTS.md',
'CHANGELOG.md',
'CONTEXT.md',
'CONTRIBUTING.md',
'LICENSE',
'SECURITY.md',
];
type WorkflowDoc = {
// A bare `on:` key can parse as the boolean key `true` under YAML 1.1
// semantics; scripts/gate/workflows.ts already guards against this, so this
// test mirrors that fallback rather than trusting `on` alone.
on?: Record<string, { 'paths-ignore'?: string[] }>;
true?: Record<string, { 'paths-ignore'?: string[] }>;
};
function pathsIgnore(file: string): string[] {
const doc = parse(
fs.readFileSync(path.join(repoRoot, '.github/workflows', file), 'utf8'),
) as WorkflowDoc;
const on = doc.on ?? doc.true ?? {};
return on.pull_request?.['paths-ignore'] ?? [];
}
test.each(WORKFLOWS)('%s skips a pull_request triggered by only a root doc', (file) => {
const ignored = pathsIgnore(file);
for (const rootDoc of ROOT_DOCS) {
expect(
ignored.some((pattern) => matchesGlob(pattern, rootDoc)),
`${file}'s paths-ignore must match ${rootDoc} (got ${JSON.stringify(ignored)})`,
).toBe(true);
}
});