name: Tests # Comprehensive test orchestrator. Runs the full eliza test suite plus # focused server/client/plugin slices and the database security guard. # This is a peer of `ci.yaml` — `ci.yaml` is the canonical lint+typecheck+ # build+core-test gate; this workflow adds the broader integration surface. on: # Post-merge integration surface. PRs to `develop` run the lightweight # lint/typecheck/build gate (develop-pr.yml); the full suite runs here once # changes land on `develop`, plus nightly and on demand. push: branches: [develop] workflow_dispatch: schedule: # Nightly live smoke for container-backed remote capability plugins. - cron: "17 9 * * *" # CI fan-out fix: the PR-scoped concurrency from #14069 fell back to # github.run_id on push, so every develop push got a unique group and # nothing ever superseded. A 37-merge wave queued thousands of push runs # and starved the self-hosted fleet. Fall back to github.ref (all develop # pushes share a group) and cancel superseded runs on push too. PR behavior # is unchanged (pull_request.number still wins the key); merge_group and # schedule events are not in the cancel set, so the merge queue and nightly # runs always complete. concurrency: group: test-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} env: CI: "true" # pinned: floating canary hangs `bun install` on hosted CI runners and writes # lockfileVersion 2 which breaks --frozen-lockfile (#11184/#9454). This env # drives the integration/unit/lint/cloud-live-e2e jobs' bun-version input. BUN_VERSION: "1.3.14" NODE_VERSION: "24.15.0" NODE_NO_WARNINGS: "1" NODE_OPTIONS: "--max-old-space-size=8192" # Baseline PR jobs are secret-free. Dedicated live jobs inject provider keys # locally and remain opt-in/scheduled where external services are involved. # Default to least privilege. Override per-job where needed. permissions: contents: read jobs: stale-base-preflight: name: PR stale-base preflight # PR-only in EFFECT — every step gates on github.event_name == # 'pull_request' — but the JOB itself runs (and no-op-succeeds) on every # event. It used to be job-level `if: github.event_name == 'pull_request'`, # which SKIPPED it on push; because `changes` needs this job and GitHub # evaluates the implicit success() over the TRANSITIVE needs chain # (actions/runner#491), that one skipped ancestor silently skip-cascaded # every push-event lane downstream of `changes` whose `if` carries no # status-check function — develop push runs went vacuously red from the # moment the needs edge landed. Step-level gating keeps the PR-only # contract (ci-merge-gate-contract.mjs) and the `changes` ordering while # letting push/schedule/dispatch fan-outs actually run. runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 10 env: BASE_REF: ${{ github.event.pull_request.base.ref }} HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} ACK: ${{ contains(github.event.pull_request.labels.*.name, 'stale-base-ack') && '1' || '' }} steps: - name: Checkout target branch (guard logic comes from the base, not the PR) if: github.event_name == 'pull_request' uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: ref: ${{ github.event.pull_request.base.ref }} # The guard disables lazy promisor fetches while it runs. Fetch the # same bounded history window up front so merge commits can resolve # their parents without network fallback. fetch-depth: 1500 filter: blob:none submodules: false - name: Setup Node.js for stale-base guard if: github.event_name == 'pull_request' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Fetch PR head + bounded history (blobless) if: github.event_name == 'pull_request' run: | set -euo pipefail git fetch --quiet --no-tags --filter=blob:none --depth=1500 \ "https://github.com/${HEAD_REPO}.git" "$HEAD_SHA" git fetch --quiet --no-tags --filter=blob:none --depth=1500 origin \ "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" - name: Self-test the guard on fixture repos if: github.event_name == 'pull_request' run: node packages/scripts/stale-base-guard.self-test.mjs - name: Detect silent reverts + stale base before fanning out tests if: github.event_name == 'pull_request' env: GIT_NO_LAZY_FETCH: "1" run: | set -euo pipefail node packages/scripts/stale-base-guard.mjs \ --base "refs/remotes/origin/${BASE_REF}" \ --head "$HEAD_SHA" \ --window 1200 \ --max-behind-commits 200 \ --max-behind-hours 72 \ ${ACK:+--ack} \ --github \ --summary "$GITHUB_STEP_SUMMARY" changes: name: Classify changed paths needs: stale-base-preflight if: always() && (github.event_name != 'pull_request' || needs.stale-base-preflight.result == 'success') # Route with the same fleet-aware conditional as the test lanes. GitHub-hosted # runners are billing-frozen (#13481), so a hardcoded ubuntu-24.04 here queued # forever and — since every lane needs this classifier — stalled the entire # ci-ok chain while the self-hosted fleet sat idle. Self-hosted when online; # hosted only when an operator flags the fleet down (and billing is restored). runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 10 outputs: server: ${{ steps.filter.outputs.server }} client: ${{ steps.filter.outputs.client }} plugins: ${{ steps.filter.outputs.plugins }} desktop: ${{ steps.filter.outputs.desktop }} zero_key: ${{ steps.filter.outputs.zero_key }} cloud: ${{ steps.filter.outputs.cloud }} 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: ${{ env.NODE_VERSION }} - name: Validate CI path gate run: node packages/scripts/ci-path-gate.self-test.mjs - name: Validate CI workflow dedup contract run: node packages/scripts/ci-workflow-dedup-contract.mjs - name: Validate GitHub-native turbo cache contract run: node packages/scripts/ci-turbo-cache-contract.mjs - name: Validate CI Bun version pin contract run: node packages/scripts/ci-bun-version-contract.mjs - name: Validate zero-key command ownership contract run: node packages/scripts/ci-zero-key-command-ownership-contract.mjs - name: Validate Windows command coverage contract run: node packages/scripts/ci-windows-command-coverage-contract.mjs - name: Install alias-read guard parser dependency run: npm install --prefix "$RUNNER_TEMP/alias-read-guard-deps" --no-save --no-package-lock --ignore-scripts typescript@6.0.3 - name: Validate brand-env alias-read guard self-test env: NODE_PATH: ${{ runner.temp }}/alias-read-guard-deps/node_modules run: node packages/scripts/alias-read-guard.mjs --self-test - name: Validate merge-gate robustness contract self-test run: node packages/scripts/ci-merge-gate-contract.mjs --self-test - name: Validate merge-gate robustness contract run: node packages/scripts/ci-merge-gate-contract.mjs - name: Determine affected test lanes id: filter shell: bash env: PR_LABELS: ${{ github.event_name == 'pull_request' && join(github.event.pull_request.labels.*.name, ',') || '' }} BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }} HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} run: | set -euo pipefail if [ "${GITHUB_EVENT_NAME}" = "merge_group" ]; then { echo "server=true" echo "client=true" echo "plugins=true" echo "desktop=true" echo "zero_key=true" echo "cloud=true" } >> "$GITHUB_OUTPUT" { echo "### Tests path gate" echo echo "Merge queue runs validate the synthesized queue SHA, so path" echo "diffs are deliberately not used. Running the full required" echo "test suite for the \`ci-ok\` aggregate check." } >> "$GITHUB_STEP_SUMMARY" exit 0 fi node packages/scripts/ci-path-gate.mjs \ --config test \ --event "${{ github.event_name }}" \ --base "$BASE_SHA" \ --head "$HEAD_SHA" \ --labels "$PR_LABELS" \ --output "$GITHUB_OUTPUT" \ --summary "$GITHUB_STEP_SUMMARY" test-runner-vacuous-green-guard: name: Test-runner vacuous-green guard needs: changes # #13620/#12342: this guard protects the ordinary PR path from runner lanes # that collect no real tests or swallow a failure as "no tests found". # Keep it outside path gates and outside the opt-in `ci:full` heavy family. runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 15 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: ${{ env.BUN_VERSION }} - name: Test-runner vacuous-green guard run: bun test packages/scripts/__tests__/run-all-tests-vacuous-green-guard.test.ts server-tests: name: Server Tests needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.server == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 60 env: PGLITE_WASM_MODE: node steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" - name: Build view bundles run: bun run build:views - name: Views system tests run: bunx vitest run packages/agent/src/__tests__/views-system-smoke.test.ts packages/agent/src/__tests__/views-registry-integration.test.ts packages/agent/src/__tests__/plugin-tui-view-coverage.test.ts - name: Remote capability router tests run: bun run test:remote-capabilities - name: Remote capability plugin surface audit run: bun run test:remote-capabilities:surface-audit - name: Remote capability live CI audit run: bun run test:remote-capabilities:live-ci-audit - name: Remote capability live CI audit self-test run: bun run test:remote-capabilities:live-ci-audit:self-test - name: Remote capability naming audit run: bun run test:remote-capabilities:naming-audit - name: Remote capability naming audit self-test run: bun run test:remote-capabilities:naming-audit:self-test - name: Remote capability source-build smoke run: bun run test:remote-capabilities:source-build - name: Remote capability fixture server smoke run: bun run test:remote-capabilities:fixture-server - name: Remote capability live report validator self-test run: bun run test:remote-capabilities:validate-live-reports:self-test - name: Remote capability GitHub live evidence self-test run: bun run test:remote-capabilities:github-live-evidence:self-test - name: Remote capability GitHub live artifact self-test run: bun run test:remote-capabilities:github-live-artifacts:self-test - name: Remote capability Docker smoke run: bun run test:remote-capabilities:docker - name: Run server tests run: bun run test:server client-tests: name: Client Tests needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.client == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 60 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" - name: Run client tests run: bun run test:client - name: Install Playwright Chromium run: .github/scripts/install-playwright-browsers.sh chromium - name: Remote capability UI smoke run: bun run test:remote-capabilities:ui # Perf gate (#9954): drive REAL overlay thread-scroll + pull-to-maximize / # top-pull-restore in headless chromium, feed REAL rAF/longtask/layout-shift # entries through the shared frame-budget + layout-stability detectors, # HARD-FAIL on dropped frames, p95 frame time, or CLS. The gate first proves # its CLS detector has teeth by injecting a real non-transient shift and # asserting it is flagged (#14333), so a true CLS of 0.0000 cannot be a # vacuous pass from a dead observer or an over-broad transient marker. - name: Perf gate e2e (#9954) run: bun run --cwd packages/ui test:perf-gate-e2e - name: Upload perf gate e2e artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: perf-gate-e2e-artifacts path: packages/ui/src/components/shell/__e2e__/output-perf-gate-e2e/ if-no-files-found: ignore plugin-tests: name: Plugin Tests (${{ matrix.shard }}/4) needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.plugins == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 95 strategy: fail-fast: false matrix: shard: [1, 2, 3, 4] env: TEST_SHARD: ${{ matrix.shard }}/4 # Share compiled rust artifacts across all plugin crates so the # `elizaos = "2.0.0"` crate (and its sqlx/hyper/etc transitive deps) # only compiles once, not once per plugin. Without this, each # plugin-{discord,groq,openai,sql,whatsapp} rebuilds the same heavy # dep tree from scratch, blowing past the step timeout. CARGO_TARGET_DIR: ${{ github.workspace }}/.cargo-shared-target CARGO_INCREMENTAL: "0" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false fetch-depth: 1 - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" - name: Cache cargo registry and shared target dir uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: | ~/.cargo/registry ~/.cargo/git ${{ github.workspace }}/.cargo-shared-target key: ${{ runner.os }}-cargo-plugin-tests-${{ hashFiles('plugins/*/rust/Cargo.lock', 'plugins/*/rust/Cargo.toml') }} restore-keys: | ${{ runner.os }}-cargo-plugin-tests- # The view-bundle coverage tests assert every view plugin has a built # dist/views/bundle.js; build them before running plugin tests. - name: Build view bundles run: bun run build:views - name: Run plugin tests # Cap step below the 95m job budget so a flaky plugin # (most recently plugin-shell rust PTY tests in run 25234887145) # fails fast instead of burning the whole job's wall clock. timeout-minutes: 85 run: bun run test:plugins - name: Prune cargo cache payload if: always() run: | node packages/scripts/rm-path-recursive.mjs \ "${CARGO_TARGET_DIR}/debug/incremental" \ "${CARGO_TARGET_DIR}/debug/.fingerprint" \ "${CARGO_TARGET_DIR}/debug/build" \ "${CARGO_TARGET_DIR}/debug/examples" \ "${CARGO_TARGET_DIR}/release/incremental" \ "${CARGO_TARGET_DIR}/release/.fingerprint" \ "${CARGO_TARGET_DIR}/release/build" \ "${CARGO_TARGET_DIR}/release/examples" find "${CARGO_TARGET_DIR}" -type f \( -name '*.d' -o -name '*.rlib' -o -name '*.rmeta' \) -delete 2>/dev/null || true plugin-tests-status: name: Plugin Tests needs: - changes - plugin-tests if: ${{ !cancelled() && always() }} # Fleet-aware (see `changes`): hardcoded hosted froze this ci-ok dependency. runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 10 steps: - name: Check plugin shard results shell: bash run: | set -u result="${{ needs.plugin-tests.result }}" echo "plugin-tests matrix: ${result}" if [ "$result" = "success" ]; then exit 0 fi if [ "$result" = "skipped" ] && [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ needs.changes.outputs.plugins }}" != "true" ]; then echo "::notice title=Plugin Tests::Path gate skipped plugin tests for this PR context." exit 0 fi echo "::error title=Plugin Tests::Plugin shard matrix finished with '${result}'" exit 1 # First CI consumer of the repo-level integration lane # (packages/test/vitest/integration.config.ts). The lane was dead in CI: # nothing invoked the config, its @elizaos/core alias broke the /node # subpath, and its eliza/-prefixed paths matched zero files in a flat # checkout (#11047). Runs plugin-personal-assistant's test:integration — # the src-integration lane (scheduler/recurrence/global-pause real-DB # suites) plus the repo config over the package's named test/ files — # keyless. Burn down the remaining plugins/*/test integration suites from # here. integration-tests: name: Integration Lane (personal-assistant) needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.plugins == 'true' || needs.changes.outputs.server == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 45 env: PGLITE_WASM_MODE: node steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" # The personal-assistant barrel imports sibling plugin dists (e.g. # plugin-calendly pulls "@elizaos/core/node") straight from # node_modules, so the lane needs the package's workspace dependency # graph built. Turbo replays warm runs from cache. - name: Build personal-assistant dependency graph run: node packages/scripts/run-turbo.mjs run build --filter='@elizaos/plugin-personal-assistant...' - name: Run personal-assistant integration lanes timeout-minutes: 35 run: bun run --cwd plugins/plugin-personal-assistant test:integration desktop-contract: name: Electrobun Desktop Contract needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.desktop == '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: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" - name: Build workspace packages for Vitest resolution run: bun run build:core - name: Run Electrobun contract tests working-directory: packages/app-core/platforms/electrobun run: bun run test zero-key-unit-coverage: name: Zero-Key unit + UI coverage needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.zero_key == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 30 env: TEST_LANE: "pr" ELIZA_LIVE_TEST: "0" SCENARIO_USE_LLM_PROXY: "1" CEREBRAS_API_KEY: "" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" GROQ_API_KEY: "" OPENROUTER_API_KEY: "" GOOGLE_GENERATIVE_AI_API_KEY: "" XAI_API_KEY: "" NVIDIA_API_KEY: "" ELIZAOS_CLOUD_API_KEY: "" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - 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" skip-avatar-clone: "true" no-vision-deps: "true" # The scenario CLI imports @elizaos/core from source, which imports the # gitignored ./generated/validation-keyword-data.ts. --ignore-scripts skips # postinstall, so generate it explicitly before any step resolves core. - name: Ensure generated shared i18n data run: node packages/app-core/scripts/ensure-shared-i18n-data.mjs - name: Scenario runner unit coverage run: bun run --cwd packages/scenario-runner test -- src/executor.test.ts src/runtime-factory.test.ts src/scenario-pr-workflow.test.ts - name: Deterministic LLM proxy unit coverage working-directory: packages run: bunx vitest run --config test/mocks/vitest.config.ts test/mocks/__tests__/llm-proxy-plugin.test.ts - name: UI voice interaction coverage run: bun run --cwd packages/ui test -- src/hooks/useVoiceChat.bidirectional.test.tsx src/hooks/useContinuousChat.test.tsx - name: UI dynamic view interaction coverage run: bun run --cwd packages/ui test -- src/App.navigate-view-wiring.test.tsx src/app-navigate-view.test.ts src/components/shell/__tests__/shell-assistant-flow.test.tsx src/components/views/DynamicViewLoader.test.tsx src/hooks/useAvailableViews.test.tsx src/hooks/useDesktopTabs.test.tsx src/state/startup-phase-hydrate.view-interact.test.ts - name: App route + ui-smoke spec coverage gates run: bun run --cwd packages/app test -- test/route-coverage.test.ts test/ui-smoke-coverage.test.ts test/view-interaction-coverage.test.ts zero-key-browser: name: Zero-Key app browser needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.zero_key == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 35 env: TEST_LANE: "pr" ELIZA_LIVE_TEST: "0" SCENARIO_USE_LLM_PROXY: "1" CEREBRAS_API_KEY: "" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" GROQ_API_KEY: "" OPENROUTER_API_KEY: "" GOOGLE_GENERATIVE_AI_API_KEY: "" XAI_API_KEY: "" NVIDIA_API_KEY: "" ELIZAOS_CLOUD_API_KEY: "" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - 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" skip-avatar-clone: "true" no-vision-deps: "true" - name: Ensure generated shared i18n data run: node packages/app-core/scripts/ensure-shared-i18n-data.mjs - name: Install Playwright Chromium + WebKit run: .github/scripts/install-playwright-browsers.sh chromium webkit - name: Actual app assistant pill browser coverage run: bun run --cwd packages/app test:e2e test/ui-smoke/assistant-home-flow.spec.ts --project=chromium -g "captures first-run, assistant home, chat suppression, and view pill states" - name: Actual app assistant voice browser coverage run: bun run --cwd packages/app test:e2e test/ui-smoke/assistant-home-flow.spec.ts --project=chromium -g "drives the assistant home voice path with a scripted browser STT turn" - name: Actual app TTS and STT browser coverage run: bun run --cwd packages/app test:e2e test/ui-smoke/tts-stt-e2e.spec.ts --project=chromium - name: Actual app plugin view browser coverage run: bun run --cwd packages/app test:e2e test/ui-smoke/plugin-views-visual.spec.ts test/ui-smoke/plugin-views-interaction.spec.ts test/ui-smoke/orchestrator-gui-workbench.spec.ts test/ui-smoke/screenshare-gui-interactions.spec.ts test/ui-smoke/task-coordinator-gui-interactions.spec.ts --project=chromium # WebKit cross-engine pointer/focus lane (#10722): the playwright config's # opt-in `webkit` project (Desktop Safari) over the keyless chat # pointer/focus/composer specs. Without this step the lane never runs # anywhere — Chromium-only coverage ships WebKit-specific regressions to # iOS/iPadOS Safari unseen. - name: Actual app WebKit pointer/focus coverage run: PLAYWRIGHT_WEBKIT=1 bun run --cwd packages/app test:e2e --project=webkit - name: Upload zero-key browser artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: tests-zero-key-browser path: | packages/app/playwright-report/ packages/app/test-results/ if-no-files-found: ignore retention-days: 3 zero-key-diagnostics: name: Zero-Key diagnostics needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.zero_key == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 30 env: TEST_LANE: "pr" ELIZA_LIVE_TEST: "0" SCENARIO_USE_LLM_PROXY: "1" CEREBRAS_API_KEY: "" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" GROQ_API_KEY: "" OPENROUTER_API_KEY: "" GOOGLE_GENERATIVE_AI_API_KEY: "" XAI_API_KEY: "" NVIDIA_API_KEY: "" ELIZAOS_CLOUD_API_KEY: "" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - 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" skip-avatar-clone: "true" no-vision-deps: "true" - name: Ensure generated shared i18n data run: node packages/app-core/scripts/ensure-shared-i18n-data.mjs - name: Electrobun window and dynamic-view coverage run: bun run --cwd packages/app-core/platforms/electrobun test src/native/desktop-window.test.ts src/rpc-handlers.test.ts src/dynamic-view-rpc-schema.test.ts src/surface-windows.test.ts src/dynamic-views/host.test.ts src/application-menu.test.ts - name: View management action coverage run: bun run --cwd plugins/plugin-app-control test -- src/actions/views-management.test.ts - name: App verification diagnostics coverage run: bun run --cwd plugins/plugin-app-control test -- src/services/__tests__/app-verification.integration.test.ts src/services/__tests__/verification-room-bridge.test.ts - name: Computer-use screenshot quality diagnostics coverage run: bun run --cwd plugins/plugin-computeruse test -- test/helpers/screenshot-quality.test.ts src/__tests__/browser-auto-open.test.ts - name: App screenshot quality diagnostics coverage run: bun run --cwd packages/app test -- test/screenshot-quality.test.ts - name: App-core screenshot quality diagnostics coverage run: bun run --cwd packages/app-core test -- test/app/screenshot-quality.test.ts zero-key-scenario-runner: name: Zero-Key scenario runner needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.zero_key == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 30 env: TEST_LANE: "pr" ELIZA_LIVE_TEST: "0" SCENARIO_USE_LLM_PROXY: "1" CEREBRAS_API_KEY: "" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" GROQ_API_KEY: "" OPENROUTER_API_KEY: "" GOOGLE_GENERATIVE_AI_API_KEY: "" XAI_API_KEY: "" NVIDIA_API_KEY: "" ELIZAOS_CLOUD_API_KEY: "" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - 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" skip-avatar-clone: "true" no-vision-deps: "true" # The scenario CLI imports @elizaos/core from source, which imports the # gitignored ./generated/validation-keyword-data.ts. --ignore-scripts skips # postinstall, so generate it explicitly before any step resolves core. - name: Ensure generated shared i18n data run: node packages/app-core/scripts/ensure-shared-i18n-data.mjs - name: Scenario catalog coverage gate run: node packages/scripts/check-scenario-workflow-coverage.mjs --report-dir reports/scenarios/catalog-inventory - name: Persona catalog coverage gate run: node packages/scripts/check-lifeops-persona-catalog-coverage.mjs - name: Build workspace packages for scenario resolution run: bunx turbo run build --filter=@elizaos/scenario-runner... - name: Zero-cost scenario e2e run: bun run --cwd packages/scenario-runner test:pr:e2e - name: Upload deterministic E2E reports if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: tests-zero-key-e2e-report path: | reports/scenarios/pr-deterministic/ reports/scenarios/pr-deterministic-corpus/ reports/scenarios/pr-deterministic-orchestrator/ reports/scenarios/pr-deterministic-lifeops/ reports/scenarios/catalog-inventory/ if-no-files-found: ignore retention-days: 3 - name: Prune deterministic E2E scratch output if: always() run: node packages/scripts/rm-path-recursive.mjs reports/scenarios/pr-deterministic reports/scenarios/pr-deterministic-corpus reports/scenarios/pr-deterministic-orchestrator reports/scenarios/pr-deterministic-lifeops reports/scenarios/catalog-inventory packages/app/playwright-report packages/app/test-results zero-key-harness-e2e: name: Zero-Key harness E2E needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.zero_key == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 15 env: TEST_LANE: "pr" ELIZA_LIVE_TEST: "0" SCENARIO_USE_LLM_PROXY: "1" CEREBRAS_API_KEY: "" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" GROQ_API_KEY: "" OPENROUTER_API_KEY: "" GOOGLE_GENERATIVE_AI_API_KEY: "" XAI_API_KEY: "" NVIDIA_API_KEY: "" ELIZAOS_CLOUD_API_KEY: "" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - 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" skip-avatar-clone: "true" no-vision-deps: "true" - name: Ensure generated shared i18n data run: node packages/app-core/scripts/ensure-shared-i18n-data.mjs - name: Keyless harness e2e (mock-LLM + recorded fixtures) working-directory: packages run: bunx vitest run --config test/mocks/vitest.config.ts test/mocks/__tests__/ - name: Per-plugin keyless harness adoption proofs run: | bun run --cwd plugins/plugin-anthropic test:harness bun run --cwd plugins/plugin-discord test:harness zero-key-e2e: name: Zero-Key Deterministic E2E needs: - changes - zero-key-unit-coverage - zero-key-browser - zero-key-diagnostics - zero-key-scenario-runner - zero-key-harness-e2e if: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || needs.changes.outputs.zero_key == 'true') }} runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} steps: - name: Check zero-key slices shell: bash run: | set -u bad=0 for pair in \ "zero-key-unit-coverage:${{ needs.zero-key-unit-coverage.result }}" \ "zero-key-browser:${{ needs.zero-key-browser.result }}" \ "zero-key-diagnostics:${{ needs.zero-key-diagnostics.result }}" \ "zero-key-scenario-runner:${{ needs.zero-key-scenario-runner.result }}" \ "zero-key-harness-e2e:${{ needs.zero-key-harness-e2e.result }}"; do name="${pair%%:*}" result="${pair##*:}" echo "$name: $result" if [ "$result" != "success" ]; then echo "::error title=Tests zero-key::$name finished with $result" bad=1 fi done if [ "$bad" -ne 0 ]; then exit 1 fi echo "All zero-key slices passed." cloud-live-e2e: name: Cloud Live E2E (Eliza Cloud) needs: changes if: github.event_name != 'pull_request' || needs.changes.outputs.cloud == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 60 outputs: skip: ${{ steps.cloud.outputs.skip }} capability_skip: ${{ steps.cloud.outputs.capability_skip }} steps: - name: Check cloud secrets id: cloud env: EVENT_NAME: ${{ github.event_name }} ELIZAOS_CLOUD_API_KEY: ${{ secrets.ELIZAOS_CLOUD_API_KEY != '' && secrets.ELIZAOS_CLOUD_API_KEY || secrets.ELIZACLOUD_API_KEY }} ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED: ${{ vars.ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED != '' && vars.ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED || secrets.ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED }} run: | strict_context=false case "${EVENT_NAME}" in workflow_dispatch|schedule) strict_context=true ;; esac if [ -z "${ELIZAOS_CLOUD_API_KEY}" ]; then message="No Eliza Cloud API key configured" if [ "${strict_context}" = "true" ]; then echo "::error::${message}; strict ${EVENT_NAME} live lanes must fail instead of greening by skip." exit 1 fi echo "::warning::${message} - skipping optional cloud live E2E for ${EVENT_NAME}." echo "skip=true" >> "$GITHUB_OUTPUT" echo "capability_skip=true" >> "$GITHUB_OUTPUT" exit 0 else echo "skip=false" >> "$GITHUB_OUTPUT" fi if [ "${ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED}" = "1" ]; then echo "capability_skip=false" >> "$GITHUB_OUTPUT" else if [ "${EVENT_NAME}" = "workflow_dispatch" ] || [ "${EVENT_NAME}" = "schedule" ]; then echo "::error::Remote capability cloud sandbox live smoke is required for ${EVENT_NAME}; set ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED=1 instead of producing a green skip." exit 1 fi echo "::notice::Remote capability cloud sandbox live smoke skipped because ELIZA_REMOTE_CAPABILITY_CLOUD_LIVE_ENABLED is not configured." echo "capability_skip=true" >> "$GITHUB_OUTPUT" fi - name: Checkout if: steps.cloud.outputs.skip != 'true' uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js if: steps.cloud.outputs.skip != 'true' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies if: steps.cloud.outputs.skip != 'true' uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" - name: Run Cloud tests if: steps.cloud.outputs.skip != 'true' shell: bash run: | set -o pipefail log_file="$(mktemp)" if bun run --cwd packages/cloud/sdk test:e2e 2>&1 | tee "$log_file"; then exit 0 fi # Soft-fail ONLY on genuine provider-side unavailability (HTTP 429/5xx, quota # exhaustion, or network errors) that a contributor can't control. Client-side # failures must redden the job so real regressions aren't masked: 4xx auth/permission # (e.g. 403 access_denied / "missing required permission"), assertion failures and # timeouts all fall through to exit 1. Patterns are anchored to HTTP status codes and # error tokens so they can't collide with a benign test title (the old allowlist # matched "credit balance" from the test name "gets credit balance and summary", # swallowing every failure on every run). if grep -Eiq '(status code|statusCode|HTTP)[: ]*429|too many requests|rate.?limit|(status code|statusCode|HTTP)[: ]*5[0-9][0-9]|service unavailable|internal server error|temporarily unavailable|overloaded|exceeded your current quota|quota exceeded|insufficient[_ -]?(quota|balance|funds|credits?)|please add credits|top[_ -]?up your credits|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|socket hang up' "$log_file"; then echo "::warning::Cloud Live E2E soft-failed: external provider unavailable (HTTP 429/5xx, quota, or network error). Genuine test failures are NOT suppressed." exit 0 fi exit 1 env: ELIZA_LIVE_TEST: "1" ELIZAOS_CLOUD_API_KEY: ${{ secrets.ELIZAOS_CLOUD_API_KEY != '' && secrets.ELIZAOS_CLOUD_API_KEY || secrets.ELIZACLOUD_API_KEY }} ELIZA_CLOUD_API_KEY: ${{ secrets.ELIZAOS_CLOUD_API_KEY != '' && secrets.ELIZAOS_CLOUD_API_KEY || secrets.ELIZACLOUD_API_KEY }} ELIZAOS_CLOUD_BASE_URL: ${{ secrets.ELIZAOS_CLOUD_BASE_URL }} ELIZA_CLOUD_BASE_URL: ${{ secrets.ELIZAOS_CLOUD_BASE_URL }} ELIZA_CLOUD_API_BASE_URL: ${{ secrets.ELIZAOS_CLOUD_API_BASE_URL }} - name: Prepare remote capability cloud live report directory if: steps.cloud.outputs.capability_skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: | node packages/scripts/rm-path-recursive.mjs reports/remote-capabilities/cloud mkdir -p reports/remote-capabilities/cloud - name: Remote capability cloud sandbox live smoke if: steps.cloud.outputs.capability_skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: bun run --cwd packages/agent test:remote-capabilities:cloud-live env: ELIZA_LIVE_TEST: "1" ELIZA_REMOTE_CAPABILITY_LIVE_REPORT_DIR: reports/remote-capabilities/cloud ELIZAOS_CLOUD_API_KEY: ${{ secrets.ELIZAOS_CLOUD_API_KEY != '' && secrets.ELIZAOS_CLOUD_API_KEY || secrets.ELIZACLOUD_API_KEY }} ELIZAOS_CLOUD_BASE_URL: ${{ secrets.ELIZAOS_CLOUD_BASE_URL }} ELIZA_CLOUD_BASE_URL: ${{ secrets.ELIZAOS_CLOUD_BASE_URL }} - name: Validate remote capability cloud live report if: steps.cloud.outputs.capability_skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: bun run test:remote-capabilities:validate-live-reports --kind cloud --expect-count 1 --max-age-minutes 90 --max-future-minutes 5 --require-ci --require-file-identity --match-github-env reports/remote-capabilities/cloud - name: Upload remote capability cloud live report if: steps.cloud.outputs.capability_skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: remote-capability-cloud-live-report path: reports/remote-capabilities/cloud/*.json if-no-files-found: error retention-days: 7 - name: Prune remote capability cloud report if: always() && steps.cloud.outputs.capability_skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: node packages/scripts/rm-path-recursive.mjs reports/remote-capabilities/cloud - name: Note remote capability cloud live smoke not observed if: steps.cloud.outputs.skip != 'true' && github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' run: | echo "::notice::Remote capability cloud sandbox live smoke is only observed on workflow_dispatch or schedule events." { echo "### Remote capability cloud live smoke" echo "" echo "Not observed for event \`${{ github.event_name }}\`. The full-surface cloud sandbox smoke runs only on \`workflow_dispatch\` and \`schedule\`." } >> "$GITHUB_STEP_SUMMARY" provider-live-e2e: name: Remote Capability Provider Live E2E runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 45 outputs: skip: ${{ steps.providers.outputs.skip }} steps: - name: Check provider endpoint secrets id: providers env: EVENT_NAME: ${{ github.event_name }} E2B_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_E2B_URL }} HOME_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_URL }} MOBILE_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_URL }} DESKTOP_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_URL }} run: | strict_context=false case "${EVENT_NAME}" in workflow_dispatch|schedule) strict_context=true ;; esac if [ -z "${E2B_URL}${HOME_URL}${MOBILE_URL}${DESKTOP_URL}" ]; then message="No remote capability provider endpoints configured" if [ "${strict_context}" = "true" ]; then echo "::error::${message}; strict ${EVENT_NAME} live lanes must fail instead of greening by skip." exit 1 fi echo "::warning::${message} - skipping optional provider live E2E." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi missing_required=() if [ -z "${E2B_URL}" ]; then missing_required+=("ELIZA_REMOTE_CAPABILITY_E2B_URL") fi if [ -z "${HOME_URL}" ]; then missing_required+=("ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_URL") fi if [ -z "${MOBILE_URL}" ]; then missing_required+=("ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_URL") fi if [ "${#missing_required[@]}" -gt 0 ]; then message="Missing required remote capability provider endpoint secrets: ${missing_required[*]}" if [ "${strict_context}" = "true" ]; then echo "::error::${message}; strict ${EVENT_NAME} live lanes must fail instead of greening by skip." exit 1 fi echo "::warning::${message}. Skipping optional provider live E2E for ${EVENT_NAME}." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Checkout if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup Node.js if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" - name: Prepare remote capability provider live report directory if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: | node packages/scripts/rm-path-recursive.mjs reports/remote-capabilities/providers mkdir -p reports/remote-capabilities/providers - name: Remote capability URL-backed provider live smoke if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: bun run --cwd packages/agent test:remote-capabilities:provider-live env: ELIZA_LIVE_TEST: "1" ELIZA_REMOTE_CAPABILITY_LIVE_REPORT_DIR: reports/remote-capabilities/providers ELIZA_REMOTE_CAPABILITY_E2B_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_E2B_URL }} ELIZA_REMOTE_CAPABILITY_E2B_TOKEN: ${{ secrets.ELIZA_REMOTE_CAPABILITY_E2B_TOKEN }} ELIZA_REMOTE_CAPABILITY_E2B_ENDPOINT_ID: ${{ secrets.ELIZA_REMOTE_CAPABILITY_E2B_ENDPOINT_ID }} ELIZA_REMOTE_CAPABILITY_E2B_MODULES: ${{ secrets.ELIZA_REMOTE_CAPABILITY_E2B_MODULES }} ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_URL }} ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_TOKEN: ${{ secrets.ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_TOKEN }} ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_ENDPOINT_ID: ${{ secrets.ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_ENDPOINT_ID }} ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_MODULES: ${{ secrets.ELIZA_REMOTE_CAPABILITY_HOME_MACHINE_MODULES }} ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_URL }} ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_TOKEN: ${{ secrets.ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_TOKEN }} ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_ENDPOINT_ID: ${{ secrets.ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_ENDPOINT_ID }} ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_MODULES: ${{ secrets.ELIZA_REMOTE_CAPABILITY_MOBILE_COMPANION_MODULES }} ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_URL: ${{ secrets.ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_URL }} ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_TOKEN: ${{ secrets.ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_TOKEN }} ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_ENDPOINT_ID: ${{ secrets.ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_ENDPOINT_ID }} ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_MODULES: ${{ secrets.ELIZA_REMOTE_CAPABILITY_DESKTOP_COMPANION_MODULES }} - name: Validate remote capability provider live reports if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: bun run test:remote-capabilities:validate-live-reports --kind provider --expect-count 3..4 --max-age-minutes 90 --max-future-minutes 5 --allowed-providers e2b,home-machine,mobile-companion,desktop-companion --require-providers e2b,home-machine,mobile-companion --require-ci --require-file-identity --match-github-env reports/remote-capabilities/providers - name: Upload remote capability provider live report if: steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: remote-capability-provider-live-report path: reports/remote-capabilities/providers/*.json if-no-files-found: error retention-days: 7 - name: Prune remote capability provider reports if: always() && steps.providers.outputs.skip != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') run: node packages/scripts/rm-path-recursive.mjs reports/remote-capabilities/providers - name: Note provider live smoke not observed if: steps.providers.outputs.skip != 'true' && github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' run: | echo "::notice::Remote capability provider live smoke is only observed on workflow_dispatch or schedule events." { echo "### Remote capability provider live smoke" echo "" echo "Endpoint secrets are configured, but the full-surface provider live smoke was not observed for event \`${{ github.event_name }}\`. It runs only on \`workflow_dispatch\` and \`schedule\`." } >> "$GITHUB_STEP_SUMMARY" github-live-artifact-validate: name: Remote Capability GitHub Live Artifact Validator needs: - cloud-live-e2e - provider-live-e2e if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && (needs.cloud-live-e2e.outputs.capability_skip != 'true' || needs.provider-live-e2e.outputs.skip != 'true') runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 15 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} install-command: bun install install-native-deps: "false" skip-avatar-clone: "true" no-vision-deps: "true" - name: Download cloud + provider live report artifacts run: | mkdir -p reports/remote-capabilities/cloud reports/remote-capabilities/providers gh run download "$GITHUB_RUN_ID" \ --repo "$GITHUB_REPOSITORY" \ --name remote-capability-cloud-live-report \ --dir reports/remote-capabilities/cloud || echo "::notice::remote-capability-cloud-live-report artifact not present" gh run download "$GITHUB_RUN_ID" \ --repo "$GITHUB_REPOSITORY" \ --name remote-capability-provider-live-report \ --dir reports/remote-capabilities/providers || echo "::notice::remote-capability-provider-live-report artifact not present" - name: Re-validate cloud live report from downloaded artifact if: needs.cloud-live-e2e.result == 'success' && needs.cloud-live-e2e.outputs.capability_skip != 'true' run: bun run test:remote-capabilities:validate-live-reports --kind cloud --expect-count 1 --max-age-minutes 120 --max-future-minutes 5 --require-ci --require-file-identity reports/remote-capabilities/cloud - name: Re-validate provider live reports from downloaded artifacts if: needs.provider-live-e2e.result == 'success' && needs.provider-live-e2e.outputs.skip != 'true' run: bun run test:remote-capabilities:validate-live-reports --kind provider --expect-count 3..4 --max-age-minutes 120 --max-future-minutes 5 --allowed-providers e2b,home-machine,mobile-companion,desktop-companion --require-providers e2b,home-machine,mobile-companion --require-ci --require-file-identity reports/remote-capabilities/providers # The merge queue's sole required context is `ci-ok`, and until now `ci-ok` # needed only the test lanes — so a change that breaks repo-wide typecheck, # fails biome lint, is unformatted, or commits a secret could still merge to # develop as long as tests passed (all of these are required on `main` but were # advisory on develop; #13617 F2). This job restores parity: it runs the same # lint / format / typecheck / secret-scan gates that guard `main`, and `ci-ok` # needs it. It runs anywhere `ci-ok` treats skipped jobs as failures (merge # queue, develop push, manual dispatch, and nightly schedule); on PRs # quality.yml already covers this surface, so the job self-skips and `ci-ok`'s # lenient PR path lets the skip pass. Runner: fleet-aware like the test lanes — # it was pinned to GitHub-hosted for independence from the self-hosted fleet, # but with hosted runners billing-frozen (#13481) that pin froze this ci-ok # dependency; route to self-hosted while the fleet is online. merge-quality-gate: name: Merge Queue Quality Gate needs: changes if: >- github.event_name == 'merge_group' || github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 40 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: fetch-depth: 0 submodules: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup workspace dependencies uses: ./.github/actions/setup-bun-workspace with: bun-version: ${{ env.BUN_VERSION }} - name: Repo-wide biome lint run: bun run lint - name: Repo-wide biome format check run: bun run format:check - name: Repo-wide typecheck run: bun run typecheck - name: Stale base guard env: CURRENT_SHA: ${{ github.sha }} run: | set -euo pipefail if git rev-parse -q --verify "${CURRENT_SHA}^" >/dev/null 2>&1; then BASE_SHA="$(git rev-parse "${CURRENT_SHA}^1")" node packages/scripts/stale-base-guard.self-test.mjs node packages/scripts/stale-base-guard.mjs \ --base "$BASE_SHA" \ --head "$CURRENT_SHA" \ --merge-base "$BASE_SHA" \ --window 1200 \ --max-behind-commits 200 \ --max-behind-hours 72 \ --github \ --summary "$GITHUB_STEP_SUMMARY" else echo "::notice::Current SHA has no parent; stale-base guard has no target branch parent to compare." fi - name: Install gitleaks (OSS binary — no license required) run: | set -euo pipefail GITLEAKS_VERSION=8.30.1 curl -sSL -o /tmp/gitleaks.tar.gz \ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks mkdir -p "$HOME/.local/bin" install -m 0755 /tmp/gitleaks "$HOME/.local/bin/gitleaks" echo "$HOME/.local/bin" >> "$GITHUB_PATH" "$HOME/.local/bin/gitleaks" version - name: Secret scan (gitleaks) env: CURRENT_SHA: ${{ github.sha }} run: | set -euo pipefail # Scan the queue/tip commit only. New content is what the merge # introduces; a full-history re-scan re-flags long-known vendored # artifacts (see gitleaks.yml for the same rationale). Merge queue # SHAs are merge commits, and `git log -p -1 ` emits no patch # by default, so use `-m -p` whenever the tip has multiple parents. parent_count="$(git rev-list --parents -n 1 "$CURRENT_SHA" | wc -w | tr -d ' ')" if [ "$parent_count" -gt 2 ]; then RANGE_ARGS=(--log-opts "-m -p -1 ${CURRENT_SHA}") elif [ "$parent_count" -eq 2 ]; then RANGE_ARGS=(--log-opts "-1 ${CURRENT_SHA}") else echo "gitleaks: tip ${CURRENT_SHA} is an orphan/root baseline commit (no parent);" echo "its content was already scanned incrementally on develop. Skipping full-tree re-scan." exit 0 fi gitleaks detect \ --config .gitleaks.toml \ --source . \ --verbose \ --redact \ --report-format sarif \ --report-path gitleaks.sarif \ --no-banner \ "${RANGE_ARGS[@]}" ci-ok: name: ci-ok if: ${{ !cancelled() && always() }} needs: - stale-base-preflight - changes - test-runner-vacuous-green-guard - merge-quality-gate - server-tests - client-tests - plugin-tests-status - integration-tests - desktop-contract - zero-key-e2e - cloud-live-e2e - provider-live-e2e - github-live-artifact-validate # Fleet-aware (see `changes`): the aggregate gate itself must run to report a # conclusion — a hardcoded hosted pin froze `ci-ok` on the billing-frozen # fleet (#13481), so the required check never posted and nothing could merge. runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 10 steps: - name: Check test results shell: bash run: | set -u echo "=== Required test job results ===" echo "stale-base-preflight:${{ needs.stale-base-preflight.result }}" echo "changes: ${{ needs.changes.result }}" echo "runner guard: ${{ needs.test-runner-vacuous-green-guard.result }}" echo "merge-quality-gate:${{ needs.merge-quality-gate.result }}" echo "server-tests: ${{ needs.server-tests.result }}" echo "client-tests: ${{ needs.client-tests.result }}" echo "plugin-tests: ${{ needs.plugin-tests-status.result }}" echo "integration-tests:${{ needs.integration-tests.result }}" echo "desktop-contract: ${{ needs.desktop-contract.result }}" echo "zero-key-e2e: ${{ needs.zero-key-e2e.result }}" echo "cloud-live-e2e: ${{ needs.cloud-live-e2e.result }}" echo "provider-live-e2e:${{ needs.provider-live-e2e.result }}" echo "github-live-artifact-validate:${{ needs.github-live-artifact-validate.result }}" echo "===================================" strict_results="${{ github.event_name == 'push' || github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }}" bad=0 for pair in \ "stale-base-preflight:${{ needs.stale-base-preflight.result }}" \ "changes:${{ needs.changes.result }}" \ "test-runner-vacuous-green-guard:${{ needs.test-runner-vacuous-green-guard.result }}" \ "merge-quality-gate:${{ needs.merge-quality-gate.result }}" \ "server-tests:${{ needs.server-tests.result }}" \ "client-tests:${{ needs.client-tests.result }}" \ "plugin-tests:${{ needs.plugin-tests-status.result }}" \ "integration-tests:${{ needs.integration-tests.result }}" \ "desktop-contract:${{ needs.desktop-contract.result }}" \ "zero-key-e2e:${{ needs.zero-key-e2e.result }}" \ "cloud-live-e2e:${{ needs.cloud-live-e2e.result }}" \ "provider-live-e2e:${{ needs.provider-live-e2e.result }}" \ "github-live-artifact-validate:${{ needs.github-live-artifact-validate.result }}"; do name="${pair%%:*}" result="${pair##*:}" if [ "$name" = "stale-base-preflight" ] && [ "$result" = "skipped" ] && [ "${{ github.event_name }}" != "pull_request" ]; then echo "::notice title=Test gate::PR stale-base preflight is skipped for ${{ github.event_name }}; merge-quality-gate runs the strict stale-base guard for this event." continue fi if [ "$name" = "github-live-artifact-validate" ] && [ "$result" = "skipped" ] && [ "${{ github.event_name }}" != "workflow_dispatch" ] && [ "${{ github.event_name }}" != "schedule" ]; then echo "::notice title=Test gate::Live artifact validation is observed only on workflow_dispatch/schedule, so its skip is allowed for ${{ github.event_name }}." continue fi if [ "$name" = "github-live-artifact-validate" ] && [ "$result" = "skipped" ] && [ "${{ needs.cloud-live-e2e.outputs.capability_skip }}" = "true" ] && [ "${{ needs.provider-live-e2e.outputs.skip }}" = "true" ]; then echo "::notice title=Test gate::Live artifact validation skipped because no live report producers were configured." continue fi if [ "$result" = "skipped" ] && [ "$strict_results" != "true" ]; then echo "::notice title=Test gate::Path-gated job '${name}' was skipped for this PR context." continue fi if [ "$result" != "success" ]; then echo "::error title=Test gate::Required job '${name}' finished with '${result}' (expected success)" bad=1 fi done if [ "$bad" -ne 0 ]; then exit 1 fi echo "All required test jobs passed. Required develop ruleset check: ci-ok."