426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
622 lines
18 KiB
JavaScript
622 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
// Drives repo automation ci path gate with explicit CLI and CI behavior.
|
|
import { spawnSync } from "node:child_process";
|
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
|
|
const CONFIGS = {
|
|
test: {
|
|
title: "Tests path gate",
|
|
outputs: ["server", "client", "plugins", "desktop", "zero_key", "cloud"],
|
|
labels: {
|
|
"ci:full": [
|
|
"server",
|
|
"client",
|
|
"plugins",
|
|
"desktop",
|
|
"zero_key",
|
|
"cloud",
|
|
],
|
|
"ci:server": ["server"],
|
|
"ci:client": ["client"],
|
|
"ci:plugins": ["plugins"],
|
|
"ci:desktop": ["desktop"],
|
|
"ci:e2e": ["zero_key"],
|
|
"ci:zero-key": ["zero_key"],
|
|
"ci:cloud": ["cloud"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["server", "client", "plugins", "desktop", "zero_key", "cloud"],
|
|
patterns: [
|
|
"package.json",
|
|
"bun.lock",
|
|
"turbo.json",
|
|
"tsconfig*.json",
|
|
"vitest.config.*",
|
|
],
|
|
reason: "workspace toolchain",
|
|
},
|
|
{
|
|
lanes: ["server", "client", "plugins", "desktop", "zero_key", "cloud"],
|
|
patterns: [
|
|
".github/workflows/test.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
],
|
|
reason: "test workflow or shared CI setup",
|
|
},
|
|
{
|
|
lanes: ["server", "client", "plugins", "desktop", "zero_key"],
|
|
patterns: [
|
|
"packages/core/**",
|
|
"packages/agent/**",
|
|
"packages/shared/**",
|
|
"packages/prompts/**",
|
|
"packages/test/**",
|
|
],
|
|
reason: "shared runtime surface",
|
|
},
|
|
{
|
|
lanes: ["client", "zero_key"],
|
|
patterns: ["packages/app/**", "packages/ui/**"],
|
|
reason: "app or shared UI",
|
|
},
|
|
{
|
|
lanes: ["server", "client", "desktop", "zero_key"],
|
|
patterns: ["packages/app-core/**"],
|
|
reason: "app-core host or desktop bridge",
|
|
},
|
|
{
|
|
lanes: ["server", "zero_key"],
|
|
patterns: ["packages/scenario-runner/**", "packages/scripts/**"],
|
|
reason: "scenario runner or repo scripts",
|
|
},
|
|
{
|
|
lanes: ["server", "client", "zero_key", "cloud"],
|
|
patterns: ["packages/cloud/sdk/**", "packages/cloud/shared/**"],
|
|
reason: "cloud surface",
|
|
},
|
|
{
|
|
lanes: ["server"],
|
|
patterns: ["packages/security/**", "packages/vault/**"],
|
|
reason: "security or vault package",
|
|
},
|
|
{
|
|
lanes: ["server"],
|
|
patterns: ["packages/elizaos/**", "packages/skills/**"],
|
|
reason:
|
|
"elizaos CLI or runtime skills - test:server runs these suites (previously unmapped: a CLI/skills-only PR skipped every test lane)",
|
|
},
|
|
{
|
|
lanes: ["plugins", "zero_key"],
|
|
patterns: ["plugins/**"],
|
|
reason: "plugin surface",
|
|
},
|
|
],
|
|
// Fail-safe: a PR that changes real code/test surface (packages/** or
|
|
// plugins/**) but matches no rule above must NOT skip every lane and pass
|
|
// green. Any such orphan path runs the `server` lane as a minimal safe net
|
|
// (it builds core + runs the core/agent/shared suite). Pure docs/marketing
|
|
// surfaces are exempted so they can still skip cleanly.
|
|
failSafe: {
|
|
lanes: ["server"],
|
|
codeRoots: ["packages/**", "plugins/**"],
|
|
ignore: [
|
|
"packages/docs/**",
|
|
"packages/homepage/**",
|
|
"packages/cloud/docs-redirect/**",
|
|
],
|
|
reason:
|
|
"unmapped code path - no lane rule matched; running the server lane as a fail-safe so a code change can never skip every test lane",
|
|
},
|
|
},
|
|
"scenario-pr": {
|
|
title: "Scenario PR E2E path gate",
|
|
outputs: ["run_scenario_pr"],
|
|
labels: {
|
|
"ci:full": ["run_scenario_pr"],
|
|
"ci:e2e": ["run_scenario_pr"],
|
|
"ci:scenario": ["run_scenario_pr"],
|
|
"ci:zero-key": ["run_scenario_pr"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["run_scenario_pr"],
|
|
patterns: [
|
|
"package.json",
|
|
"bun.lock",
|
|
"turbo.json",
|
|
"tsconfig*.json",
|
|
"vite.config.*",
|
|
"vitest.config.*",
|
|
],
|
|
reason: "workspace toolchain",
|
|
},
|
|
{
|
|
lanes: ["run_scenario_pr"],
|
|
patterns: [
|
|
".github/workflows/scenario-pr.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
],
|
|
reason: "scenario workflow or shared CI setup",
|
|
},
|
|
{
|
|
lanes: ["run_scenario_pr"],
|
|
patterns: [
|
|
"packages/app/**",
|
|
"packages/ui/**",
|
|
"packages/app-core/**",
|
|
"packages/scenario-runner/**",
|
|
"packages/core/**",
|
|
"packages/agent/**",
|
|
"packages/shared/**",
|
|
"packages/scripts/**",
|
|
"packages/test/**",
|
|
"packages/prompts/**",
|
|
],
|
|
reason: "scenario runtime, UI, or support package",
|
|
},
|
|
{
|
|
lanes: ["run_scenario_pr"],
|
|
patterns: [
|
|
"plugins/plugin-app-control/**",
|
|
"plugins/plugin-computeruse/**",
|
|
"plugins/plugin-github/**",
|
|
],
|
|
reason: "scenario-critical plugin",
|
|
},
|
|
{
|
|
lanes: ["run_scenario_pr"],
|
|
patterns: [
|
|
"plugins/plugin-*/src/**",
|
|
"plugins/plugin-*/package.json",
|
|
"plugins/plugin-*/vite.config.*",
|
|
"plugins/plugin-*/vitest.config.*",
|
|
],
|
|
reason: "plugin implementation surface",
|
|
},
|
|
],
|
|
},
|
|
docker: {
|
|
title: "Docker smoke path gate",
|
|
outputs: ["docker"],
|
|
labels: {
|
|
"ci:full": ["docker"],
|
|
"ci:docker": ["docker"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["docker"],
|
|
patterns: [
|
|
".github/workflows/docker-ci-smoke.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
"package.json",
|
|
"bun.lock",
|
|
"bunfig.toml",
|
|
"turbo.json",
|
|
"packages/app-core/deploy/**",
|
|
"packages/app-core/scripts/docker-ci-smoke.sh",
|
|
"packages/app-core/scripts/docker-healthcheck.mjs",
|
|
],
|
|
reason: "Docker smoke workflow or image contract",
|
|
},
|
|
{
|
|
lanes: ["docker"],
|
|
patterns: [
|
|
"packages/app-core/**",
|
|
"packages/agent/**",
|
|
"packages/core/**",
|
|
"packages/shared/**",
|
|
"packages/prompts/**",
|
|
"plugins/**",
|
|
],
|
|
reason: "runtime included in production image",
|
|
},
|
|
],
|
|
},
|
|
mobile: {
|
|
title: "Mobile smoke path gate",
|
|
outputs: ["ios", "android"],
|
|
labels: {
|
|
"ci:full": ["ios", "android"],
|
|
"ci:mobile": ["ios", "android"],
|
|
"ci:ios": ["ios"],
|
|
"ci:android": ["android"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["ios", "android"],
|
|
patterns: [
|
|
".github/workflows/mobile-build-smoke.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
"package.json",
|
|
"bun.lock",
|
|
"packages/agent/**",
|
|
"packages/app/**",
|
|
"packages/app-core/**",
|
|
"packages/core/**",
|
|
"packages/native/plugins/**",
|
|
"packages/shared/**",
|
|
"plugins/plugin-sql/**",
|
|
],
|
|
reason: "mobile app or runtime dependency",
|
|
},
|
|
{
|
|
lanes: ["ios"],
|
|
patterns: ["packages/app/ios/**", "packages/app-core/platforms/ios/**"],
|
|
reason: "iOS native surface",
|
|
},
|
|
{
|
|
lanes: ["android"],
|
|
patterns: [
|
|
"packages/app/android/**",
|
|
"packages/app-core/platforms/android/**",
|
|
],
|
|
reason: "Android native surface",
|
|
},
|
|
],
|
|
},
|
|
"dev-smoke": {
|
|
title: "Dev smoke path gate",
|
|
outputs: ["dev_smoke"],
|
|
labels: {
|
|
"ci:full": ["dev_smoke"],
|
|
"ci:dev-smoke": ["dev_smoke"],
|
|
"ci:e2e": ["dev_smoke"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["dev_smoke"],
|
|
patterns: [
|
|
".github/workflows/dev-smoke.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
"package.json",
|
|
"bun.lock",
|
|
"packages/app/**",
|
|
"packages/app-core/**",
|
|
"packages/core/**",
|
|
"packages/shared/**",
|
|
"packages/ui/**",
|
|
],
|
|
reason: "dev server or onboarding chat surface",
|
|
},
|
|
],
|
|
},
|
|
"windows-dev": {
|
|
title: "Windows dev smoke path gate",
|
|
outputs: ["windows_dev"],
|
|
labels: {
|
|
"ci:full": ["windows_dev"],
|
|
"ci:windows": ["windows_dev"],
|
|
"ci:desktop": ["windows_dev"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["windows_dev"],
|
|
patterns: [
|
|
".github/workflows/windows-dev-smoke.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
"package.json",
|
|
"bun.lock",
|
|
"packages/app-core/scripts/**",
|
|
"packages/app-core/platforms/electrobun/**",
|
|
"packages/app/**",
|
|
],
|
|
reason: "Windows dev bootstrap surface",
|
|
},
|
|
],
|
|
},
|
|
"desktop-preload": {
|
|
title: "Windows desktop preload path gate",
|
|
outputs: ["desktop_preload"],
|
|
labels: {
|
|
"ci:full": ["desktop_preload"],
|
|
"ci:windows": ["desktop_preload"],
|
|
"ci:desktop": ["desktop_preload"],
|
|
},
|
|
rules: [
|
|
{
|
|
lanes: ["desktop_preload"],
|
|
patterns: [
|
|
".github/workflows/windows-desktop-preload-smoke.yml",
|
|
".github/actions/setup-bun-workspace/**",
|
|
"packages/scripts/ci-path-gate.mjs",
|
|
"package.json",
|
|
"bun.lock",
|
|
"packages/app-core/scripts/**",
|
|
"packages/app-core/platforms/electrobun/**",
|
|
"packages/app/**",
|
|
],
|
|
reason: "desktop preload or Electrobun surface",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (!arg.startsWith("--")) {
|
|
throw new Error(`unexpected argument: ${arg}`);
|
|
}
|
|
const key = arg.slice(2);
|
|
const next = argv[index + 1];
|
|
if (next === undefined || next.startsWith("--")) {
|
|
args[key] = "true";
|
|
} else {
|
|
args[key] = next;
|
|
index += 1;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function globToRegExp(pattern) {
|
|
const sentinel = "\0";
|
|
const escaped = pattern
|
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
.replace(/\*\*/g, sentinel)
|
|
.replace(/\*/g, "[^/]*")
|
|
.replaceAll(sentinel, ".*");
|
|
return new RegExp(`^${escaped}$`);
|
|
}
|
|
|
|
const patternCache = new Map();
|
|
function matches(pattern, path) {
|
|
if (!patternCache.has(pattern)) {
|
|
patternCache.set(pattern, globToRegExp(pattern));
|
|
}
|
|
return patternCache.get(pattern).test(path);
|
|
}
|
|
|
|
// Diffs from the merge-base of base..head, not from the base TIP. The base
|
|
// SHA the workflow passes is `pull_request.base.sha` — the base branch's tip,
|
|
// which advances as other PRs merge. A plain two-dot `git diff base head`
|
|
// would charge a PR that trails its base branch with every develop-side file
|
|
// it never touched, over-triggering heavy lanes (including the fail-safe
|
|
// lanes) for unrelated changes (#16125). The merge-base diff is exactly what
|
|
// the GitHub "Files changed" tab shows. Callers check out with fetch-depth: 0
|
|
// so the merge-base is resolvable; if it is not (bad fetch depth, unrelated
|
|
// histories), fail loud rather than silently diffing the entire tree.
|
|
export function gitChangedFiles(base, head, cwd) {
|
|
const mergeBaseResult = spawnSync("git", ["merge-base", base, head], {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
const mergeBase = mergeBaseResult.stdout.trim();
|
|
if (mergeBaseResult.status !== 0 || !mergeBase) {
|
|
throw new Error(
|
|
`no merge-base between ${base} and ${head} — insufficient fetch depth or unrelated histories` +
|
|
(mergeBaseResult.stderr ? `: ${mergeBaseResult.stderr.trim()}` : ""),
|
|
);
|
|
}
|
|
const result = spawnSync("git", ["diff", "--name-only", mergeBase, head], {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr || `git diff exited with ${result.status}`);
|
|
}
|
|
return result.stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function addLane(matchesByLane, lane, source) {
|
|
if (!matchesByLane.has(lane)) {
|
|
matchesByLane.set(lane, []);
|
|
}
|
|
matchesByLane.get(lane).push(source);
|
|
}
|
|
|
|
function anyMatch(patterns, path) {
|
|
return patterns.some((pattern) => matches(pattern, path));
|
|
}
|
|
|
|
// Guarantees a PR that changes real code/test surface cannot skip every test
|
|
// lane and pass green. Any changed file under a `codeRoot` that matched no rule
|
|
// (and is not an explicitly-exempt docs/marketing path) enables the fail-safe
|
|
// lanes. Configs without a `failSafe` block are unaffected.
|
|
function applyFailSafe(config, changedFiles, matchedPaths, matchesByLane) {
|
|
const failSafe = config.failSafe;
|
|
if (!failSafe) {
|
|
return;
|
|
}
|
|
for (const path of changedFiles) {
|
|
if (matchedPaths.has(path)) {
|
|
continue;
|
|
}
|
|
if (!anyMatch(failSafe.codeRoots, path)) {
|
|
continue;
|
|
}
|
|
if (failSafe.ignore && anyMatch(failSafe.ignore, path)) {
|
|
continue;
|
|
}
|
|
for (const lane of failSafe.lanes) {
|
|
addLane(matchesByLane, lane, {
|
|
kind: "failsafe",
|
|
path,
|
|
reason: failSafe.reason,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function evaluate(config, { eventName, labels, base, head, changedFilesPath }) {
|
|
const matchesByLane = new Map(config.outputs.map((output) => [output, []]));
|
|
let changedFiles = [];
|
|
|
|
// PRs are the only place where this script narrows coverage by changed path.
|
|
// Push, scheduled, and manual runs stay broad because they protect branch
|
|
// health and release confidence after multiple PRs have composed.
|
|
if (eventName !== "pull_request") {
|
|
for (const lane of config.outputs) {
|
|
addLane(matchesByLane, lane, {
|
|
kind: "event",
|
|
reason: `${eventName} runs this lane by default`,
|
|
});
|
|
}
|
|
return { changedFiles, matchesByLane };
|
|
}
|
|
|
|
changedFiles = changedFilesPath
|
|
? readFileSync(changedFilesPath, "utf8")
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
: gitChangedFiles(base, head);
|
|
const labelSet = new Set(
|
|
labels
|
|
.split(",")
|
|
.map((label) => label.trim())
|
|
.filter(Boolean),
|
|
);
|
|
|
|
for (const label of labelSet) {
|
|
for (const lane of config.labels[label] || []) {
|
|
addLane(matchesByLane, lane, {
|
|
kind: "label",
|
|
label,
|
|
reason: `forced by ${label}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
const matchedPaths = new Set();
|
|
for (const path of changedFiles) {
|
|
for (const rule of config.rules) {
|
|
const pattern = rule.patterns.find((candidate) =>
|
|
matches(candidate, path),
|
|
);
|
|
if (!pattern) {
|
|
continue;
|
|
}
|
|
matchedPaths.add(path);
|
|
for (const lane of rule.lanes) {
|
|
addLane(matchesByLane, lane, {
|
|
kind: "path",
|
|
path,
|
|
pattern,
|
|
reason: rule.reason,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
applyFailSafe(config, changedFiles, matchedPaths, matchesByLane);
|
|
|
|
return { changedFiles, matchesByLane };
|
|
}
|
|
|
|
function markdown(config, { changedFiles, matchesByLane, labels, eventName }) {
|
|
const lines = [
|
|
`### ${config.title}`,
|
|
"",
|
|
`- Event: \`${eventName}\``,
|
|
`- Force labels: \`${labels || "(none)"}\``,
|
|
"",
|
|
"| Lane | Run | Why |",
|
|
"| --- | --- | --- |",
|
|
];
|
|
|
|
for (const lane of config.outputs) {
|
|
const sources = matchesByLane.get(lane) || [];
|
|
const reasons = sources.length
|
|
? sources
|
|
.slice(0, 6)
|
|
.map((source) => {
|
|
if (source.kind === "path") {
|
|
return `\`${source.path}\` matched \`${source.pattern}\` (${source.reason})`;
|
|
}
|
|
if (source.kind === "failsafe") {
|
|
return `\`${source.path}\` unmapped -> fail-safe (${source.reason})`;
|
|
}
|
|
if (source.kind === "label") {
|
|
return source.reason;
|
|
}
|
|
return source.reason;
|
|
})
|
|
.join("<br>")
|
|
: "No matching paths or force labels.";
|
|
lines.push(`| \`${lane}\` | \`${sources.length > 0}\` | ${reasons} |`);
|
|
}
|
|
|
|
if (changedFiles.length > 0) {
|
|
lines.push("", "<details><summary>Changed files</summary>", "");
|
|
for (const path of changedFiles) {
|
|
lines.push(`- \`${path}\``);
|
|
}
|
|
lines.push("", "</details>");
|
|
}
|
|
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
function writeOutputs(path, config, matchesByLane) {
|
|
const body = config.outputs
|
|
.map((lane) => `${lane}=${(matchesByLane.get(lane) || []).length > 0}`)
|
|
.join("\n");
|
|
if (path) {
|
|
appendFileSync(path, `${body}\n`);
|
|
} else {
|
|
console.log(body);
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const configName = args.config;
|
|
const config = CONFIGS[configName];
|
|
if (!config) {
|
|
throw new Error(
|
|
`unknown config '${configName}'. Expected one of: ${Object.keys(CONFIGS).join(", ")}`,
|
|
);
|
|
}
|
|
|
|
const eventName =
|
|
args.event || process.env.GITHUB_EVENT_NAME || "pull_request";
|
|
const labels = args.labels || "";
|
|
const base = args.base || "";
|
|
const head = args.head || "";
|
|
const changedFilesPath = args["changed-files"] || "";
|
|
|
|
if (eventName === "pull_request" && !changedFilesPath && (!base || !head)) {
|
|
throw new Error(
|
|
"--base and --head are required for pull_request events unless --changed-files is provided",
|
|
);
|
|
}
|
|
|
|
const result = evaluate(config, {
|
|
eventName,
|
|
labels,
|
|
base,
|
|
head,
|
|
changedFilesPath,
|
|
});
|
|
writeOutputs(
|
|
args.output || process.env.GITHUB_OUTPUT,
|
|
config,
|
|
result.matchesByLane,
|
|
);
|
|
|
|
const summary = markdown(config, { ...result, labels, eventName });
|
|
if (args.summary || process.env.GITHUB_STEP_SUMMARY) {
|
|
appendFileSync(args.summary || process.env.GITHUB_STEP_SUMMARY, summary);
|
|
} else {
|
|
console.log(summary);
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
main();
|
|
}
|