fix(ci): stop source-only PR fallback requiring generated locales (#120973)

* fix(ci): scope native i18n parity for release gates

Punchcard-Session: amber-workshop-workshop-36

* fix(ci): preserve native locale scope for PR release gates

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Vincent Koc
2026-08-26 17:50:48 +08:00
committed by GitHub
parent f4cbdefc79
commit ce54cae86d
3 changed files with 25 additions and 8 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ jobs:
run_ui_tests: ${{ steps.manifest.outputs.run_ui_tests }}
ui_e2e_matrix: ${{ steps.manifest.outputs.ui_e2e_matrix }}
run_native_i18n: ${{ steps.manifest.outputs.run_native_i18n }}
strict_native_i18n: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.strict_native_i18n }}
strict_native_i18n: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.strict_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 }}
+6 -5
View File
@@ -20,8 +20,9 @@ heads, and manual dispatches use isolated groups. `preflight` classifies the
diff and turns expensive lanes off when only unrelated areas changed. Ordinary
manual `workflow_dispatch` runs intentionally bypass smart scoping and fan out
the full graph for release candidates and broad validation. Exact-head
`release_gate` fallbacks retain the pull request's macOS and iOS scope instead
of forcing unrelated Apple lanes. Android lanes stay opt-in through
`release_gate` fallbacks retain the pull request's macOS, iOS, and native
generated-locale scope instead of forcing unrelated Apple lanes or locale
parity. Native source verification still runs. Android lanes stay opt-in through
`include_android` (or the `release_gate` input). Release-only
plugin coverage lives in the separate
[`Plugin Prerelease`](#plugin-prerelease) workflow and only runs from
@@ -47,7 +48,7 @@ dispatch.
| `check-additional-*` | Boundary check stripes (including prompt snapshot drift), session accessor/transcript reader/SQLite transaction boundaries, extension lint groups, package boundary compile/canary, and runtime topology architecture; the pure-reporting plugin SDK API diff runs on manual and release dispatches only | Node-relevant changes |
| `checks-node-compat-node22` | Node 22 compatibility build and smoke lane | Full Release Validation and manual dispatches only |
| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed (PRs and manual dispatch) |
| `native-i18n` | Verify native source extraction and localization safety on source PRs; enforce full translated/platform-generated parity on generated PRs and manual CI | Native i18n-relevant changes |
| `native-i18n` | Verify native source extraction and localization safety on source PRs and release gates; enforce generated parity on generated PRs, generated-scope release gates, and ordinary manual CI | Native i18n-relevant changes |
| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes |
| `checks-windows` | Windows-specific process/path tests plus shared runtime import specifier regressions | Windows-relevant changes |
| `macos-node` | Focused macOS TypeScript tests: launchd, Homebrew, runtime paths, packaging scripts, process-group wrapper | macOS-relevant changes |
@@ -110,7 +111,7 @@ When the check fails, update the PR body instead of pushing another code commit.
## Scope and routing
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Ordinary manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed. The exact-head `release_gate` exception evaluates the fetched pull request merge tree and retains its macOS, iOS-build, and screenshot-risk decisions.
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Ordinary manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed. The exact-head `release_gate` exception evaluates the fetched pull request merge tree and retains its macOS, iOS-build, screenshot-risk, and generated-native-locale decisions while still verifying native sources.
Release screenshot routing is deliberately conservative because an app change can break deterministic App Store capture without breaking compilation. Pull requests and exact-head release gates run the full iPhone, iPad, and Watch matrix when the diff touches `apps/ios/**`, linked OpenClawKit or Swabble code, Apple Swift configuration, or the scripts used by screenshot capture. CI runs at most two device shards in parallel, keeps scenarios serial within each device, and captures Watch evidence in the iPhone shard. A hosted reducer verifies the exact evidence union before publishing the sole canonical artifact consumed by `openclaw/ci-gate`. Ordinary manual CI and Full Release Validation always run that matrix. The screenshot decision is independent of macOS routing; a pure iOS app change does not select macOS jobs by itself.
@@ -182,7 +183,7 @@ Barnacle treats bug-labeled issues as verification candidates rather than inacti
## Manual dispatches
Ordinary manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI/native app i18n. Node 22 compatibility runs in Full Release Validation and manual dispatches only; push and pull request CI skip it. The exact-head `release_gate` fallback instead keeps the pull request's macOS and iOS scope, including conservative release screenshot capture for screenshot-pipeline owners. Automatic source PRs verify native extraction inventory and Android/Apple localization safety without requiring translated or platform-generated output in the same PR. The serialized Native App Locale Refresh workflow rebuilds those artifacts in one isolated PR and enables exact-head auto-merge after required checks pass. Full native parity remains blocking for generated-artifact PRs, manual CI, Full Release Validation, and release prep. Control UI locale parity remains advisory on automatic PR and `main` runs and blocking on manual/release CI. Standalone manual CI dispatches run Android only with `include_android=true` (the `release_gate` input also forces Android); the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled.
Ordinary manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI/native app i18n. Node 22 compatibility runs in Full Release Validation and manual dispatches only; push and pull request CI skip it. The exact-head `release_gate` fallback instead keeps the pull request's macOS, iOS, and generated-native-locale scope, including conservative release screenshot capture for screenshot-pipeline owners. Automatic source PRs and release gates verify native extraction inventory and Android/Apple localization safety without requiring translated or platform-generated output in the same PR. The serialized Native App Locale Refresh workflow rebuilds those artifacts in one isolated PR and enables exact-head auto-merge after required checks pass. Full native parity remains blocking for generated-artifact PRs, generated-scope release gates, ordinary manual CI, Full Release Validation, and release prep. Control UI locale parity remains advisory on automatic PR and `main` runs and blocking on manual/release CI. Standalone manual CI dispatches run Android only with `include_android=true` (the `release_gate` input also forces Android); the full release umbrella enables Android by passing `include_android=true` without setting `release_gate`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled.
PR baseline ratchets derive their comparison state from the checked-out synthetic merge tree and verify its head parent against the event head. The max-lines entry chains the environment-variable budget with the same fork-point ref before the assertion-safety check, so production source growth cannot first surface on `main`. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref; ratchet baselines are compared with the target's merge base against the default-branch head resolved for that run. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head and `pull_request_number` to identify the open PR whose merge tree is validated.
+18 -2
View File
@@ -7548,6 +7548,9 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
it("splits native source verification from generated locale parity", () => {
const workflow = readCiWorkflow();
const manifestStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const localeJob = workflow.jobs["native-i18n"];
const sourceStep = localeJob.steps.find(
(step: WorkflowStep) => step.name === "Verify native app i18n source",
@@ -7556,11 +7559,19 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
(step: WorkflowStep) => step.name === "Check native app generated locale parity",
);
const packageScripts = JSON.parse(readFileSync("package.json", "utf8")).scripts;
const fullReleaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8");
const fullReleaseCiCase = expectDefined(
fullReleaseSource.match(/case "\$CHILD_WORKFLOW_KIND" in\n\s+ci\)([\s\S]*?)\n\s+;;/u)?.[1],
"Full Release CI dispatch case",
);
expect(packageScripts["native:i18n:baseline"]).toContain("baseline --write");
expect(packageScripts["native:i18n:verify"]).toContain(" verify");
expect(workflow.jobs.preflight.outputs.strict_native_i18n).toContain(
"steps.changed_scope.outputs.strict_native_i18n",
expect(workflow.jobs.preflight.outputs.strict_native_i18n).toBe(
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.strict_native_i18n }}",
);
expect(manifestStep.env.OPENCLAW_CI_RUN_NATIVE_I18N).toBe(
"${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_native_i18n || 'false' }}",
);
expect(sourceStep.run).toContain("pnpm native:i18n:verify");
expect(sourceStep.run).toContain("Historical release targets");
@@ -7568,6 +7579,11 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
expect(parityStep.run).toContain("pnpm native:i18n:check");
expect(parityStep.run).not.toContain("pnpm android:i18n:check");
expect(parityStep.run).not.toContain("pnpm apple:i18n:check");
expect(fullReleaseCiCase).toContain(
'args=(-f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id")',
);
expect(fullReleaseCiCase).toContain('dispatch_child ci.yml "$dispatch_run_name"');
expect(fullReleaseCiCase).not.toContain("release_gate");
});
it("runs built runtime verifiers inside the artifact-check wave", () => {