mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:28:38 +00:00
ci: move changed-scope logic into tested script
This commit is contained in:
66
.github/workflows/ci.yml
vendored
66
.github/workflows/ci.yml
vendored
@@ -57,71 +57,7 @@ jobs:
|
|||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
BASE="${{ github.event.pull_request.base.sha }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CHANGED="$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")"
|
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
||||||
if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then
|
|
||||||
# Fail-safe: run broad checks if detection fails.
|
|
||||||
echo "run_node=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "run_macos=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "run_android=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_node=false
|
|
||||||
run_macos=false
|
|
||||||
run_android=false
|
|
||||||
has_non_docs=false
|
|
||||||
has_non_native_non_docs=false
|
|
||||||
|
|
||||||
while IFS= read -r path; do
|
|
||||||
[ -z "$path" ] && continue
|
|
||||||
case "$path" in
|
|
||||||
docs/*|*.md|*.mdx)
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
has_non_docs=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
# Generated protocol models are already covered by protocol:check and
|
|
||||||
# should not force the full native macOS lane.
|
|
||||||
apps/macos/Sources/OpenClawProtocol/*|apps/shared/OpenClawKit/Sources/OpenClawProtocol/*)
|
|
||||||
;;
|
|
||||||
apps/macos/*|apps/ios/*|apps/shared/*|Swabble/*)
|
|
||||||
run_macos=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
apps/android/*|apps/shared/*)
|
|
||||||
run_android=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
src/*|test/*|extensions/*|packages/*|scripts/*|ui/*|.github/*|openclaw.mjs|package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig*.json|vitest*.ts|tsdown.config.ts|.oxlintrc.json|.oxfmtrc.jsonc)
|
|
||||||
run_node=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
apps/android/*|apps/ios/*|apps/macos/*|apps/shared/*|Swabble/*|appcast.xml)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
has_non_native_non_docs=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <<< "$CHANGED"
|
|
||||||
|
|
||||||
# If there are non-doc files outside native app trees, keep Node checks enabled.
|
|
||||||
if [ "$run_node" = false ] && [ "$has_non_docs" = true ] && [ "$has_non_native_non_docs" = true ]; then
|
|
||||||
run_node=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "run_node=${run_node}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "run_macos=${run_macos}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "run_android=${run_android}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ Jobs are ordered so cheap checks fail before expensive ones run:
|
|||||||
2. `build-artifacts` (blocked on above)
|
2. `build-artifacts` (blocked on above)
|
||||||
3. `checks`, `checks-windows`, `macos`, `android` (blocked on build)
|
3. `checks`, `checks-windows`, `macos`, `android` (blocked on build)
|
||||||
|
|
||||||
|
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
|
||||||
|
|
||||||
## Runners
|
## Runners
|
||||||
|
|
||||||
| Runner | Jobs |
|
| Runner | Jobs |
|
||||||
|
|||||||
133
scripts/ci-changed-scope.mjs
Normal file
133
scripts/ci-changed-scope.mjs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { appendFileSync } from "node:fs";
|
||||||
|
|
||||||
|
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean }} ChangedScope */
|
||||||
|
|
||||||
|
const DOCS_PATH_RE = /^(docs\/|.*\.mdx?$)/;
|
||||||
|
const MACOS_PROTOCOL_GEN_RE =
|
||||||
|
/^(apps\/macos\/Sources\/OpenClawProtocol\/|apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\/)/;
|
||||||
|
const MACOS_NATIVE_RE = /^(apps\/macos\/|apps\/ios\/|apps\/shared\/|Swabble\/)/;
|
||||||
|
const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/;
|
||||||
|
const NODE_SCOPE_RE =
|
||||||
|
/^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|\.github\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.oxlintrc\.json$|\.oxfmtrc\.jsonc$)/;
|
||||||
|
const NATIVE_ONLY_RE =
|
||||||
|
/^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/shared\/|Swabble\/|appcast\.xml$)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} changedPaths
|
||||||
|
* @returns {ChangedScope}
|
||||||
|
*/
|
||||||
|
export function detectChangedScope(changedPaths) {
|
||||||
|
if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
|
||||||
|
return { runNode: true, runMacos: true, runAndroid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
let runNode = false;
|
||||||
|
let runMacos = false;
|
||||||
|
let runAndroid = false;
|
||||||
|
let hasNonDocs = false;
|
||||||
|
let hasNonNativeNonDocs = false;
|
||||||
|
|
||||||
|
for (const rawPath of changedPaths) {
|
||||||
|
const path = String(rawPath).trim();
|
||||||
|
if (!path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DOCS_PATH_RE.test(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNonDocs = true;
|
||||||
|
|
||||||
|
if (!MACOS_PROTOCOL_GEN_RE.test(path) && MACOS_NATIVE_RE.test(path)) {
|
||||||
|
runMacos = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ANDROID_NATIVE_RE.test(path)) {
|
||||||
|
runAndroid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NODE_SCOPE_RE.test(path)) {
|
||||||
|
runNode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NATIVE_ONLY_RE.test(path)) {
|
||||||
|
hasNonNativeNonDocs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runNode && hasNonDocs && hasNonNativeNonDocs) {
|
||||||
|
runNode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { runNode, runMacos, runAndroid };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} base
|
||||||
|
* @param {string} [head]
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function listChangedPaths(base, head = "HEAD") {
|
||||||
|
if (!base) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const output = execSync(`git diff --name-only ${base} ${head}`, {
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
return output
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ChangedScope} scope
|
||||||
|
* @param {string} [outputPath]
|
||||||
|
*/
|
||||||
|
export function writeGitHubOutput(scope, outputPath = process.env.GITHUB_OUTPUT) {
|
||||||
|
if (!outputPath) {
|
||||||
|
throw new Error("GITHUB_OUTPUT is required");
|
||||||
|
}
|
||||||
|
appendFileSync(outputPath, `run_node=${scope.runNode}\n`, "utf8");
|
||||||
|
appendFileSync(outputPath, `run_macos=${scope.runMacos}\n`, "utf8");
|
||||||
|
appendFileSync(outputPath, `run_android=${scope.runAndroid}\n`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirectRun() {
|
||||||
|
const direct = process.argv[1];
|
||||||
|
return Boolean(direct && import.meta.url.endsWith(direct));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string[]} argv */
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = { base: "", head: "HEAD" };
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
if (argv[i] === "--base") {
|
||||||
|
args.base = argv[i + 1] ?? "";
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (argv[i] === "--head") {
|
||||||
|
args.head = argv[i + 1] ?? "HEAD";
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectRun()) {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
try {
|
||||||
|
const changedPaths = listChangedPaths(args.base, args.head);
|
||||||
|
if (changedPaths.length === 0) {
|
||||||
|
writeGitHubOutput({ runNode: true, runMacos: true, runAndroid: true });
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
writeGitHubOutput(detectChangedScope(changedPaths));
|
||||||
|
} catch {
|
||||||
|
writeGitHubOutput({ runNode: true, runMacos: true, runAndroid: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/scripts/ci-changed-scope.test.ts
Normal file
65
src/scripts/ci-changed-scope.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { detectChangedScope } from "../../scripts/ci-changed-scope.mjs";
|
||||||
|
|
||||||
|
describe("detectChangedScope", () => {
|
||||||
|
it("fails safe when no paths are provided", () => {
|
||||||
|
expect(detectChangedScope([])).toEqual({
|
||||||
|
runNode: true,
|
||||||
|
runMacos: true,
|
||||||
|
runAndroid: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps all lanes off for docs-only changes", () => {
|
||||||
|
expect(detectChangedScope(["docs/ci.md", "README.md"])).toEqual({
|
||||||
|
runNode: false,
|
||||||
|
runMacos: false,
|
||||||
|
runAndroid: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enables node lane for node-relevant files", () => {
|
||||||
|
expect(detectChangedScope(["src/plugins/runtime/index.ts"])).toEqual({
|
||||||
|
runNode: true,
|
||||||
|
runMacos: false,
|
||||||
|
runAndroid: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps node lane off for native-only changes", () => {
|
||||||
|
expect(detectChangedScope(["apps/macos/Sources/Foo.swift"])).toEqual({
|
||||||
|
runNode: false,
|
||||||
|
runMacos: true,
|
||||||
|
runAndroid: false,
|
||||||
|
});
|
||||||
|
expect(detectChangedScope(["apps/shared/OpenClawKit/Sources/Foo.swift"])).toEqual({
|
||||||
|
runNode: false,
|
||||||
|
runMacos: true,
|
||||||
|
runAndroid: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not force macOS for generated protocol model-only changes", () => {
|
||||||
|
expect(detectChangedScope(["apps/macos/Sources/OpenClawProtocol/GatewayModels.swift"])).toEqual(
|
||||||
|
{
|
||||||
|
runNode: false,
|
||||||
|
runMacos: false,
|
||||||
|
runAndroid: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enables node lane for non-native non-doc files by fallback", () => {
|
||||||
|
expect(detectChangedScope(["README.md"])).toEqual({
|
||||||
|
runNode: false,
|
||||||
|
runMacos: false,
|
||||||
|
runAndroid: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(detectChangedScope(["assets/icon.png"])).toEqual({
|
||||||
|
runNode: true,
|
||||||
|
runMacos: false,
|
||||||
|
runAndroid: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user