mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(ci): centralize changed-path facts (#104897)
This commit is contained in:
+17
-36
@@ -2,18 +2,15 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
||||
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
|
||||
export { normalizeChangedPath } from "./lib/changed-path-facts.mjs";
|
||||
|
||||
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
|
||||
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
|
||||
const RAW_SYNC_CHANGED_LANES_ENV = "OPENCLAW_CHANGED_LANES_RAW_SYNC";
|
||||
|
||||
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
|
||||
const APP_PATH_RE = /^(?:apps\/|Swabble\/|appcast\.xml$)/u;
|
||||
const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u;
|
||||
const CORE_PATH_RE = /^(?:src\/|packages\/)/u;
|
||||
const UI_PATH_RE = /^(?:ui\/|tsconfig\.ui\.json$)/u;
|
||||
const SCRIPTS_TYPECHECK_PATH_RE =
|
||||
/^(?:scripts\/.*\.(?:[cm]?ts|[cm]?tsx)|tsconfig\.scripts\.json)$/u;
|
||||
// Keep aligned with tsconfig.strict-ratchet.json includes and its oxlint override.
|
||||
@@ -38,11 +35,6 @@ export const STRICT_RATCHET_PACKAGE_DIRS = [
|
||||
];
|
||||
const TEST_ROOT_TYPECHECK_PATH_RE =
|
||||
/^(?:test\/(?!fixtures\/).*\.(?:[cm]?ts|[cm]?tsx)|test\/tsconfig\/tsconfig\.test\.root\.json)$/u;
|
||||
const TOOLING_PATH_RE =
|
||||
/^(?:scripts\/|test\/vitest\/|\.github\/|\.vscode\/|config\/|deploy\/|git-hooks\/|Dockerfile\.sandbox(?:-(?:browser|common))?$|Makefile$|docker-setup\.sh$|setup-podman\.sh$|openclaw\.podman\.env$|skills\/pyproject\.toml$|vitest(?:\..+)?\.config\.ts$|tsconfig.*\.json$|\.dockerignore$|\.gitignore$|\.jscpd\.json$|\.npmignore$|\.pre-commit-config\.yaml$|\.swiftformat$|\.swiftlint\.yml$|\.oxlint.*|\.oxfmt.*)/u;
|
||||
const ROOT_GLOBAL_PATH_RE =
|
||||
/^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u;
|
||||
const LEGACY_ROOT_ASSET_PATH_RE = /^assets\//u;
|
||||
export const LIVE_DOCKER_AUTH_SHELL_TARGETS = [
|
||||
"scripts/lib/live-docker-auth.sh",
|
||||
"scripts/test-live-acp-bind-docker.sh",
|
||||
@@ -59,8 +51,6 @@ const LIVE_DOCKER_TOOLING_PATHS = new Set([
|
||||
"src/gateway/live-agent-probes.test.ts",
|
||||
]);
|
||||
const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u;
|
||||
const TEST_PATH_RE =
|
||||
/(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u;
|
||||
const PUBLIC_EXTENSION_CONTRACT_RE =
|
||||
/^(?:src\/plugin-sdk\/|src\/plugins\/contracts\/|src\/channels\/plugins\/|scripts\/lib\/plugin-sdk-entrypoints\.json$|scripts\/sync-plugin-sdk-exports\.mjs$|scripts\/generate-plugin-sdk-api-baseline\.ts$)/u;
|
||||
/**
|
||||
@@ -91,16 +81,6 @@ export const RELEASE_METADATA_PATHS = new Set([
|
||||
* }} ChangedLaneResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalizes a changed file path into repo-relative POSIX form.
|
||||
*/
|
||||
export function normalizeChangedPath(inputPath) {
|
||||
return String(inputPath ?? "")
|
||||
.trim()
|
||||
.replaceAll("\\", "/")
|
||||
.replace(/^\.\/+/u, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the default changed-lanes result object.
|
||||
*/
|
||||
@@ -124,7 +104,7 @@ export function createEmptyChangedLanes() {
|
||||
}
|
||||
|
||||
export function isChangedLaneTestPath(changedPath) {
|
||||
return TEST_PATH_RE.test(normalizeChangedPath(changedPath));
|
||||
return getChangedPathFacts(normalizeChangedPath(changedPath)).isChangedLaneTest;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +140,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
paths.every((changedPath) => RELEASE_METADATA_PATHS.has(changedPath))
|
||||
) {
|
||||
lanes.releaseMetadata = true;
|
||||
lanes.docs = paths.some((changedPath) => DOCS_PATH_RE.test(changedPath));
|
||||
lanes.docs = paths.some((changedPath) => getChangedPathFacts(changedPath).surface === "docs");
|
||||
for (const changedPath of paths) {
|
||||
reasons.push(`${changedPath}: release metadata`);
|
||||
}
|
||||
@@ -168,6 +148,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
}
|
||||
|
||||
for (const changedPath of paths) {
|
||||
const facts = getChangedPathFacts(changedPath);
|
||||
if (SCRIPTS_TYPECHECK_PATH_RE.test(changedPath)) {
|
||||
lanes.scripts = true;
|
||||
}
|
||||
@@ -183,7 +164,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
lanes.testRoot = true;
|
||||
}
|
||||
|
||||
if (DOCS_PATH_RE.test(changedPath)) {
|
||||
if (facts.surface === "docs") {
|
||||
lanes.docs = true;
|
||||
continue;
|
||||
}
|
||||
@@ -208,7 +189,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ROOT_GLOBAL_PATH_RE.test(changedPath)) {
|
||||
if (facts.surface === "rootGlobal") {
|
||||
lanes.all = true;
|
||||
extensionImpactFromCore = true;
|
||||
reasons.push(`${changedPath}: root config/package surface`);
|
||||
@@ -225,8 +206,8 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EXTENSION_PATH_RE.test(changedPath)) {
|
||||
if (isChangedLaneTestPath(changedPath)) {
|
||||
if (facts.surface === "extension") {
|
||||
if (facts.isChangedLaneTest) {
|
||||
lanes.extensionTests = true;
|
||||
reasons.push(`${changedPath}: extension test`);
|
||||
} else {
|
||||
@@ -237,8 +218,8 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CORE_PATH_RE.test(changedPath)) {
|
||||
if (isChangedLaneTestPath(changedPath)) {
|
||||
if (facts.surface === "source" || facts.surface === "package") {
|
||||
if (facts.isChangedLaneTest) {
|
||||
lanes.coreTests = true;
|
||||
reasons.push(`${changedPath}: core test`);
|
||||
} else {
|
||||
@@ -249,8 +230,8 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (UI_PATH_RE.test(changedPath)) {
|
||||
if (isChangedLaneTestPath(changedPath)) {
|
||||
if (facts.surface === "ui") {
|
||||
if (facts.isChangedLaneTest) {
|
||||
lanes.coreTests = true;
|
||||
reasons.push(`${changedPath}: UI test`);
|
||||
} else {
|
||||
@@ -261,25 +242,25 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (APP_PATH_RE.test(changedPath)) {
|
||||
if (facts.surface === "app") {
|
||||
lanes.apps = true;
|
||||
reasons.push(`${changedPath}: app surface`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (changedPath.startsWith("test/") || changedPath.startsWith("test-fixtures/")) {
|
||||
if (facts.surface === "rootTest" || facts.surface === "testFixture") {
|
||||
lanes.tooling = true;
|
||||
reasons.push(`${changedPath}: root test/support surface`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TOOLING_PATH_RE.test(changedPath)) {
|
||||
if (facts.surface === "rootTooling") {
|
||||
lanes.tooling = true;
|
||||
reasons.push(`${changedPath}: tooling surface`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LEGACY_ROOT_ASSET_PATH_RE.test(changedPath)) {
|
||||
if (facts.surface === "legacyRootAsset") {
|
||||
lanes.tooling = true;
|
||||
reasons.push(`${changedPath}: legacy root asset cleanup`);
|
||||
continue;
|
||||
|
||||
@@ -14,13 +14,12 @@ import { performance } from "node:perf_hooks";
|
||||
import {
|
||||
LIVE_DOCKER_AUTH_SHELL_TARGETS,
|
||||
detectChangedLanesForPaths,
|
||||
isChangedLaneTestPath,
|
||||
listChangedPathsFromGit,
|
||||
listStagedChangedPaths,
|
||||
normalizeChangedPath,
|
||||
} from "./changed-lanes.mjs";
|
||||
import { shrinkwrapPackageDirsForChangedPaths } from "./generate-npm-shrinkwrap.mjs";
|
||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
||||
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
|
||||
import { printTimingSummary } from "./lib/check-timing-summary.mjs";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
import {
|
||||
@@ -220,7 +219,9 @@ export function shouldRunAppcastOwnerTest(paths) {
|
||||
}
|
||||
|
||||
export function shouldRunTestTempCreationReport(paths) {
|
||||
return paths.some((changedPath) => isChangedLaneTestPath(changedPath));
|
||||
return paths.some(
|
||||
(changedPath) => getChangedPathFacts(normalizeChangedPath(changedPath)).isChangedLaneTest,
|
||||
);
|
||||
}
|
||||
|
||||
export function createShrinkwrapGuardCommand(paths) {
|
||||
@@ -439,7 +440,7 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
}
|
||||
if (
|
||||
lanes.liveDockerTooling &&
|
||||
result.paths.some((changedPath) => changedPath.startsWith("src/"))
|
||||
result.paths.some((changedPath) => getChangedPathFacts(changedPath).surface === "source")
|
||||
) {
|
||||
addTypecheck("typecheck core tests", ["tsgo:core:test"]);
|
||||
addLint("lint core", ["lint:core"]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Determines CI scope from changed paths.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { getChangedPathFacts } from "./lib/changed-path-facts.mjs";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
|
||||
|
||||
@@ -32,7 +33,6 @@ const EMPTY_SCOPE = {
|
||||
runUiTests: false,
|
||||
};
|
||||
|
||||
const DOCS_PATH_RE = /^(docs\/|.*\.mdx?$)/;
|
||||
const SKILLS_PYTHON_SCOPE_RE = /^(skills\/|skills\/pyproject\.toml$)/;
|
||||
const INSTALL_SMOKE_WORKFLOW_SCOPE_RE = /^\.github\/workflows\/install-smoke\.yml$/;
|
||||
const NATIVE_PROTOCOL_GEN_RE = /^apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\//;
|
||||
@@ -52,16 +52,12 @@ const WINDOWS_TEST_SCOPE_RE =
|
||||
/^(src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/windows-install-roots\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|test\/scripts\/(?:format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/;
|
||||
const WINDOWS_DAEMON_SCOPE_RE =
|
||||
/^src\/daemon\/(?:schtasks(?:[-.][^/]+)?|runtime-hints\.windows-paths(?:\.test)?|test-helpers\/schtasks-(?:base-mocks|fixtures))\.ts$/;
|
||||
const TEST_ONLY_PATH_RE =
|
||||
/(^test\/|\/test\/|\/tests\/|(?:^|\/)[^/]+\.(?:test|spec|test-utils|test-support|test-harness|e2e-harness)\.[cm]?[jt]sx?$)/;
|
||||
const CONTROL_UI_I18N_SCOPE_RE =
|
||||
/^(ui\/src\/i18n\/|scripts\/control-ui-i18n\.ts$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
|
||||
const CONTROL_UI_TEST_SCOPE_RE =
|
||||
/^(ui\/|test\/vitest\/vitest\.shared\.config\.ts$|scripts\/ensure-playwright-chromium\.mjs$)/;
|
||||
const NATIVE_I18N_SCOPE_RE =
|
||||
/^(?:apps\/\.i18n\/|apps\/android\/app\/src\/main\/|apps\/ios\/|apps\/macos\/Sources\/|apps\/shared\/OpenClawKit\/Sources\/|scripts\/(?:android-app-i18n|apple-app-i18n|native-app-i18n)\.ts$|test\/scripts\/(?:android-app-i18n|apple-app-i18n|native-app-i18n)\.test\.ts$|\.github\/workflows\/(?:ci|native-app-locale-refresh)\.yml$)/;
|
||||
const NATIVE_ONLY_RE =
|
||||
/^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|apps\/swabble\/|Swabble\/|appcast\.xml$)/;
|
||||
const FAST_INSTALL_SMOKE_SCOPE_RE =
|
||||
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh)$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
|
||||
const FULL_INSTALL_SMOKE_SCOPE_RE =
|
||||
@@ -71,7 +67,7 @@ const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE =
|
||||
const NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE =
|
||||
/^src\/plugins\/contracts\/(?:inventory\/bundled-capability-metadata|registry|tts-contract-suites)\.ts$/;
|
||||
const NODE_FAST_CI_ROUTING_SCOPE_RE =
|
||||
/^(scripts\/(?:ci-changed-scope|check-changed|run-vitest|test-projects(?:\.test-support)?)\.mjs$|scripts\/test-projects\.test-support\.d\.mts$|src\/commands\/status\.scan-result\.test\.ts$|src\/scripts\/ci-changed-scope\.test\.ts$|test\/scripts\/(?:changed-lanes|run-vitest|test-projects)\.test\.ts$)/;
|
||||
/^(scripts\/(?:ci-changed-scope|check-changed|run-vitest|test-projects(?:\.test-support)?)\.mjs$|scripts\/(?:test-projects\.test-support|lib\/changed-path-facts)\.d\.mts$|scripts\/lib\/changed-path-facts\.mjs$|src\/commands\/status\.scan-result\.test\.ts$|src\/scripts\/ci-changed-scope\.test\.ts$|test\/scripts\/(?:changed-lanes|changed-path-facts|run-vitest|test-projects)\.test\.ts$)/;
|
||||
const NODE_FAST_SCOPE_RE = new RegExp(
|
||||
`${NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.source}|${NODE_FAST_CI_ROUTING_SCOPE_RE.source}`,
|
||||
);
|
||||
@@ -101,14 +97,15 @@ export function detectChangedScope(changedPaths) {
|
||||
let hasNonNativeNonDocs = false;
|
||||
|
||||
for (const rawPath of changedPaths) {
|
||||
const path = rawPath.trim();
|
||||
const facts = getChangedPathFacts(rawPath);
|
||||
const { path } = facts;
|
||||
if (!path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isAppleSwiftConfig = APPLE_SWIFT_CONFIG_RE.test(path);
|
||||
|
||||
if (DOCS_PATH_RE.test(path)) {
|
||||
if (facts.surface === "docs") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -143,9 +140,7 @@ export function detectChangedScope(changedPaths) {
|
||||
|
||||
if (
|
||||
(WINDOWS_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path)) &&
|
||||
(!TEST_ONLY_PATH_RE.test(path) ||
|
||||
WINDOWS_TEST_SCOPE_RE.test(path) ||
|
||||
WINDOWS_DAEMON_SCOPE_RE.test(path))
|
||||
(!facts.isTestOnly || WINDOWS_TEST_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path))
|
||||
) {
|
||||
runWindows = true;
|
||||
}
|
||||
@@ -162,7 +157,7 @@ export function detectChangedScope(changedPaths) {
|
||||
runUiTests = true;
|
||||
}
|
||||
|
||||
if (!NATIVE_ONLY_RE.test(path)) {
|
||||
if (!facts.isNativeOnly) {
|
||||
hasNonNativeNonDocs = true;
|
||||
}
|
||||
}
|
||||
@@ -209,8 +204,9 @@ export function detectNodeFastScope(changedPaths) {
|
||||
let runCiRouting = false;
|
||||
|
||||
for (const rawPath of changedPaths) {
|
||||
const path = rawPath.trim();
|
||||
if (!path || DOCS_PATH_RE.test(path)) {
|
||||
const facts = getChangedPathFacts(rawPath);
|
||||
const { path } = facts;
|
||||
if (!path || facts.surface === "docs") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -236,11 +232,12 @@ export function detectNodeFastScope(changedPaths) {
|
||||
* @returns {InstallSmokeScope}
|
||||
*/
|
||||
function detectInstallSmokeScopeForPath(path) {
|
||||
const facts = getChangedPathFacts(path);
|
||||
const runFullInstallSmoke = FULL_INSTALL_SMOKE_SCOPE_RE.test(path);
|
||||
const runFastInstallSmoke =
|
||||
runFullInstallSmoke ||
|
||||
FAST_INSTALL_SMOKE_SCOPE_RE.test(path) ||
|
||||
(FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path));
|
||||
(FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE.test(path) && !facts.isTestOnly);
|
||||
return { runFastInstallSmoke, runFullInstallSmoke };
|
||||
}
|
||||
|
||||
@@ -259,8 +256,9 @@ export function detectInstallSmokeScope(changedPaths) {
|
||||
let runFastInstallSmoke = false;
|
||||
let runFullInstallSmoke = false;
|
||||
for (const rawPath of changedPaths) {
|
||||
const path = rawPath.trim();
|
||||
if (!path || DOCS_PATH_RE.test(path)) {
|
||||
const facts = getChangedPathFacts(rawPath);
|
||||
const { path } = facts;
|
||||
if (!path || facts.surface === "docs") {
|
||||
continue;
|
||||
}
|
||||
const pathScope = detectInstallSmokeScopeForPath(path);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export type ChangedPathSurface =
|
||||
| "docs"
|
||||
| "source"
|
||||
| "package"
|
||||
| "ui"
|
||||
| "extension"
|
||||
| "app"
|
||||
| "rootTest"
|
||||
| "testFixture"
|
||||
| "rootTooling"
|
||||
| "rootGlobal"
|
||||
| "legacyRootAsset"
|
||||
| "unknown";
|
||||
|
||||
export type ChangedPathFacts = {
|
||||
path: string;
|
||||
surface: ChangedPathSurface;
|
||||
isChangedLaneTest: boolean;
|
||||
isTestOnly: boolean;
|
||||
isNativeOnly: boolean;
|
||||
};
|
||||
|
||||
export function normalizeChangedPath(inputPath: unknown): string;
|
||||
export function getChangedPathFacts(inputPath: unknown): ChangedPathFacts;
|
||||
@@ -0,0 +1,73 @@
|
||||
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
|
||||
const APP_PATH_RE = /^(?:apps\/|Swabble\/|appcast\.xml$)/u;
|
||||
const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u;
|
||||
const SOURCE_PATH_RE = /^src\//u;
|
||||
const PACKAGE_PATH_RE = /^packages\//u;
|
||||
const UI_PATH_RE = /^(?:ui\/|tsconfig\.ui\.json$)/u;
|
||||
const ROOT_TEST_PATH_RE = /^test\//u;
|
||||
const TEST_FIXTURE_PATH_RE = /^test-fixtures\//u;
|
||||
const ROOT_TOOLING_PATH_RE =
|
||||
/^(?:scripts\/|test\/vitest\/|\.github\/|\.vscode\/|config\/|deploy\/|git-hooks\/|Dockerfile\.sandbox(?:-(?:browser|common))?$|Makefile$|docker-setup\.sh$|setup-podman\.sh$|openclaw\.podman\.env$|skills\/pyproject\.toml$|vitest(?:\..+)?\.config\.ts$|tsconfig.*\.json$|\.dockerignore$|\.gitignore$|\.jscpd\.json$|\.npmignore$|\.pre-commit-config\.yaml$|\.swiftformat$|\.swiftlint\.yml$|\.oxlint.*|\.oxfmt.*)/u;
|
||||
const ROOT_GLOBAL_PATH_RE =
|
||||
/^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u;
|
||||
const LEGACY_ROOT_ASSET_PATH_RE = /^assets\//u;
|
||||
const CHANGED_LANE_TEST_PATH_RE =
|
||||
/(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u;
|
||||
const TEST_ONLY_PATH_RE =
|
||||
/(^test\/|\/test\/|\/tests\/|(?:^|\/)[^/]+\.(?:test|spec|test-utils|test-support|test-harness|e2e-harness)\.[cm]?[jt]sx?$)/u;
|
||||
const NATIVE_ONLY_PATH_RE =
|
||||
/^(?:apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|apps\/swabble\/|Swabble\/|appcast\.xml$)/u;
|
||||
|
||||
/**
|
||||
* @typedef {"docs" | "source" | "package" | "ui" | "extension" | "app" | "rootTest" | "testFixture" | "rootTooling" | "rootGlobal" | "legacyRootAsset" | "unknown"} ChangedPathSurface
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalizes a changed file path into repo-relative POSIX form.
|
||||
*/
|
||||
export function normalizeChangedPath(inputPath) {
|
||||
return String(inputPath ?? "")
|
||||
.trim()
|
||||
.replaceAll("\\", "/")
|
||||
.replace(/^\.\/+/u, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns shared path facts without imposing a caller's lane-selection policy.
|
||||
*/
|
||||
export function getChangedPathFacts(inputPath) {
|
||||
const path = String(inputPath ?? "").trim();
|
||||
let surface = /** @type {ChangedPathSurface} */ ("unknown");
|
||||
|
||||
if (DOCS_PATH_RE.test(path)) {
|
||||
surface = "docs";
|
||||
} else if (ROOT_GLOBAL_PATH_RE.test(path)) {
|
||||
surface = "rootGlobal";
|
||||
} else if (EXTENSION_PATH_RE.test(path)) {
|
||||
surface = "extension";
|
||||
} else if (SOURCE_PATH_RE.test(path)) {
|
||||
surface = "source";
|
||||
} else if (PACKAGE_PATH_RE.test(path)) {
|
||||
surface = "package";
|
||||
} else if (UI_PATH_RE.test(path)) {
|
||||
surface = "ui";
|
||||
} else if (APP_PATH_RE.test(path)) {
|
||||
surface = "app";
|
||||
} else if (ROOT_TEST_PATH_RE.test(path)) {
|
||||
surface = "rootTest";
|
||||
} else if (TEST_FIXTURE_PATH_RE.test(path)) {
|
||||
surface = "testFixture";
|
||||
} else if (ROOT_TOOLING_PATH_RE.test(path)) {
|
||||
surface = "rootTooling";
|
||||
} else if (LEGACY_ROOT_ASSET_PATH_RE.test(path)) {
|
||||
surface = "legacyRootAsset";
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
surface,
|
||||
isChangedLaneTest: CHANGED_LANE_TEST_PATH_RE.test(path),
|
||||
isTestOnly: TEST_ONLY_PATH_RE.test(path),
|
||||
isNativeOnly: NATIVE_ONLY_PATH_RE.test(path),
|
||||
};
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
detectChangedLanes,
|
||||
listChangedPathsFromGit as listChangedPathsFromGitSource,
|
||||
} from "./changed-lanes.mjs";
|
||||
import { getChangedPathFacts } from "./lib/changed-path-facts.mjs";
|
||||
import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-scheduling.mjs";
|
||||
import {
|
||||
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
|
||||
@@ -3469,7 +3470,13 @@ function isRoutableChangedTarget(changedPath) {
|
||||
if (changedPath.endsWith(".live.test.ts")) {
|
||||
return false;
|
||||
}
|
||||
return /^(?:src|test|extensions|ui|packages)(?:\/|$)/u.test(changedPath);
|
||||
const surface = getChangedPathFacts(changedPath).surface;
|
||||
return (
|
||||
["source", "package", "extension", "rootTest"].includes(surface) ||
|
||||
changedPath === "ui" ||
|
||||
changedPath.startsWith("ui/") ||
|
||||
["src", "test", "extensions", "packages"].includes(changedPath)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSiblingTestTarget(changedPath, cwd) {
|
||||
@@ -3523,7 +3530,15 @@ function resolvePreciseChangedTestTargets(changedPath, options) {
|
||||
if (options.skipImportGraph === true) {
|
||||
return null;
|
||||
}
|
||||
if (/^(?:src|test\/helpers|extensions|packages|ui\/src|ui\/config)\//u.test(changedPath)) {
|
||||
const facts = getChangedPathFacts(changedPath);
|
||||
if (
|
||||
facts.surface === "source" ||
|
||||
facts.surface === "package" ||
|
||||
facts.surface === "extension" ||
|
||||
changedPath.startsWith("test/helpers/") ||
|
||||
changedPath.startsWith("ui/src/") ||
|
||||
changedPath.startsWith("ui/config/")
|
||||
) {
|
||||
const affectedTests = resolveAffectedTestsFromImportGraph(changedPath, cwd, {
|
||||
forceFull: options.forceFullImportGraph === true,
|
||||
});
|
||||
@@ -3671,7 +3686,7 @@ function classifyTarget(arg, cwd) {
|
||||
if (relative === "extensions") {
|
||||
return "extensionFull";
|
||||
}
|
||||
if (relative.startsWith("extensions/")) {
|
||||
if (getChangedPathFacts(relative).surface === "extension") {
|
||||
const extensionRoot = relative.split("/").slice(0, 2).join("/");
|
||||
const splitChannelShard = resolveSplitChannelExtensionShard(extensionRoot);
|
||||
if (splitChannelShard) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getChangedPathFacts,
|
||||
normalizeChangedPath,
|
||||
} from "../../scripts/lib/changed-path-facts.mjs";
|
||||
|
||||
describe("changed path facts", () => {
|
||||
it("preserves the existing surface classifications", () => {
|
||||
const cases = [
|
||||
["docs/ci.md", "docs"],
|
||||
["README.md", "docs"],
|
||||
["src/config/defaults.ts", "source"],
|
||||
["packages/gateway-client/src/client.ts", "package"],
|
||||
["extensions/slack/src/index.ts", "extension"],
|
||||
["ui/src/app.ts", "ui"],
|
||||
["apps/macos/Sources/OpenClaw/AppDelegate.swift", "app"],
|
||||
["test/scripts/changed-lanes.test.ts", "rootTest"],
|
||||
["test-fixtures/sample.ts", "testFixture"],
|
||||
["scripts/check-changed.mjs", "rootTooling"],
|
||||
[".github/workflows/ci.yml", "rootTooling"],
|
||||
["package.json", "rootGlobal"],
|
||||
["assets/legacy.png", "legacyRootAsset"],
|
||||
[".crabbox.yaml", "unknown"],
|
||||
] as const;
|
||||
|
||||
for (const [changedPath, surface] of cases) {
|
||||
expect(getChangedPathFacts(changedPath).surface, changedPath).toBe(surface);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves test and native-only predicates independently from surfaces", () => {
|
||||
expect(getChangedPathFacts("extensions/slack/src/index.test.ts")).toMatchObject({
|
||||
surface: "extension",
|
||||
isChangedLaneTest: true,
|
||||
isTestOnly: true,
|
||||
isNativeOnly: false,
|
||||
});
|
||||
expect(getChangedPathFacts("test/helpers/fixture.ts")).toMatchObject({
|
||||
surface: "rootTest",
|
||||
isChangedLaneTest: true,
|
||||
isTestOnly: true,
|
||||
isNativeOnly: false,
|
||||
});
|
||||
expect(getChangedPathFacts("apps/shared/OpenClawKit/Sources/Foo.swift")).toMatchObject({
|
||||
surface: "app",
|
||||
isChangedLaneTest: false,
|
||||
isTestOnly: false,
|
||||
isNativeOnly: true,
|
||||
});
|
||||
expect(getChangedPathFacts("apps/web/index.ts")).toMatchObject({
|
||||
surface: "app",
|
||||
isNativeOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps normalization separate from classification", () => {
|
||||
expect(normalizeChangedPath(" .\\extensions\\slack\\src\\index.test.ts ")).toBe(
|
||||
"extensions/slack/src/index.test.ts",
|
||||
);
|
||||
expect(getChangedPathFacts("./src/config/defaults.ts").surface).toBe("unknown");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user