Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

697 lines
33 KiB
YAML

name: Mobile Build Smoke Test
on:
# Reusable entry for the scheduled exhaustive develop lane (#12342).
workflow_call:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, labeled]
workflow_dispatch:
inputs:
cloud_api_base:
description: "Eliza Cloud API base for the iOS cloud runtime-mode smoke (defaults to the staging sandbox var, then prod)"
required: false
default: ""
schedule:
# Nightly iOS local (full-Bun on-device engine) lane (#13578). This mode
# is too heavy for the PR path — a full-Bun engine build, a real ~1.3GB
# GGUF, and CPU inference in the simulator — so it runs on a schedule and
# gates that nightly run instead of every pull request.
- cron: "0 7 * * *"
concurrency:
group: mobile-smoke-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
BUN_VERSION: "canary"
# Default to least privilege. Override per-job where needed.
permissions:
contents: read
jobs:
changes:
name: Classify changed paths
# Path classifier is a git-diff + node script: no self-hosted resources.
# Keep it on GitHub-hosted so a drained hetzner fleet cannot leave it
# queued indefinitely and pile up runs (#13617/#8501).
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
timeout-minutes: 10
outputs:
ios: ${{ steps.filter.outputs.ios }}
android: ${{ steps.filter.outputs.android }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
submodules: false
- name: Setup Node.js for path gate
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: "24"
- name: Determine affected mobile smoke surface
id: filter
shell: bash
env:
PR_LABELS: ${{ github.event_name == 'pull_request' && join(github.event.pull_request.labels.*.name, ',') || '' }}
run: |
set -euo pipefail
node packages/scripts/ci-path-gate.mjs \
--config mobile \
--event "${{ github.event_name }}" \
--base "${{ github.event.pull_request.base.sha }}" \
--head "${{ github.event.pull_request.head.sha }}" \
--labels "$PR_LABELS" \
--output "$GITHUB_OUTPUT" \
--summary "$GITHUB_STEP_SUMMARY"
build-ios:
name: iOS Simulator Build
needs: changes
if: github.event_name != 'pull_request' || needs.changes.outputs.ios == 'true'
runs-on: macos-15
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
# This workflow validates the app-core mobile project wiring, not
# native inference submodule health. Keep checkout source-clean and
# let setup-bun-workspace/postinstall initialize only the submodules
# it actually needs.
submodules: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 24
- name: Setup workspace dependencies
uses: ./.github/actions/setup-bun-workspace
with:
bun-version: ${{ env.BUN_VERSION }}
install-native-deps: "false"
install-command: bun install --ignore-scripts --no-frozen-lockfile
run-postinstall: "true"
skip-avatar-clone: "true"
no-vision-deps: "true"
- name: Generate protobuf types
# Restores the step that was on the last green Mobile Build Smoke
# run (52ab686a, 2026-05-09). Without these generated types in
# `packages/core/src/types/generated/`, `@elizaos/core`'s build
# fails downstream, which cascades into `@elizaos/shared` and
# vite config resolution.
run: |
if [ -d packages/schemas ] && [ -f packages/schemas/buf.gen.yaml ] && [ ! -d packages/core/src/types/generated ]; then
cd packages/schemas
node node_modules/@bufbuild/buf/install.js 2>/dev/null || true
bunx @bufbuild/buf@1.67.0 generate
fi
- name: Build Vite config package deps
# vite.config.ts imports dist-only workspace packages
# (`@elizaos/shared` and `@elizaos/ui/config/app-config`), and the
# app aliases plugin-browser source which imports `@elizaos/vault`.
# Build those package chains before Vite evaluates/bundles the app.
run: bun turbo run build --filter=@elizaos/ui --filter=@elizaos/vault
- name: Build iOS simulator app
run: node --max-old-space-size=8192 packages/app-core/scripts/run-mobile-build.mjs ios
env:
LANG: en_US.UTF-8
# The simulator smokes drive a loopback host agent. Use the direct
# developer iOS lane; App Store/cloud builds intentionally reject
# cleartext loopback and are covered by release workflows.
ELIZA_BUILD_VARIANT: "direct"
ELIZA_RELEASE_AUTHORITY: "developer-toolchain"
VITE_ELIZA_IOS_ALLOW_SIMULATOR_LOOPBACK: "1"
ELIZA_IOS_FULL_BUN_ENGINE: "0"
ELIZA_IOS_BUILD_DESTINATION: "generic/platform=iOS Simulator"
ELIZA_IOS_BUILD_SDK: "iphonesimulator"
ELIZA_MOBILE_BUILD_TIMING_OUT: ${{ runner.temp }}/ios-simulator-build-timing.json
ELIZA_MOBILE_BUILD_TIMING_TARGET: ios-ipa
NODE_OPTIONS: "--max-old-space-size=8192"
- name: Check iOS simulator build-time budget
run: |
bun run --cwd packages/app perf:startup-budget -- \
--build-timing "$RUNNER_TEMP/ios-simulator-build-timing.json" \
--out "$RUNNER_TEMP/ios-simulator-build-budget-report.json"
- name: Upload iOS simulator build budget report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-simulator-build-budget
path: |
${{ runner.temp }}/ios-simulator-build-timing.json
${{ runner.temp }}/ios-simulator-build-budget-report.json
if-no-files-found: warn
retention-days: 7
- name: Verify .app exists
run: |
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "App.app" -path "*/Debug-iphonesimulator/*" | head -1)
if [ -z "$APP_PATH" ]; then
echo "ERROR: App.app not found in DerivedData"
exit 1
fi
echo "Built app at: $APP_PATH"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$APP_PATH/Info.plist")
echo "Bundle ID: $BUNDLE_ID"
# Bundle ID source of truth is packages/app/app.config.ts (read
# by run-mobile-build.mjs via regex). Current value is
# `app.eliza` since commit d042ed57b5 renamed it from the
# legacy `ai.elizaos.app`. The two prior ids remain accepted
# because release branches and downstream forks may not have
# rebased onto the rename yet.
case "$BUNDLE_ID" in
app.eliza|ai.elizaos.app|ai.elizaos.Eliza) ;;
*)
echo "ERROR: Unexpected bundle ID: $BUNDLE_ID"
exit 1
;;
esac
- name: Verify staged renderer is the freshly built one (no stale UI, #9309)
run: |
# The staged iOS public/ must carry exactly the renderer just built in
# packages/app/dist (build stamp match). Fails loudly on stale/missing UI.
node packages/app-core/scripts/verify-ondevice-artifact.mjs --platform ios
# The renderer build stamp must also be baked into the built .app bundle.
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "App.app" -path "*/Debug-iphonesimulator/*" | head -1)
if [ ! -f "$APP_PATH/public/eliza-renderer-build.json" ]; then
echo "ERROR: built .app is missing the renderer build stamp at public/eliza-renderer-build.json"
exit 1
fi
echo "Renderer build stamp present in built .app:"
cat "$APP_PATH/public/eliza-renderer-build.json"
- name: Run iOS onboarding -> home simulator smoke
run: |
set -euo pipefail
node packages/app/scripts/ios-onboarding-smoke.mjs
- name: Upload iOS onboarding evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-onboarding-to-home
path: packages/app/test-results/ios-onboarding-to-home/**
if-no-files-found: warn
retention-days: 7
- name: Run iOS native attachment smoke (#10936)
# Evidence lane for the content-addressed media + native
# Filesystem/Share path. Promoted to a hard-gating step (#13576):
# a regression in the media-store / Filesystem / Share path now
# turns Mobile Build Smoke red instead of self-skipping to green.
run: |
set -euo pipefail
node packages/app/scripts/ios-attachment-smoke.mjs
- name: Upload iOS native attachment evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-attachment-smoke
path: packages/app/test-results/ios-attachment-smoke/**
if-no-files-found: warn
retention-days: 7
- name: Run iOS local-chat simulator smoke (#9943)
# Wire the existing iOS chat device smoke into CI. mobile-local-chat-smoke
# drives a real chat turn through the installed simulator app (the iOS
# self-driving-WebView mechanism — WKWebView is not CDP/Playwright-drivable,
# so this is the only viable iOS device-chat coverage). It previously ran
# in ZERO CI lanes (issue #9943 item 5). Promoted to a hard-gating
# step (#13576): a regression in iOS chat send/receive through the
# app-core API now turns Mobile Build Smoke red instead of
# self-skipping to green.
run: |
set -euo pipefail
node packages/app/scripts/mobile-local-chat-smoke.mjs \
--platform ios --require-installed --start-host-agent
- name: Upload iOS local-chat evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-local-chat
path: packages/app/test-results/ios-local-chat/**
if-no-files-found: warn
retention-days: 7
- name: Run Mobile Safari chat-sheet visual smoke (#15666)
# run-chat-sheet-mobile-safari-smoke.mjs builds the WebKit chat-sheet
# fixture via the Playwright smoke, serves it over loopback HTTP, opens
# it in real Mobile Safari on the booted simulator, and asserts a
# non-blank screen capture. Wired here because this is the only
# PR-path lane with an iOS Simulator available, and the ui-e2e
# runner-coverage ratchet (packages/scripts/__tests__/
# ui-e2e-runner-coverage.test.ts) requires every packages/ui __e2e__
# runner to be invoked by a workflow leg.
run: |
set -euo pipefail
bunx playwright install webkit
bun run --cwd packages/ui test:chat-sheet-mobile-safari-smoke
- name: Upload Mobile Safari chat-sheet evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-mobile-safari-chat-sheet
path: packages/ui/src/components/shell/__e2e__/output-mobile-safari/**
if-no-files-found: warn
retention-days: 7
- name: Verify required Info.plist keys for background HealthKit
# Background HealthKit + BGTaskScheduler integration is in
# progress: no plist generator is configured yet (no
# capacitor.config.ts customization, no run-mobile-build.mjs
# writer for these keys), so until that lands the verify is a
# non-blocking warning. Flip back to a hard fail when the
# generator pipeline lands.
continue-on-error: true
run: |
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "App.app" -path "*/Debug-iphonesimulator/*" | head -1)
INFO_PLIST="$APP_PATH/Info.plist"
for key in NSHealthShareUsageDescription BGTaskSchedulerPermittedIdentifiers UIBackgroundModes; do
if ! /usr/libexec/PlistBuddy -c "Print :$key" "$INFO_PLIST" >/dev/null 2>&1; then
echo "::warning::Info.plist missing required key: $key (TODO: wire up plist customization for background HealthKit)"
/usr/libexec/PlistBuddy -c "Print" "$INFO_PLIST" | head -80
exit 1
fi
done
# Confirm the sleep-refresh task identifier is whitelisted so
# BGTaskScheduler.register actually succeeds on device.
if ! /usr/libexec/PlistBuddy -c "Print :BGTaskSchedulerPermittedIdentifiers" "$INFO_PLIST" \
| grep -q "ai.eliza.mobile-signals.sleep-refresh"; then
echo "::warning::BGTaskSchedulerPermittedIdentifiers missing ai.eliza.mobile-signals.sleep-refresh"
exit 1
fi
- name: Verify Fastlane config landed in packages/app/ios
# Capacitor's iOS project ends up at packages/app/ios/ once
# run-mobile-build.mjs runs. overlayIosNativeFiles() copies
# the Fastlane config there. Skip when the dir hasn't been
# populated yet (Phase D mobile pipeline isn't always run in
# smoke).
continue-on-error: true
run: |
if [ ! -d packages/app/ios ]; then
echo "::warning::packages/app/ios/ not present; skipping Fastlane config verify"
exit 0
fi
for f in packages/app/ios/Gemfile packages/app/ios/fastlane/Fastfile packages/app/ios/fastlane/Appfile packages/app/ios/fastlane/Matchfile; do
if [ ! -f "$f" ]; then
echo "::warning::$f missing. overlayIosNativeFiles() should have copied Fastlane config here so CI can call \`fastlane beta|release\`."
exit 1
fi
done
echo "Fastlane config present at packages/app/ios/."
build-ios-local:
name: iOS Local (full-Bun) Simulator Smoke
# Nightly / on-demand only — never on the PR path. The full-Bun on-device
# engine build + real GGUF + CPU inference is far too heavy to gate every
# pull request, so the local runtime mode gates the scheduled run instead.
# This closes the coverage gap called out in #13578: remote mode is
# CI-blocking, but local (full-Bun) mode had no unattended lane. Mirrors
# the remote build-ios job, swapping the direct/loopback host-agent smoke
# for the on-device full-Bun engine smoke (no host agent — the engine runs
# in-process and the smoke exchanges the request/result via Preferences).
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: macos-15
timeout-minutes: 90
env:
# A real, small on-device GGUF — the same eliza-1 e2b text model the
# Android live smoke stages. mobile-local-chat-smoke.mjs copies whatever
# ELIZA_IOS_FULL_BUN_SMOKE_MODEL_PATH points at into the sim container and
# registers it under the `eliza-1-2b` id, capping the context to 4096 so
# model load + first reply stay fast on a simulator.
IOS_FULL_BUN_SMOKE_MODEL_URL: "https://huggingface.co/elizaos/eliza-1/resolve/main/bundles/e2b/text/eliza-1-e2b-32k.gguf?download=true"
IOS_FULL_BUN_SMOKE_MODEL_FILE: "eliza-1-e2b-32k.gguf"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
submodules: false
# The full-Bun on-device engine builds the MTP-forked llama.cpp kernels
# (run-mobile-build.mjs ios-local with ELIZA_IOS_FULL_BUN_ENGINE=1), which
# requires the llama.cpp fork checkout. `submodules: false` above keeps the
# checkout source-clean, so init just that submodule (not the full recursive
# tree) before the build — build-llama-cpp-mtp.mjs otherwise aborts with
# "no llama.cpp fork checkout found".
- name: Initialize llama.cpp fork submodule (full-Bun MTP kernels)
run: git submodule update --init --recursive plugins/plugin-local-inference/native/llama.cpp
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 24
- name: Setup workspace dependencies
uses: ./.github/actions/setup-bun-workspace
with:
bun-version: ${{ env.BUN_VERSION }}
install-native-deps: "false"
install-command: bun install --ignore-scripts --no-frozen-lockfile
run-postinstall: "true"
skip-avatar-clone: "true"
no-vision-deps: "true"
- name: Generate protobuf types
run: |
if [ -d packages/schemas ] && [ -f packages/schemas/buf.gen.yaml ] && [ ! -d packages/core/src/types/generated ]; then
cd packages/schemas
node node_modules/@bufbuild/buf/install.js 2>/dev/null || true
bunx @bufbuild/buf@1.67.0 generate
fi
- name: Build Vite config package deps
run: bun turbo run build --filter=@elizaos/ui --filter=@elizaos/vault
- name: Cache full-Bun smoke GGUF
id: model-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ~/.cache/eliza-ios-full-bun-smoke
key: ios-full-bun-smoke-model-${{ env.IOS_FULL_BUN_SMOKE_MODEL_FILE }}
- name: Download full-Bun smoke GGUF (cache miss)
if: steps.model-cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
mkdir -p "$HOME/.cache/eliza-ios-full-bun-smoke"
curl -fL --retry 3 --retry-all-errors \
-o "$HOME/.cache/eliza-ios-full-bun-smoke/$IOS_FULL_BUN_SMOKE_MODEL_FILE" \
"$IOS_FULL_BUN_SMOKE_MODEL_URL"
ls -la "$HOME/.cache/eliza-ios-full-bun-smoke/"
- name: Build iOS simulator app (full-Bun local runtime)
# Mirrors `build:ios:local:sim` (run-mobile-build.mjs ios-local with
# ELIZA_IOS_FULL_BUN_ENGINE=1). The remote lane builds the same target
# with the engine disabled (ELIZA_IOS_FULL_BUN_ENGINE=0); flipping it on
# is exactly the coverage this lane adds.
run: node --max-old-space-size=8192 packages/app-core/scripts/run-mobile-build.mjs ios-local
env:
LANG: en_US.UTF-8
ELIZA_BUILD_VARIANT: "direct"
ELIZA_RELEASE_AUTHORITY: "developer-toolchain"
VITE_ELIZA_IOS_ALLOW_SIMULATOR_LOOPBACK: "1"
ELIZA_IOS_FULL_BUN_ENGINE: "1"
ELIZA_IOS_BUILD_DESTINATION: "generic/platform=iOS Simulator"
ELIZA_IOS_BUILD_SDK: "iphonesimulator"
ELIZA_MOBILE_BUILD_TIMING_OUT: ${{ runner.temp }}/ios-full-bun-build-timing.json
ELIZA_MOBILE_BUILD_TIMING_TARGET: ios-ipa
NODE_OPTIONS: "--max-old-space-size=8192"
- name: Check iOS full-Bun build-time budget
run: |
bun run --cwd packages/app perf:startup-budget -- \
--build-timing "$RUNNER_TEMP/ios-full-bun-build-timing.json" \
--out "$RUNNER_TEMP/ios-full-bun-build-budget-report.json"
- name: Upload iOS full-Bun build budget report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-full-bun-build-budget
path: |
${{ runner.temp }}/ios-full-bun-build-timing.json
${{ runner.temp }}/ios-full-bun-build-budget-report.json
if-no-files-found: warn
retention-days: 7
- name: Verify .app exists
run: |
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "App.app" -path "*/Debug-iphonesimulator/*" | head -1)
if [ -z "$APP_PATH" ]; then
echo "ERROR: App.app not found in DerivedData"
exit 1
fi
echo "Built app at: $APP_PATH"
- name: Verify staged renderer is the freshly built one (no stale UI, #9309)
run: |
node packages/app-core/scripts/verify-ondevice-artifact.mjs --platform ios
- name: Boot simulator and install the full-Bun app
run: |
set -euo pipefail
# Pick an available iPhone simulator; boot it and wait for readiness.
UDID=$(xcrun simctl list devices available -j \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);let u="";for(const runtime of Object.keys(j.devices)){if(!/iOS/.test(runtime))continue;for(const dev of j.devices[runtime]){if(dev.isAvailable&&/iPhone/.test(dev.name)){u=dev.udid;break}}if(u)break}process.stdout.write(u)})')
if [ -z "$UDID" ]; then
echo "ERROR: no available iPhone simulator found"
xcrun simctl list devices available
exit 1
fi
echo "Booting simulator $UDID"
xcrun simctl boot "$UDID" || true
xcrun simctl bootstatus "$UDID" -b
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "App.app" -path "*/Debug-iphonesimulator/*" | head -1)
echo "Installing $APP_PATH"
xcrun simctl install "$UDID" "$APP_PATH"
- name: Run iOS full-Bun local-chat simulator smoke (#13578)
# Gating check for the nightly lane: build the on-device full-Bun
# engine, stage the real GGUF, and assert the exact on-device reply
# (`ios smoke model works`). No `continue-on-error` — a regression in
# engine boot, the on-device model registry, or first-reply generation
# fails the scheduled run.
run: |
set -euo pipefail
mkdir -p packages/app/test-results/ios-full-bun-local-chat
# Point the smoke at the cached GGUF via an absolute path it can stat.
export ELIZA_IOS_FULL_BUN_SMOKE_MODEL_PATH="$HOME/.cache/eliza-ios-full-bun-smoke/$IOS_FULL_BUN_SMOKE_MODEL_FILE"
if [ ! -f "$ELIZA_IOS_FULL_BUN_SMOKE_MODEL_PATH" ]; then
echo "ERROR: staged smoke model missing at $ELIZA_IOS_FULL_BUN_SMOKE_MODEL_PATH"
exit 1
fi
node packages/app/scripts/mobile-local-chat-smoke.mjs \
--platform ios --require-installed --ios-select-local --ios-full-bun-smoke \
2>&1 | tee packages/app/test-results/ios-full-bun-local-chat/smoke.log
- name: Upload iOS full-Bun local-chat evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-full-bun-local-chat
path: packages/app/test-results/ios-full-bun-local-chat/**
if-no-files-found: warn
retention-days: 7
ios-cloud-mode:
name: iOS Cloud Runtime-Mode Smoke
# Nightly / on-demand only — never on the PR path. Provisioning a real Eliza
# Cloud agent (Hetzner-backed) is slow and costs org credit, so the cloud
# runtime mode gates the scheduled run alongside the full-Bun local lane.
# This closes the third row of the #13578 coverage matrix: remote mode is
# PR-blocking and local (full-Bun) mode gates nightly, but cloud mode had no
# unattended lane at all — the XCUITest cloud-onboarding path XCTSkips at the
# OAuth wall without a device Cloud session, and cloud-provisioning-e2e.mjs
# (the programmatic cloud runtime assertion) was only reachable behind the
# unwired `ios-e2e.mjs --cloud`. cloud-provisioning-e2e.mjs has no workspace
# imports (node built-ins + fetch only) and needs no build/simulator, so the
# cloud runtime-mode gate is a fast ubuntu job seeded with a real Cloud
# credential rather than a browser OAuth leg.
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
timeout-minutes: 30
env:
# Headless session seed (#13578 done-when #2): a real Cloud credential
# injected from the org secret, bypassing the interactive device-code /
# OAuth browser leg that XCTSkips on the simulator. ELIZACLOUD_API_KEY is
# the same secret launch-hardening-8756.yml uses for its fresh-agent probe.
ELIZA_CLOUD_AUTH_TOKEN: ${{ secrets.ELIZACLOUD_API_KEY }}
# ORIGIN ONLY — cloud-provisioning-e2e.mjs appends `/api/v1/...` itself, so
# this must NOT carry an `/api/v1` suffix (unlike the sandbox-live-smoke
# `_URL` var, which does). Overridable via workflow_dispatch to target a
# throwaway org; defaults to production Cloud otherwise.
ELIZA_CLOUD_API_BASE: ${{ github.event.inputs.cloud_api_base || 'https://api.elizacloud.ai' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
submodules: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 24
- name: Run iOS cloud runtime-mode provisioning smoke (#13578)
# Gating check for the cloud runtime mode. With the seed present this is a
# HARD gate (no continue-on-error): a regression in cloud agent creation,
# Hetzner provisioning, or the provisioned runtime never answering fails
# the scheduled run. cloud-provisioning-e2e.mjs provisions a real agent,
# waits for its runtime URL, and probes /api/status|health|me — the same
# programmatic cloud path the iOS `ios-e2e.mjs --cloud` route calls. When
# the secret is absent (forks without secret access) the lane records an
# explicit skip in the step summary rather than silently passing green.
run: |
set -euo pipefail
if [ -z "${ELIZA_CLOUD_AUTH_TOKEN:-}" ]; then
{
echo "### iOS cloud runtime-mode smoke"
echo ""
echo "skipped: missing ELIZACLOUD_API_KEY secret (no Cloud session seed available)"
} >> "$GITHUB_STEP_SUMMARY"
echo "iOS cloud runtime-mode smoke skipped: missing ELIZACLOUD_API_KEY secret"
exit 0
fi
mkdir -p packages/app/test-results/ios-cloud-mode
node packages/app/scripts/cloud-provisioning-e2e.mjs \
--fresh-agent \
--agent-name "Eliza iOS Cloud Runtime-Mode Smoke" \
--report packages/app/test-results/ios-cloud-mode/cloud-provisioning.json \
2>&1 | tee packages/app/test-results/ios-cloud-mode/smoke.log
- name: Upload iOS cloud runtime-mode evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ios-cloud-mode
path: packages/app/test-results/ios-cloud-mode/**
if-no-files-found: warn
retention-days: 7
build-android:
name: Android Debug Build
needs: changes
if: github.event_name != 'pull_request' || needs.changes.outputs.android == 'true'
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
# Android cloud-debug smoke sets ELIZA_ANDROID_SKIP_FORK_LLAMA_LIB=1
# below, so recursive native inference submodules are not part of this
# contract. Avoid failing app smoke on optional nested native pins.
submodules: false
- name: Setup Java 21
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287
with:
distribution: temurin
java-version: 21
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 24
- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699
with:
packages: "platform-tools platforms;android-34 build-tools;34.0.0"
- name: Setup workspace dependencies
uses: ./.github/actions/setup-bun-workspace
with:
bun-version: ${{ env.BUN_VERSION }}
install-command: bun install --ignore-scripts --no-frozen-lockfile
install-native-deps: "false"
run-postinstall: "true"
skip-avatar-clone: "true"
no-vision-deps: "true"
- name: Generate protobuf types
# Restores the step that was on the last green Mobile Build Smoke
# run (52ab686a, 2026-05-09). Without these generated types in
# `packages/core/src/types/generated/`, `@elizaos/core`'s build
# fails downstream, which cascades into `@elizaos/shared` and
# vite config resolution.
run: |
if [ -d packages/schemas ] && [ -f packages/schemas/buf.gen.yaml ] && [ ! -d packages/core/src/types/generated ]; then
cd packages/schemas
node node_modules/@bufbuild/buf/install.js 2>/dev/null || true
bunx @bufbuild/buf@1.67.0 generate
fi
- name: Build Vite config package deps
# vite.config.ts imports dist-only workspace packages
# (`@elizaos/shared` and `@elizaos/ui/config/app-config`), and the
# app aliases plugin-browser source which imports `@elizaos/vault`.
# Build those package chains before Vite evaluates/bundles the app.
run: bun turbo run build --filter=@elizaos/ui --filter=@elizaos/vault
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null 2>&1 || true
# Build the mobile agent bundle (Phase D) before Capacitor's
# Android sync needs it. The bundler dedupes @elizaos/core,
# @elizaos/shared, and @elizaos/plugin-sql to their src trees, so
# the workspace plugin packages have to be present in
# node_modules. setup-bun-workspace handles install; we just kick
# off the bundle here.
- name: Build mobile agent bundle
run: bun run --cwd packages/agent build:mobile
# Guard against the May-5 Bun.build TLA regression that required
# hot-patching `s/init_eliza()/await init_eliza()/` on every device
# bundle. The fix is in 12bfccb481 (lazy plugin-sql resolution from
# ensureCoreStaticPluginsRegistered); a regression would re-emit
# `var init_eliza = __esm(async () => {...})`. See
# docs/porting/on-device-quantization-porting-plan.md §"Current
# state on the AOSP image" for context.
- name: Verify init_eliza is sync (no TLA regression)
run: |
BUNDLE=packages/agent/dist-mobile/agent-bundle.js
if grep -qE 'var init_eliza\b[^=]*= __esm\(async ' "$BUNDLE"; then
echo "::error::init_eliza re-emitted as async __esm — Bun.build cross-module TLA regression"
exit 1
fi
echo "init_eliza sync emit confirmed; no TLA regression"
- name: Build Android cloud debug app
run: node --max-old-space-size=8192 packages/app-core/scripts/run-mobile-build.mjs android-cloud-debug
env:
NODE_OPTIONS: "--max-old-space-size=8192"
# This smoke job intentionally installs no native deps.
# Real local/AOSP Android builds still fail if fork llama libs are
# missing; the opt-out only lets CI validate Capacitor/Gradle wiring.
ELIZA_ANDROID_SKIP_FORK_LLAMA_LIB: "1"
ELIZA_MOBILE_BUILD_TIMING_OUT: ${{ runner.temp }}/android-cloud-debug-build-timing.json
ELIZA_MOBILE_BUILD_TIMING_TARGET: android-apk
- name: Check Android cloud-debug build-time budget
run: |
bun run --cwd packages/app perf:startup-budget -- \
--build-timing "$RUNNER_TEMP/android-cloud-debug-build-timing.json" \
--out "$RUNNER_TEMP/android-cloud-debug-build-budget-report.json"
- name: Upload Android cloud-debug build budget report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: android-cloud-debug-build-budget
path: |
${{ runner.temp }}/android-cloud-debug-build-timing.json
${{ runner.temp }}/android-cloud-debug-build-budget-report.json
if-no-files-found: warn
retention-days: 7
- name: Verify APK exists
run: |
APK_PATH="packages/app-core/platforms/android/app/build/outputs/apk/debug/app-debug.apk"
if [ ! -f "$APK_PATH" ]; then
echo "::error::Debug APK not found at $APK_PATH"
exit 1
fi
APK_SIZE=$(stat -c%s "$APK_PATH" 2>/dev/null || stat -f%z "$APK_PATH")
echo "Debug APK built: $APK_PATH ($APK_SIZE bytes)"
- name: Upload debug APK
if: ${{ hashFiles('packages/app-core/platforms/android/app/build/outputs/apk/debug/app-debug.apk') != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: android-debug-apk
path: packages/app-core/platforms/android/app/build/outputs/apk/debug/app-debug.apk
retention-days: 7
if-no-files-found: error