ci: narrow native-host lane selection and reuse build caches (#2131)

* ci: narrow native-host lane selection and reuse build caches

Unowned src/ and bridge/ files no longer fall back to the full baseline.
They run the Linux suite plus the Linux x64 installed-package/Inspector
baseline. New apple and package scopes own the Apple artifact lane, the
package matrix and the Windows curated lane, including the test files those
lanes invoke. Manual-only verifier entrypoints run static checks and the
Linux suite. CHANGELOG, SECURITY, GitHub templates and the native Windows
README are documentation-only.

Pull requests omit the macOS x64 package host unless labelled ci:full.
The Windows and Apple lanes restore the shared Turbo build cache, the
Windows lane drops its duplicate typecheck, and the Linux suite uses six
shards. Main pushes keep the full baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: match package script entrypoints against scope globs

Script-only package.json edits resolved their entrypoint with literal
equality, so glob-owned verifiers such as scripts/verify/apple/** and
scripts/verify/inspector/** fell back to the full baseline instead of
selecting their owning lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-12 06:07:22 -07:00 committed by GitHub
parent 5190843f8f
commit 4f46eac453
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 413 additions and 45 deletions

View File

@ -105,7 +105,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
shard: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -372,7 +372,7 @@ jobs:
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: changes
needs: [changes, build]
if: needs.changes.outputs.apple == 'true'
runs-on: macos-15
timeout-minutes: 15
@ -383,6 +383,10 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: rea-build-cache
path: .turbo/cache/
- name: Verify Apple artifact formats and metadata
env:
REA_ARTIFACT_NODE_COMPILE_CACHE: ${{ runner.temp }}/rea-artifact-node-cache
@ -445,7 +449,7 @@ jobs:
windows-curated:
name: Windows capability and package lane
needs: [changes, windows-native-build]
needs: [changes, build, windows-native-build]
if: needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
@ -456,8 +460,11 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: rea-build-cache
path: .turbo/cache/
- run: npm run build:cached
- run: npm run typecheck
- uses: actions/download-artifact@v4
with:
name: windows-native-controls

View File

@ -837,25 +837,42 @@ not make native host acceptance unavailable.
## Native platform baseline in CI
Pull requests that change only root `README*.md` files, `docs/`, `AGENTS.md`,
or `CONTRIBUTING.md` run formatting and generated-document validation without
the source-test shards or native package lanes. Classification compares the PR
head with its merge base, so later base-branch changes do not expand that scope.
Static and coverage aggregate jobs remain present and fail if classification
fails. Every code change retains the complete four-shard Linux suite; individual
test files are not selected by imports or filenames.
`CONTRIBUTING.md`, `CHANGELOG.md`, `SECURITY.md`, `native/windows/README.md`,
or the GitHub issue and pull-request templates run formatting and
generated-document validation without the source-test shards or native package
lanes. Classification compares the PR head with its merge base, so later
base-branch changes do not expand that scope. Static and coverage aggregate jobs
remain present and fail if classification fails. Every code change retains the
complete six-shard Linux suite; individual test files are not selected by
imports or filenames.
`scripts/ci/plan.mjs` selects lanes using the ownership table in
`scripts/ci/scopes.mjs`. Provider implementation changes run their real-provider
verifiers and the Linux x64 installed-package/Inspector baseline. Test-only
changes run the Linux suite without native package verification. Website changes
run website checks. Provider workflow changes validate the workflow and run that provider's verifier.
Manual-provider/Android workflow changes run workflow validation only.
Shared process/contract/CLI boundaries, dependencies, packaging inputs, unknown
paths, and changes to the planner select the full baseline. Package script-only
edits select their owning verifier when its entrypoint is identifiable; unknown
or shared script changes select everything. Android build-script edits retain
the Linux suite and workflow validation; real Android verification stays manual.
Renames account for both paths.
verifiers and the Linux x64 installed-package/Inspector baseline. The `apple`
scope owns the Apple artifact lane; the `package` scope owns the multi-host
package matrix and the Windows curated lane, including Windows native controls,
Inspector, setup, configuration and packaging entrypoints. Runtime sources under
`src/` or `bridge/` without an owner, and the authored skill, run the Linux suite
and the Linux x64 installed-package/Inspector baseline only. Test-only changes
run the Linux suite without native package verification, except test files that
the Apple or Windows curated lanes invoke explicitly, which also select that
lane. Edits to verifier entrypoints run only by manual, licensed or self-hosted
workflows run static checks and the Linux suite. Website changes run website
checks. Provider workflow changes validate the workflow and run that provider's
verifier. Manual-provider/Android workflow changes run workflow validation only.
Shared process/contract/CLI boundaries, dependencies, other packaging inputs,
unknown paths, and changes to the planner select the full baseline. Package
script-only edits select their owning verifier when its entrypoint is
identifiable; unknown or shared script changes select everything. Android
build-script edits retain the Linux suite and workflow validation; real Android
verification stays manual. Renames account for both paths.
This narrower selection trades PR-time coverage for runner capacity. A change to
unowned shared runtime code that fails only on arm64, macOS, Windows, the Apple
artifact lane, or a provider lane that does not own the file is not caught on
the PR; the main-push full baseline reports it after merge. Ownership globs do
not follow imports. Use the `ci:full` label when a change may have native-host
or provider effects outside its owners.
Main pushes and manual dispatch retain the full baseline, including all portable
provider lanes and the native matrix below. A `ci:full` PR label selects the full
@ -879,12 +896,16 @@ CI exercises the pinned Node.js runtime on native hosted runners:
| Linux x64 | `ubuntu-latest` | Installed package and real Node Inspector CLI/MCP |
| Linux arm64 (aarch64) | `ubuntu-24.04-arm` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 arm64 | `macos-15` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP (main, dispatch and `ci:full` only) |
| Windows x64 | `windows-latest` | Curated capabilities, native controls, installed package and real Node Inspector CLI/MCP |
macOS x64 is the slowest package host. Pull requests without the `ci:full` label
omit it, so darwin x64 installed-package and Inspector regressions surface on
the main-push full baseline rather than on the PR.
Package lanes assert the actual Node platform/architecture before verification
and record those values with the Node version. The four-host package matrix
runs up to four jobs concurrently on independent hosted runners with explicit
and record those values with the Node version. The package matrix runs up to
four jobs concurrently on independent hosted runners with explicit
timeouts and per-runner Node heap/thread limits; Windows retains its curated
capability lane. Package checks cover installation, CLI/MCP discovery,
target-free analysis, configuration backups/recovery, Evidence and owned
@ -1077,7 +1098,7 @@ CI owns coverage; aggregate and domain/contract thresholds are maintained in
`vitest.config.ts`. They are glob-specific and never updated automatically.
Coverage does not replace named boundary or real-provider scenarios.
CI runs four native Vitest shards without retries. Each shard emits a blob
CI runs six native Vitest shards without retries. Each shard emits a blob
report; the merge job produces aggregate coverage plus JUnit and JSON timing
reports, uploads them together, and writes the slowest files to the workflow
summary. Static checks, documentation, build, package verification, and
@ -1091,8 +1112,9 @@ architecture, and `.npmrc`; test-only changes can reuse compiled outputs. Every
build and verification command still runs, so Turbo checks task inputs and
rebuilds misses. There is no prefix fallback to accumulate older build outputs.
Native package jobs reuse the same workflow's Turbo cache for portable compiled
JavaScript, skills, and catalogs. They retain the normal build and prepack
Native package jobs, the Windows curated lane, and the Apple artifact lane reuse
the same workflow's Turbo cache for portable compiled JavaScript, skills, and
catalogs. The Windows lane relies on the Linux static job for typechecking. They retain the normal build and prepack
commands, which restore matching outputs or rebuild on a cache miss. Native
artifacts, dependency installs, package archives, and runtime verification remain
host-local. Keep platform-dependent outputs out of this shared cache.

View File

@ -1,19 +1,24 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { matchesGlob } from "node:path";
import { SCOPES } from "./scopes.mjs";
import { MANUAL_VERIFIERS, SCOPES } from "./scopes.mjs";
const flags = [
"code",
"runtime",
"package",
"apple",
"fixtures",
"readiness",
"docs",
"workflows",
...Object.keys(SCOPES),
...new Set([
"code",
"runtime",
"package",
"apple",
"fixtures",
"readiness",
"docs",
"workflows",
...Object.keys(SCOPES),
]),
];
// Test files select the native-host lanes that run them explicitly. Provider
// scopes remain implementation-owned, so test-only edits stay off those lanes.
const NATIVE_TEST_OWNERS = ["apple", "package"];
// Actionlint validates workflow syntax only. The semantic suite asserts these
// workflows' release, publication and self-hosted runner safeguards.
const GUARDED_WORKFLOWS = new Set([
@ -87,11 +92,11 @@ function classifyPackage(base, head) {
const scriptOwners =
simple &&
entries.every((entry) =>
Object.values(SCOPES).some((patterns) => patterns.includes(entry)),
Object.values(SCOPES).some((patterns) => matchesAny(entry, patterns)),
)
? Object.entries(SCOPES)
.filter(([, patterns]) =>
entries.some((entry) => patterns.includes(entry)),
entries.some((entry) => matchesAny(entry, patterns)),
)
.map(([owner]) => owner)
: [];
@ -119,14 +124,23 @@ if (
.filter(Boolean);
for (const file of files) {
if (
/^(README(?:_[^/]+)?\.md|AGENTS\.md|CONTRIBUTING\.md)$/.test(file) ||
file.startsWith("docs/")
/^(README(?:_[^/]+)?\.md|AGENTS\.md|CONTRIBUTING\.md|CHANGELOG\.md|SECURITY\.md|native\/windows\/README\.md|\.github\/pull_request_template\.md)$/.test(
file,
) ||
file.startsWith("docs/") ||
file.startsWith(".github/ISSUE_TEMPLATE/")
) {
selected.docs = true;
selected.website ||= matchesAny(file, SCOPES.website);
continue;
}
if (/^(src|tests)\/.*\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file)) {
selected.code = true;
for (const owner of NATIVE_TEST_OWNERS)
selected[owner] ||= matchesAny(file, SCOPES[owner]);
continue;
}
if (matchesAny(file, MANUAL_VERIFIERS)) {
selected.code = true;
continue;
}
@ -166,6 +180,14 @@ if (
} else if (owners.length) {
if (file.startsWith("src/") || file.startsWith("bridge/"))
selected.runtime = true;
} else if (/^(src|bridge)\//.test(file)) {
// Unowned runtime sources keep the Linux x64 installed-package baseline;
// ci:full, main pushes and manual dispatch retain every native host.
selected.runtime = true;
reasons.push(`Unowned runtime source: ${file}`);
} else if (file.startsWith(".agents/skills/")) {
selected.runtime = true;
selected.docs = true;
} else {
full(`Unclassified input: ${file}`);
}
@ -173,13 +195,19 @@ if (
} else throw new Error(`Unsupported CI event: ${event}`);
selected.runtime ||= selected.package;
const linux = { os: "ubuntu-latest", platform: "linux", architecture: "x64" };
// macOS x64 is the slowest package host. Pull requests run it only with ci:full;
// main pushes, schedules and manual dispatch always include it.
const intel =
event !== "pull_request" || process.env.CI_FULL === "true"
? [{ os: "macos-15-intel", platform: "darwin", architecture: "x64" }]
: [];
const matrix = {
include: selected.package
? [
linux,
{ os: "ubuntu-24.04-arm", platform: "linux", architecture: "arm64" },
{ os: "macos-15", platform: "darwin", architecture: "arm64" },
{ os: "macos-15-intel", platform: "darwin", architecture: "x64" },
...intel,
]
: [linux],
};

View File

@ -1,6 +1,88 @@
// Ownership of the real-provider and website verification lanes. Shared build
// inputs are classified once in plan.mjs rather than repeated in every workflow.
// Ownership of the native-host, real-provider and website verification lanes.
// Shared build inputs are classified once in plan.mjs rather than repeated in
// every workflow.
export const SCOPES = {
// Apple artifact CLI/MCP lane on macOS, including its focused regressions.
apple: [
"src/native/Apple*",
"src/native/NativeMacOS*",
"src/native/NativeUi*",
"src/native/Lldb*",
"bridge/native/**",
"bridge/process/**",
"scripts/verify/apple/**",
"tests/boundary/filesystem/installScript.test.ts",
"tests/boundary/filesystem/nibNativeHierarchy.test.ts",
"tests/boundary/filesystem/interfaceBuilderBundle.test.ts",
"tests/boundary/cli/nativePlistCli.test.ts",
"tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts",
"tests/boundary/cli/nativePlistSelectionFailureCli.test.ts",
"tests/boundary/cli/nativeDemangleCli.test.ts",
"tests/boundary/mcp/nativeDemangleMcp.test.ts",
"tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts",
"tests/boundary/cli/nativeSignatureCli.test.ts",
"tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts",
"src/process/capture/FilesystemSnapshot.test.ts",
"tests/boundary/process/darwinProcessRunTokenReader.test.ts",
"tests/boundary/process/directAnalysisSignals.test.ts",
"tests/boundary/process/processCaptureLifecycle.test.ts",
"tests/boundary/process/processCli.test.ts",
"tests/boundary/mcp/sessionMcp.test.ts",
],
// Four-host installed-package matrix and the Windows curated lane.
package: [
"src/windows/**",
"native/windows/**",
"src/inspector/**",
"scripts/verify/inspector/**",
"scripts/verify-package.mjs",
"scripts/verify-package-*",
"scripts/verify-windows-*.mjs",
"scripts/build-windows-native.mjs",
"scripts/windows-capabilities.mjs",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
"scripts/rea.mjs",
"install.sh",
"src/config/**",
"src/application/Setup*.ts",
"src/application/Uninstall*.ts",
"src/cli/interactiveSetup.ts",
"src/cli/setupCommands.ts",
"tests/boundary/filesystem/binaryTarget.test.ts",
"tests/boundary/filesystem/safeOutputTree.test.ts",
"tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts",
"tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts",
"tests/boundary/filesystem/setupUnavailableProvider.test.ts",
"tests/boundary/filesystem/windowsNpxRegistration.test.ts",
"tests/boundary/filesystem/webScriptManifestPaths.test.ts",
"tests/boundary/process/ProcessEvidence.host.test.ts",
"tests/acceptance/webModuleCli.test.ts",
"tests/acceptance/webSourceLocationCli.test.ts",
"tests/boundary/cli/referenceSourceImportStatus.test.ts",
"tests/boundary/filesystem/asarInventoryFreshness.test.ts",
"tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts",
"tests/conformance/providers/ghidra/windowsCapabilities.test.ts",
"src/ghidra/GhidraInstallation.test.ts",
"tests/boundary/providers/ghidra/ghidraTransport.test.ts",
"tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts",
"tests/boundary/providers/ghidra/ghidraLauncher.test.ts",
"tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts",
"src/windows/WindowsNativeLoader.test.ts",
"src/windows/WindowsOwnedProcess.test.ts",
"src/ghidra/GhidraLauncherEnvironment.test.ts",
"src/process/ProcessOwnership.test.ts",
"src/process/ProcessOwnership.part2.test.ts",
"src/process/ProcessOwnership.part3.test.ts",
"src/process/ProcessOwnership.validation.test.ts",
"src/application/Update.test.ts",
"tests/process-global/updateMaintenance.test.ts",
"tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts",
"tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts",
"tests/boundary/filesystem/javascriptPackagePrecedence.test.ts",
"tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts",
"tests/boundary/process/sourceMapUncHosts.test.ts",
],
browser: [
".github/workflows/real-browser.yml",
"scripts/verify-real-browser.mjs",
@ -227,3 +309,23 @@ export const SCOPES = {
".github/workflows/website-pages.yml",
],
};
// Verifier entrypoints that only manual, licensed, or self-hosted workflows
// run. No selected CI lane executes them, so their edits need only the static
// checks and the source-test suite. Shared helpers under scripts/lib remain
// broad inputs.
export const MANUAL_VERIFIERS = [
"scripts/verify-real-electron.mjs",
"scripts/verify-real-ghidra*.mjs",
"scripts/verify-real-hopper*.mjs",
"scripts/verify-real-ida.mjs",
"scripts/verify-*-client.mjs",
"scripts/verify-native-agent-client.mjs",
"scripts/verify-jeb.mjs",
"scripts/verify-apktool.mjs",
"scripts/verify-adb.mjs",
"scripts/verify-agent-experience.mjs",
"scripts/verify-packaged-ghidra-windows.mjs",
"scripts/verify-wasm-artifact.mjs",
"scripts/verify-packaged-wasm-artifact.mjs",
];

View File

@ -6,7 +6,7 @@ import {
rename,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { dirname, join, matchesGlob } from "node:path";
import { afterAll, beforeAll, expect, it } from "vitest";
import { parse } from "yaml";
@ -157,7 +157,7 @@ it("retains complete checks when implementation is renamed into documentation",
const head = await commit(directory);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
package: true,
runtime: true,
});
});
@ -268,6 +268,54 @@ it.each([
{ code: false, evm: true, browser: false, package: false },
],
["unknown-input", { code: true, package: true, evm: true, browser: true }],
[
"src/application/Example.ts",
{
code: true,
runtime: true,
package: false,
apple: false,
evm: false,
browser: false,
},
],
[
"bridge/ghidra/Example.java",
{ code: true, runtime: true, package: false, apple: false },
],
[
"src/native/AppleExample.ts",
{ code: true, runtime: true, apple: true, package: false, evm: false },
],
[
"scripts/verify/apple/example.mjs",
{ code: true, apple: true, package: false, runtime: false },
],
[
"src/windows/Example.ts",
{ code: true, runtime: true, package: true, apple: false, evm: false },
],
["install.sh", { code: true, runtime: true, package: true, evm: false }],
[
".agents/skills/reverse-engineer-anything/SKILL.md",
{ code: true, runtime: true, docs: true, package: false, evm: false },
],
[
"scripts/verify-real-ghidra.mjs",
{ code: true, runtime: false, package: false, apple: false, evm: false },
],
[
"scripts/verify-claude-client.mjs",
{ code: true, runtime: false, package: false, browser: false },
],
[
"tests/boundary/process/darwinProcessRunTokenReader.test.ts",
{ code: true, apple: true, package: false, runtime: false },
],
[
"src/process/ProcessOwnership.test.ts",
{ code: true, package: true, apple: false, evm: false, browser: false },
],
])("selects the appropriate lanes for %s", async (path, expected) => {
const { directory, base } = await fixture();
const head = await change(directory, [path]);
@ -404,6 +452,14 @@ it.each([
{ code: true, package: true, browser: true, evm: true },
],
["docs/.vitepress/config.ts", { code: false, docs: true, package: false }],
["CHANGELOG.md", { code: false, docs: true, package: false }],
["SECURITY.md", { code: false, docs: true, package: false }],
[".github/pull_request_template.md", { code: false, docs: true }],
[
".github/ISSUE_TEMPLATE/bug.yml",
{ code: false, docs: true, workflows: false },
],
["native/windows/README.md", { code: false, docs: true, package: false }],
[
"src/contracts/evm/tools.ts",
{ code: true, docs: true, evm: true, package: false },
@ -462,6 +518,43 @@ it("fails without emitting a plan when package metadata is malformed", async ()
expect(await readFile(join(directory, "scope-output"), "utf8")).toBe("");
});
it.each([
[
"verify:keyed-archive",
"npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
{ apple: true, package: false, evm: false, browser: false },
],
[
"verify:inspector",
"npm run build:cached && node scripts/verify/inspector/runtime-observation.mjs",
{ package: true, apple: false, evm: false, browser: false },
],
[
"verify:windows-native",
"node scripts/verify-windows-native.mjs",
{ package: true, apple: false, evm: false, browser: false },
],
])(
"routes the %s script to its glob-owned lane",
async (script, command, expected) => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: command } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: `${command} --verbose` } }),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
...expected,
});
},
);
it("retains a broad fallback when a provider script adds shared shell work", async () => {
const { directory } = await fixture();
const before = {
@ -538,3 +631,119 @@ it.each(["compile:termux", "build:termux", "build:termux:run"])(
});
},
);
const INTEL_HOST = {
os: "macos-15-intel",
platform: "darwin",
architecture: "x64",
};
it("keeps macOS x64 package verification for ci:full and non-PR events", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["src/windows/Example.ts"]);
const matrix = z.object({ include: z.array(z.unknown()) });
const hosts = async (event: string, full: boolean) =>
matrix.parse((await classify(directory, base, head, event, full)).matrix)
.include;
const pullRequest = await hosts("pull_request", false);
expect(pullRequest).toHaveLength(3);
expect(pullRequest).not.toContainEqual(INTEL_HOST);
expect(await hosts("pull_request", true)).toContainEqual(INTEL_HOST);
for (const event of ["push", "schedule", "workflow_dispatch"])
expect(await hosts(event, false)).toContainEqual(INTEL_HOST);
});
const workflowFile = async (name: string): Promise<unknown> =>
parse(
await readFile(
new URL(`../../../.github/workflows/${name}`, import.meta.url),
"utf8",
),
);
const scopesSchema = z.object({
SCOPES: z.record(z.string(), z.array(z.string())),
MANUAL_VERIFIERS: z.array(z.string()),
});
const loadScopes = async () =>
scopesSchema.parse(
await import(
new URL("../../../scripts/ci/scopes.mjs", import.meta.url).href
),
);
const stepRuns = z.object({
on: z.unknown(),
jobs: z.record(
z.string(),
z.object({
steps: z.array(z.object({ run: z.string().optional() })).optional(),
}),
),
});
it("owns every explicit native-host test path with the lane that runs it", async () => {
const { SCOPES } = await loadScopes();
const ci = stepRuns.parse(await workflowFile("ci.yml"));
const listed = (job: string) =>
(ci.jobs[job]?.steps ?? [])
.flatMap((step) => step.run?.split(/\s+/) ?? [])
.filter((token) => token.endsWith(".test.ts"));
const unowned = (job: string, scope: string) =>
listed(job).filter(
(path) =>
!(SCOPES[scope] ?? []).some((pattern) => matchesGlob(path, pattern)),
);
expect(listed("apple-artifacts").length).toBeGreaterThan(0);
expect(listed("windows-curated").length).toBeGreaterThan(0);
expect(unowned("apple-artifacts", "apple")).toEqual([]);
expect(unowned("windows-curated", "package")).toEqual([]);
});
it("never runs a manual-only verifier from an automatic workflow", async () => {
const { MANUAL_VERIFIERS } = await loadScopes();
const scripts = z
.object({ scripts: z.record(z.string(), z.string()) })
.parse(
JSON.parse(
await readFile(
new URL("../../../package.json", import.meta.url),
"utf8",
),
),
).scripts;
const directory = new URL("../../../.github/workflows/", import.meta.url);
const commands: string[] = [];
for (const name of await readdir(directory)) {
const workflow = stepRuns.parse(await workflowFile(name));
const triggers = JSON.stringify(workflow.on);
if (
name !== "ci.yml" &&
!triggers.includes("workflow_call") &&
!triggers.includes("pull_request")
)
continue;
for (const job of Object.values(workflow.jobs))
for (const step of job.steps ?? []) if (step.run) commands.push(step.run);
}
const files = new Set<string>();
const seen = new Set<string>();
while (commands.length) {
const command = commands.pop() ?? "";
for (const [, file] of command.matchAll(/\b(scripts\/[\w./-]+\.mjs)\b/g))
if (file) files.add(file);
for (const [, script] of command.matchAll(/npm run ([\w:.-]+)/g)) {
if (!script || seen.has(script)) continue;
seen.add(script);
const body = scripts[script];
if (body) commands.push(body);
}
}
expect(files.size).toBeGreaterThan(0);
expect(
[...files].filter((file) =>
MANUAL_VERIFIERS.some((pattern) => matchesGlob(file, pattern)),
),
).toEqual([]);
});