Files
elizaos--eliza/.github/workflows/deploy-apps-worker.yml
T
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

312 lines
14 KiB
YAML

name: Deploy Apps Worker (Product 2)
# SSHes to the apps-control host, fetches the target branch, installs the
# systemd unit shipped at packages/scripts/cloud/admin/eliza-apps-worker.service,
# and restarts the eliza-apps-worker daemon (the Product-2 deploy daemon).
#
# This is the slim sibling of deploy-eliza-provisioning-worker.yml: it deploys
# ONLY the apps-worker — NO agent-router, and NO headscale wiring (the apps
# worker reaches the per-tenant Postgres over the apps PRIVATE network, never the
# agent mesh). The apps env (APPS_DEPLOY_ENABLED, APPS_TENANT_ADMIN_DSN,
# CONTAINERS_DOCKER_NODES, …) is written separately by arm-apps-daemon.yml — this
# workflow only ships code + the unit, keeping the two concerns split.
#
# First-time setup: bootstrap an apps-control VM on the apps PRIVATE network (so
# it can reach the tenant DB at 10.30.1.10) that runs NO untrusted containers,
# then set the env's ELIZA_APPS_WORKER_HOST + ELIZA_APPS_WORKER_SSH_KEY secrets.
on:
push:
branches: [develop, main]
paths:
- '.github/workflows/deploy-apps-worker.yml'
- 'packages/scripts/cloud/admin/daemons/apps-provisioning-worker.ts'
- 'packages/scripts/cloud/admin/daemons/provisioning-worker.ts'
- 'packages/scripts/cloud/admin/eliza-apps-worker.service'
- 'packages/cloud/shared/src/lib/services/provisioning-jobs.ts'
- 'packages/cloud/shared/src/lib/services/provisioning-job-types.ts'
- 'packages/cloud/shared/src/lib/services/apps-deploy-backend.ts'
- 'packages/cloud/shared/src/lib/services/tenant-db/**'
- 'packages/cloud/shared/src/lib/services/app-deploy-runner.ts'
- 'packages/cloud/sdk/**'
- 'packages/core/**'
- 'plugins/plugin-sql/**'
workflow_dispatch:
inputs:
environment:
description: 'Target environment (host + secrets resolved from this)'
required: true
default: 'staging'
type: choice
options:
- staging
- production
concurrency:
# Per-commit group (mirrors cloud-cf-deploy #10802/#10973). A SHARED
# deploy-apps-worker-<env> group with cancel-in-progress:false makes GitHub
# keep only the latest PENDING run per group and silently cancel the older
# pending ones (no annotation) — so a hot develop push/dispatch stream
# produces a chain of cancelled runs and zero completed deploys
# (elizaOS/eliza#10839). Keying on github.sha too gives each commit its own
# group so no run is ever silently superseded. Jobs run on ubuntu-latest
# (no runner-level serialization), so a host-side flock in the SSH scripts
# serializes concurrent deploys against the shared apps-control host; each
# run deploys the branch tip at fetch time, so the last to execute lands the
# newest code. Env stays in the key so staging and production never block
# each other.
group: deploy-apps-worker-${{ github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'production' || 'staging') }}-${{ github.sha }}
cancel-in-progress: false
permissions:
contents: read
jobs:
determine-env:
name: Determine environment
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
outputs:
environment: ${{ steps.env.outputs.environment }}
branch: ${{ steps.env.outputs.branch }}
steps:
- id: env
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
env="${{ github.event.inputs.environment }}"
elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
env="production"
else
env="staging"
fi
branch=$([ "$env" = "production" ] && echo "main" || echo "develop")
echo "environment=$env" >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
echo "Resolved: environment=$env branch=$branch"
deploy:
name: Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }})
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
needs: determine-env
environment: ${{ needs.determine-env.outputs.environment }}
timeout-minutes: 15
env:
SYSTEMD_UNIT: eliza-apps-worker.service
DEPLOY_HOST: ${{ secrets.ELIZA_APPS_WORKER_HOST }}
DEPLOY_SSH_KEY: ${{ secrets.ELIZA_APPS_WORKER_SSH_KEY }}
DEPLOY_BRANCH: ${{ needs.determine-env.outputs.branch }}
steps:
- name: Check deploy configuration
id: deploy_config
run: |
if [ -z "$DEPLOY_HOST" ] || [ -z "$DEPLOY_SSH_KEY" ]; then
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::warning::Missing ELIZA_APPS_WORKER_HOST or ELIZA_APPS_WORKER_SSH_KEY; skipping apps-worker deploy."
{
echo "### Apps worker deploy skipped"
echo ""
echo "Missing \`ELIZA_APPS_WORKER_HOST\` or \`ELIZA_APPS_WORKER_SSH_KEY\` for this environment."
echo "Bootstrap an apps-control VM on the apps private network and set those secrets first."
} >> "$GITHUB_STEP_SUMMARY"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "::error::Manual deployment requires ELIZA_APPS_WORKER_HOST and ELIZA_APPS_WORKER_SSH_KEY."
exit 1
fi
exit 0
fi
echo "configured=true" >> "$GITHUB_OUTPUT"
- name: Ensure host prereqs (Node 24, swap, bunx symlink)
if: steps.deploy_config.outputs.configured == 'true'
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2
with:
host: ${{ env.DEPLOY_HOST }}
username: deploy
key: ${{ env.DEPLOY_SSH_KEY }}
command_timeout: 5m
script: |
set -euo pipefail
if ! command -v node >/dev/null 2>&1; then
echo "[host-prereqs] installing Node 24"
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
sudo apt-get install -y nodejs
fi
if [ ! -f /swapfile ]; then
echo "[host-prereqs] adding 8GB swap"
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab >/dev/null
fi
if [ ! -e /home/deploy/.bun/bin/bunx ] && [ -e /home/deploy/.bun/bin/bun ]; then
echo "[host-prereqs] creating bunx symlink"
ln -sf bun /home/deploy/.bun/bin/bunx
fi
- name: Deploy and restart apps worker
if: steps.deploy_config.outputs.configured == 'true'
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2
with:
host: ${{ env.DEPLOY_HOST }}
username: deploy
key: ${{ env.DEPLOY_SSH_KEY }}
command_timeout: 10m
envs: DEPLOY_BRANCH,SYSTEMD_UNIT
script: |
set -euo pipefail
# Serialize deploys on the host: per-SHA concurrency groups mean
# overlapping runs are expected; without this they would interleave
# git clean/bun install/systemctl restart on the same checkout.
exec 9>/tmp/eliza-apps-worker-deploy.lock
# 540s keeps the lock wait under this step's 10m command_timeout so
# a starved waiter fails with THIS message, not an opaque SSH kill.
flock -w 540 9 || { echo "::error::another apps-worker deploy holds the host lock after 9m"; exit 1; }
echo "=== Deploying eliza-apps-worker (branch: $DEPLOY_BRANCH) ==="
cd /opt/eliza
export GIT_CONFIG_COUNT=1
export GIT_CONFIG_KEY_0=safe.directory
export GIT_CONFIG_VALUE_0=/opt/eliza
rm -f .git/index.lock
sudo chown -R deploy:deploy .git
git reset --hard HEAD 2>/dev/null || true
git clean -fdx \
-e .env \
-e .env.local \
-e '.env.*' \
-e cloud/.env.local \
-e 'cloud/.env.*' \
-e node_modules
find packages -type f \( -name '*.d.ts' -o -name '*.d.ts.map' \) \
! -path '*/node_modules/*' -delete 2>/dev/null || true
rm -f bun.lock
git -c fetch.recurseSubmodules=no fetch --no-recurse-submodules \
origin "+$DEPLOY_BRANCH:refs/remotes/origin/$DEPLOY_BRANCH"
git -c submodule.recurse=false checkout --no-recurse-submodules \
-B "$DEPLOY_BRANCH" "origin/$DEPLOY_BRANCH"
# The `rm -f bun.lock` above deletes a TRACKED file; when bun.lock
# is identical between the previous HEAD and the new tip, checkout
# carries that deletion over as a local change and `bun install`
# then resolves the whole tree fresh from package.json ranges —
# newest-in-range @ai-sdk/* against the overridden provider-utils
# pin took the worker down at boot (`does not provide an export
# named 'secureJsonParse'`). Always restore the tracked lockfile so
# installs are lockfile-pinned and deterministic.
git checkout "origin/$DEPLOY_BRANCH" -- bun.lock
if ! command -v bun >/dev/null 2>&1; then
curl -fsSL https://bun.sh/install | bash -s "canary"
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
fi
cd /opt/eliza
for attempt in 1 2 3; do
if bun install --no-save --ignore-scripts; then
break
fi
if [ "$attempt" -eq 3 ]; then exit 1; fi
echo "bun install attempt $attempt failed; retrying in 10s..."
sleep 10
done
# The daemon runs under Node/tsx, which resolves linked workspace
# packages through their built `node` exports — refresh dist files.
bun run build:core
mkdir -p plugins/plugin-sql/node_modules/@elizaos
rm -rf plugins/plugin-sql/node_modules/@elizaos/core
ln -s ../../../../packages/core plugins/plugin-sql/node_modules/@elizaos/core
bun run --cwd plugins/plugin-sql build
# packages/scripts has no package.json, so bun's workspace graph
# doesn't link @elizaos/cloud-shared for the daemon — create it.
mkdir -p node_modules/@elizaos
ln -sfn ../../packages/cloud/shared node_modules/@elizaos/cloud-shared
sudo install -m 0644 \
packages/scripts/cloud/admin/eliza-apps-worker.service \
"/etc/systemd/system/$SYSTEMD_UNIT"
sudo systemctl daemon-reload
sudo systemctl enable "$SYSTEMD_UNIT"
sudo systemctl restart "$SYSTEMD_UNIT"
echo "=== Restart issued for apps worker ==="
- name: Health check
if: steps.deploy_config.outputs.configured == 'true'
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2
with:
host: ${{ env.DEPLOY_HOST }}
username: deploy
key: ${{ env.DEPLOY_SSH_KEY }}
command_timeout: 5m
envs: SYSTEMD_UNIT
script: |
set -euo pipefail
# Hold the same host lock as the deploy step so a concurrent run's
# restart can never flip the unit mid-check (false red). 240s wait
# stays under this step's 5m command_timeout.
exec 9>/tmp/eliza-apps-worker-deploy.lock
flock -w 240 9 || { echo "::error::another apps-worker deploy holds the host lock after 4m"; exit 1; }
# The apps worker has no HTTP endpoint; health = active + stable +
# no fatal/restart-loop in the journal since deploy.
HEALTH_SINCE_TS="$(date -u '+%Y-%m-%d %H:%M:%S')"
STABLE_THRESHOLD_SEC=20
FATAL_LOG_PATTERN="\[apps-worker\] (fatal|unhandled rejection)|node:internal/.*Error:|Error \[ERR_|^[A-Za-z]+Error:"
for attempt in $(seq 1 18); do
if sudo systemctl is-active --quiet "$SYSTEMD_UNIT"; then
JOURNAL=$(sudo journalctl -u "$SYSTEMD_UNIT" --since "$HEALTH_SINCE_TS" --no-pager 2>/dev/null || true)
if echo "$JOURNAL" | grep -qE "$FATAL_LOG_PATTERN"; then
echo "::error::Apps worker logged a fatal error since deploy."
echo "$JOURNAL" | tail -n 50
exit 1
fi
UPTIME_SEC=$(systemctl show "$SYSTEMD_UNIT" --property=ActiveEnterTimestampMonotonic --value | awk '{ print int($1 / 1e6) }')
NOW_SEC=$(awk '{print int($1)}' /proc/uptime)
AGE=$(( NOW_SEC - UPTIME_SEC ))
if [ "$AGE" -ge "$STABLE_THRESHOLD_SEC" ]; then
echo "Apps worker active and stable (${AGE}s) on attempt $attempt."
exit 0
fi
echo "Health check attempt $attempt/18: active but only ${AGE}s old, waiting for stability..."
else
echo "Health check attempt $attempt/18: not active yet."
fi
sleep 5
done
echo "::error::$SYSTEMD_UNIT failed to become healthy within 90s."
sudo systemctl status "$SYSTEMD_UNIT" --no-pager || true
sudo journalctl -u "$SYSTEMD_UNIT" -n 200 --no-pager || true
exit 1
- name: Notify Discord (Success)
if: success() && steps.deploy_config.outputs.configured == 'true'
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
ack_no_webhook: true
title: "🚀 Eliza Apps Worker Deployed (Product 2)"
nodetail: true
description: |
Branch: ${{ needs.determine-env.outputs.branch }}
Commit: ${{ github.sha }}
color: 0x00ff00
- name: Notify Discord (Failure)
if: failure() && steps.deploy_config.outputs.configured == 'true'
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
ack_no_webhook: true
title: "❌ Eliza Apps Worker Deploy Failed"
nodetail: true
description: |
Branch: ${{ needs.determine-env.outputs.branch }}
Commit: ${{ github.sha }}
color: 0xff0000