fix(ci): bound OCM asset transfers (#108700) #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Native App Locale Refresh | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - apps/android/app/src/main/** | |
| - apps/ios/** | |
| - apps/macos/Sources/** | |
| - apps/macos/Package.swift | |
| - apps/shared/OpenClawKit/Sources/** | |
| - apps/.i18n/native/** | |
| - apps/.i18n/native-source.json | |
| - scripts/control-ui-i18n.ts | |
| - scripts/android-app-i18n.ts | |
| - scripts/apple-app-i18n.ts | |
| - scripts/native-app-i18n.ts | |
| - ui/src/i18n/.i18n/glossary.*.json | |
| - .github/actions/create-generated-pr-tokens/action.yml | |
| - .github/actions/publish-generated-pr/action.yml | |
| - .github/workflows/native-app-locale-refresh.yml | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: native-app-locale-refresh | |
| # Finish the active full refresh; GitHub retains only the newest pending run for this group. | |
| # Publisher overlap checks defer stale generated paths to that queued reconciliation run. | |
| cancel-in-progress: false | |
| jobs: | |
| resolve-base: | |
| if: >- | |
| github.repository == 'openclaw/openclaw' && | |
| (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| sha: ${{ steps.base.outputs.sha }} | |
| steps: | |
| - name: Resolve default branch head | |
| id: base | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| sha="$( | |
| timeout --signal=TERM --kill-after=10s 60s \ | |
| gh api --method GET "repos/${REPOSITORY}/commits/${DEFAULT_BRANCH}" --jq .sha | |
| )" | |
| if [[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "Unable to resolve ${DEFAULT_BRANCH} to an exact commit." >&2 | |
| exit 1 | |
| fi | |
| echo "sha=${sha}" >> "${GITHUB_OUTPUT}" | |
| publisher-preflight: | |
| name: Verify generated PR App permissions | |
| needs: resolve-base | |
| if: needs.resolve-base.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: ${{ needs.resolve-base.outputs.sha }} | |
| persist-credentials: false | |
| submodules: false | |
| - name: Create generated PR tokens | |
| uses: ./.github/actions/create-generated-pr-tokens | |
| with: | |
| contents-client-id: Iv23liOECG0slfuhz093 | |
| contents-private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| pull-request-app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }} | |
| pull-request-private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }} | |
| refresh: | |
| needs: [resolve-base, publisher-preflight] | |
| if: >- | |
| needs.resolve-base.result == 'success' && | |
| needs.publisher-preflight.result == 'success' | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 2 | |
| matrix: | |
| locale: | |
| [ | |
| zh-CN, | |
| zh-TW, | |
| pt-BR, | |
| de, | |
| es, | |
| ja-JP, | |
| ko, | |
| fr, | |
| hi, | |
| ar, | |
| it, | |
| tr, | |
| uk, | |
| id, | |
| pl, | |
| th, | |
| vi, | |
| nl, | |
| fa, | |
| ru, | |
| sv, | |
| ] | |
| runs-on: ubuntu-latest | |
| name: Refresh native ${{ matrix.locale }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: ${{ needs.resolve-base.outputs.sha }} | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Ensure translation provider secrets exist | |
| env: | |
| OPENCLAW_DOCS_I18N_OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ]; then | |
| echo "Missing OPENCLAW_DOCS_I18N_OPENAI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY secret." | |
| exit 1 | |
| fi | |
| - name: Refresh native locale artifact | |
| env: | |
| OPENCLAW_DOCS_I18N_OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| ANTHROPIC_MODEL: claude-opus-4-8 | |
| OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE || 'gpt-5.6-sol' }} | |
| OPENCLAW_CONTROL_UI_I18N_THINKING: low | |
| OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "0" | |
| LOCALE: ${{ matrix.locale }} | |
| run: | | |
| set -euo pipefail | |
| run_refresh() { | |
| local provider="$1" | |
| local model="$2" | |
| local openai_api_key="${3-}" | |
| if [ "$provider" = "openai" ]; then | |
| OPENAI_API_KEY="$openai_api_key" \ | |
| OPENCLAW_CONTROL_UI_I18N_PROVIDER="$provider" \ | |
| OPENCLAW_CONTROL_UI_I18N_MODEL="$model" \ | |
| node --import tsx scripts/native-app-i18n.ts sync --write --locale "${LOCALE}" | |
| return | |
| fi | |
| OPENCLAW_CONTROL_UI_I18N_PROVIDER="$provider" \ | |
| OPENCLAW_CONTROL_UI_I18N_MODEL="$model" \ | |
| node --import tsx scripts/native-app-i18n.ts sync --write --locale "${LOCALE}" | |
| } | |
| run_openai_refresh() { | |
| local status=1 | |
| if [ -n "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY:-}" ]; then | |
| set +e | |
| run_refresh openai "${OPENAI_MODEL}" "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY}" | |
| status="$?" | |
| set -e | |
| if [ "$status" -eq 0 ]; then | |
| return 0 | |
| fi | |
| if [ -z "${OPENAI_API_KEY:-}" ] || [ "${OPENAI_API_KEY}" = "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY}" ]; then | |
| return "$status" | |
| fi | |
| echo "::warning::Docs OpenAI native locale refresh key failed for ${LOCALE}; retrying with repository OpenAI key." | |
| fi | |
| if [ -z "${OPENAI_API_KEY:-}" ]; then | |
| return "$status" | |
| fi | |
| run_refresh openai "${OPENAI_MODEL}" "${OPENAI_API_KEY}" | |
| } | |
| if [ -n "${ANTHROPIC_API_KEY:-}" ]; then | |
| set +e | |
| run_refresh anthropic "${ANTHROPIC_MODEL}" | |
| status="$?" | |
| set -e | |
| if [ "$status" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| if [ -z "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then | |
| exit "$status" | |
| fi | |
| echo "::warning::Anthropic native locale refresh failed for ${LOCALE}; retrying with OpenAI." | |
| fi | |
| run_openai_refresh | |
| - name: Prepare locale artifact | |
| env: | |
| LOCALE: ${{ matrix.locale }} | |
| run: | | |
| set -euo pipefail | |
| artifact_dir="${RUNNER_TEMP}/native-locale-${LOCALE}" | |
| mkdir -p "${artifact_dir}" | |
| git add -A apps/.i18n/native | |
| git diff --cached --binary --full-index -- apps/.i18n/native > "${artifact_dir}/${LOCALE}.patch" | |
| - name: Upload locale artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: native-locale-${{ matrix.locale }} | |
| path: ${{ runner.temp }}/native-locale-${{ matrix.locale }}/${{ matrix.locale }}.patch | |
| if-no-files-found: error | |
| retention-days: 1 | |
| finalize: | |
| name: Commit native locale refresh | |
| needs: [resolve-base, publisher-preflight, refresh] | |
| if: >- | |
| needs.resolve-base.result == 'success' && | |
| needs.publisher-preflight.result == 'success' && | |
| needs.refresh.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: ${{ needs.resolve-base.outputs.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| submodules: false | |
| - name: Download locale artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: native-locale-* | |
| path: ${{ runner.temp }}/native-locale-artifacts | |
| merge-multiple: true | |
| - name: Apply locale artifacts | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r patch; do | |
| if [ -s "${patch}" ]; then | |
| git apply "${patch}" | |
| fi | |
| done < <(find "${RUNNER_TEMP}/native-locale-artifacts" -type f -name '*.patch' | sort) | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| # Every worker observes the same source inventory. Generate it once here | |
| # so locale patches remain independent and can be applied sequentially. | |
| - name: Refresh shared native inventory | |
| run: node --import tsx scripts/native-app-i18n.ts sync --write | |
| - name: Refresh Android native resources | |
| run: node --import tsx scripts/android-app-i18n.ts sync | |
| - name: Refresh Apple native resources | |
| run: node --import tsx scripts/apple-app-i18n.ts sync-ios --write | |
| - name: Validate native locale refresh | |
| run: | | |
| node --import tsx scripts/native-app-i18n.ts check | |
| node --import tsx scripts/android-app-i18n.ts check | |
| node --import tsx scripts/apple-app-i18n.ts check | |
| - name: Open or update generated locale PR | |
| uses: ./.github/actions/publish-generated-pr | |
| with: | |
| contents-client-id: Iv23liOECG0slfuhz093 | |
| contents-private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| pull-request-app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }} | |
| pull-request-private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }} | |
| base-branch: ${{ github.event.repository.default_branch }} | |
| head-branch: automation/native-app-locale-refresh | |
| commit-message: "chore(i18n): refresh native locales" | |
| pr-title: "chore(i18n): refresh native locales" | |
| generated-paths: | | |
| apps/.i18n/native | |
| apps/.i18n/native-source.json | |
| apps/.i18n/apple-translation-contradictions.json | |
| apps/android/app/src/main/java/ai/openclaw/app/i18n/NativeStringResources.kt | |
| apps/android/app/src/main/res/values*/assistant.xml | |
| apps/android/app/src/main/res/values*/strings.xml | |
| apps/ios/Resources/Localizable.xcstrings | |
| apps/ios/Sources/*.lproj/InfoPlist.strings | |
| apps/ios/WatchApp/*.lproj/InfoPlist.strings | |
| apps/ios/ShareExtension/*.lproj/InfoPlist.strings | |
| apps/ios/ActivityWidget/*.lproj/InfoPlist.strings | |
| invalidation-paths: | | |
| apps/android/app/src/main | |
| apps/ios | |
| apps/macos/Sources | |
| apps/macos/Package.swift | |
| apps/shared/OpenClawKit/Sources | |
| scripts/control-ui-i18n.ts | |
| scripts/android-app-i18n.ts | |
| scripts/apple-app-i18n.ts | |
| scripts/native-app-i18n.ts | |
| ui/src/i18n/.i18n/glossary.*.json | |
| .github/actions/create-generated-pr-tokens/action.yml | |
| .github/actions/publish-generated-pr/action.yml | |
| .github/workflows/native-app-locale-refresh.yml | |
| pr-body: | | |
| ## What Problem This Solves | |
| Keeps generated native app locales synchronized without bypassing protected-branch checks. | |
| ## Why This Change Was Made | |
| The Native App Locale Refresh workflow generated this update from `${{ needs.resolve-base.outputs.sha }}` and published it through a reviewable automation branch. | |
| ## User Impact | |
| No direct user-facing change beyond refreshed translations. | |
| ## Evidence | |
| - [Locale refresh run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| - `node --import tsx scripts/native-app-i18n.ts check` | |
| - `node --import tsx scripts/android-app-i18n.ts check` | |
| - `node --import tsx scripts/apple-app-i18n.ts check` |