diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9caa381f43..2bc1c95c8fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,7 @@ jobs: run_check_additional: ${{ steps.manifest.outputs.run_check_additional }} run_check_docs: ${{ steps.manifest.outputs.run_check_docs }} run_control_ui_i18n: ${{ steps.manifest.outputs.run_control_ui_i18n }} + run_native_i18n: ${{ steps.manifest.outputs.run_native_i18n }} run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }} checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }} run_macos_node: ${{ steps.manifest.outputs.run_macos_node }} @@ -213,6 +214,7 @@ jobs: OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }} OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }} OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }} + OPENCLAW_CI_RUN_NATIVE_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_native_i18n || 'false' }} OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }} OPENCLAW_CI_REPOSITORY: ${{ github.repository }} OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }} @@ -280,6 +282,8 @@ jobs: const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly; const runControlUiI18n = parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly; + const runNativeI18n = + parseBoolean(process.env.OPENCLAW_CI_RUN_NATIVE_I18N) && !docsOnly; const checksFastCoreTasks = []; if (runNodeFull) { checksFastCoreTasks.push( @@ -346,6 +350,7 @@ jobs: run_check_additional: runNodeFull, run_check_docs: docsChanged && eventName !== "push", run_control_ui_i18n: runControlUiI18n, + run_native_i18n: runNativeI18n, run_skills_python_job: runSkillsPython, run_checks_windows: runWindows, checks_windows_matrix: createMatrix( @@ -810,7 +815,7 @@ jobs: permissions: contents: read needs: [preflight] - if: ${{ !cancelled() && always() && (needs.preflight.outputs.run_macos == 'true' || needs.preflight.outputs.run_android == 'true' || needs.preflight.outputs.run_node == 'true') }} + if: ${{ !cancelled() && always() && needs.preflight.outputs.run_native_i18n == 'true' }} runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 10 steps: diff --git a/scripts/ci-changed-scope.d.mts b/scripts/ci-changed-scope.d.mts index 8f42893bc16..3a8ab4b30bc 100644 --- a/scripts/ci-changed-scope.d.mts +++ b/scripts/ci-changed-scope.d.mts @@ -15,6 +15,7 @@ export type InstallSmokeScope = { }; export function detectChangedScope(changedPaths: string[]): ChangedScope; +export function shouldRunNativeI18n(changedPaths: string[]): boolean; export function detectInstallSmokeScope(changedPaths: string[]): InstallSmokeScope; export function listChangedPaths( base: string, @@ -26,4 +27,10 @@ export function writeGitHubOutput( scope: ChangedScope, outputPath?: string, installSmokeScope?: InstallSmokeScope, + nodeFastScope?: { + runFastOnly: boolean; + runPluginContracts: boolean; + runCiRouting: boolean; + }, + runNativeI18n?: boolean, ): void; diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 23357c38b5a..b3f9dcde2db 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -51,6 +51,8 @@ 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 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-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 = @@ -165,6 +167,14 @@ export function detectChangedScope(changedPaths) { }; } +export function shouldRunNativeI18n(changedPaths) { + return ( + !Array.isArray(changedPaths) || + changedPaths.length === 0 || + changedPaths.some((path) => NATIVE_I18N_SCOPE_RE.test(path.trim())) + ); +} + /** * @param {string[]} changedPaths * @returns {NodeFastScope} @@ -294,6 +304,7 @@ export function writeGitHubOutput( runFullInstallSmoke: scope.runChangedSmoke, }, nodeFastScope = { runFastOnly: false, runPluginContracts: false, runCiRouting: false }, + runNativeI18n = true, ) { if (!outputPath) { throw new Error("GITHUB_OUTPUT is required"); @@ -323,6 +334,7 @@ export function writeGitHubOutput( "utf8", ); appendFileSync(outputPath, `run_control_ui_i18n=${scope.runControlUiI18n}\n`, "utf8"); + appendFileSync(outputPath, `run_native_i18n=${runNativeI18n}\n`, "utf8"); } function isDirectRun() { @@ -369,7 +381,7 @@ if (isDirectRun()) { args.mergeHeadFirstParent, ); if (changedPaths.length === 0) { - writeGitHubOutput(EMPTY_SCOPE); + writeGitHubOutput(EMPTY_SCOPE, process.env.GITHUB_OUTPUT, undefined, undefined, false); process.exit(0); } writeGitHubOutput( @@ -377,8 +389,9 @@ if (isDirectRun()) { process.env.GITHUB_OUTPUT, detectInstallSmokeScope(changedPaths), detectNodeFastScope(changedPaths), + shouldRunNativeI18n(changedPaths), ); } catch { - writeGitHubOutput(FULL_SCOPE); + writeGitHubOutput(FULL_SCOPE, process.env.GITHUB_OUTPUT, undefined, undefined, true); } } diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index 3abbbc10e17..ebadc30e979 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -12,6 +12,7 @@ const { detectNodeFastScope, listChangedPaths, parseArgs, + shouldRunNativeI18n, } = (await import("../../scripts/ci-changed-scope.mjs")) as unknown as { detectChangedScope: (paths: string[]) => { runNode: boolean; @@ -32,6 +33,7 @@ const { runPluginContracts: boolean; runCiRouting: boolean; }; + shouldRunNativeI18n: (paths: string[]) => boolean; listChangedPaths: ( base: string, head?: string, @@ -120,9 +122,7 @@ describe("parseArgs", () => { it("rejects missing CI diff refs", () => { expect(() => parseArgs(["--base", "--head", "HEAD"])).toThrow("--base requires a value"); - expect(() => parseArgs(["--base", "-h", "--head", "HEAD"])).toThrow( - "--base requires a value", - ); + expect(() => parseArgs(["--base", "-h", "--head", "HEAD"])).toThrow("--base requires a value"); expect(() => parseArgs(["--head"])).toThrow("--head requires a value"); expect(() => parseArgs(["--head", "-h"])).toThrow("--head requires a value"); expect(() => parseArgs(["--base", ""])).toThrow("--base requires a value"); @@ -130,6 +130,27 @@ describe("parseArgs", () => { }); describe("detectChangedScope", () => { + it("routes only native i18n-owned paths to the native inventory job", () => { + for (const changedPath of [ + "apps/.i18n/native-source.json", + "apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt", + "apps/ios/Sources/RootTabs.swift", + "apps/macos/Sources/OpenClaw/Settings.swift", + "apps/shared/OpenClawKit/Sources/OpenClawKit/Client.swift", + "scripts/native-app-i18n.ts", + "scripts/android-app-i18n.ts", + "scripts/apple-app-i18n.ts", + "test/scripts/native-app-i18n.test.ts", + ".github/workflows/native-locale-refresh.yml", + ".github/workflows/ci.yml", + ]) { + expect(shouldRunNativeI18n([changedPath]), changedPath).toBe(true); + } + + expect(shouldRunNativeI18n(["src/config/defaults.ts"])).toBe(false); + expect(shouldRunNativeI18n(["scripts/install.sh"])).toBe(false); + }); + it("fails safe when no paths are provided", () => { expect(detectChangedScope([])).toEqual({ runNode: true, @@ -891,6 +912,7 @@ describe("detectChangedScope", () => { run_fast_install_smoke: "false", run_full_install_smoke: "false", run_control_ui_i18n: "false", + run_native_i18n: "false", }); }); }); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index fead3dedfa8..5d366865750 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -343,6 +343,8 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { "${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }}", OPENCLAW_CI_RUN_MACOS: "${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}", + OPENCLAW_CI_RUN_NATIVE_I18N: + "${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_native_i18n || 'false' }}", OPENCLAW_CI_RUN_NODE: "${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}", OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: