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
219 lines
12 KiB
YAML
219 lines
12 KiB
YAML
name: Arm Apps Daemon (Product 2)
|
|
|
|
# Arm the apps (Product 2) deploy backend on a provisioning-worker control plane
|
|
# WITHOUT hand-editing the box — the IaC counterpart to hand-SSH'ing in.
|
|
#
|
|
# The daemon already calls configureAppsDeployBackend() on boot, but it only
|
|
# provisions app containers once the apps env is present in
|
|
# /opt/eliza/cloud/.env.local. This workflow UPSERTS exactly that block (each
|
|
# key delete-then-append, every other line untouched — never clobbers the
|
|
# agent/coding fleet config) and restarts the daemon, over the SAME
|
|
# ELIZA_PROVISIONING_SSH_KEY/HOST secrets the deploy workflow already uses. So
|
|
# no operator SSH key needs to be added anywhere; dispatch it and it converges.
|
|
#
|
|
# Mirrors packages/scripts/cloud/admin/arm-apps-daemon.mjs (same upsert logic),
|
|
# so a local run and a CI run produce the same box state.
|
|
#
|
|
# Inputs:
|
|
# environment staging | production (selects the env's PROVISIONING secrets)
|
|
# app_node CONTAINERS_DOCKER_NODES value, e.g. apps-node-1:167.233.112.155:20
|
|
# tenant_admin_dsn (optional) env-sourced tenant DB admin DSN. Leave empty to
|
|
# let the daemon use the SEEDED tenant_db_clusters row
|
|
# (encrypted path). Set it only if the encrypted path isn't
|
|
# wired on this box.
|
|
# egress_proxy (optional) CONTAINERS_EGRESS_PROXY_URL
|
|
#
|
|
# Non-secret base domain comes from the env var APPS_BASE_DOMAIN (same var the
|
|
# terraform workflow reads); the Caddy admin URL is derived from the app node IP.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
environment:
|
|
description: Environment to target
|
|
type: choice
|
|
default: staging
|
|
options: [staging, production]
|
|
target:
|
|
description: "Which daemon to arm: the shared control-plane provisioning-worker (cp-daemon) or the dedicated apps-control worker (apps-worker, Product-2 isolated path)"
|
|
type: choice
|
|
default: cp-daemon
|
|
options: [cp-daemon, apps-worker]
|
|
app_node:
|
|
description: "CONTAINERS_DOCKER_NODES (id:ip:capacity), e.g. apps-node-1:167.233.112.155:20"
|
|
type: string
|
|
required: true
|
|
tenant_admin_dsn:
|
|
description: "Optional env-sourced tenant DB admin DSN (leave empty to use the seeded encrypted cluster OR fetch_dsn_from_shared_state)"
|
|
type: string
|
|
required: false
|
|
default: ""
|
|
fetch_dsn_from_shared_state:
|
|
description: "Read APPS_TENANT_ADMIN_DSN from the apps-shared terraform output instead of pasting it (keeps the password out of inputs/logs)"
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
egress_proxy:
|
|
description: "Optional CONTAINERS_EGRESS_PROXY_URL"
|
|
type: string
|
|
required: false
|
|
default: ""
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
# Key on target too: the cp-daemon and apps-worker arms touch different hosts,
|
|
# so serializing them against each other (env-only key) needlessly blocks
|
|
# arming the two independent daemons concurrently.
|
|
group: arm-apps-daemon-${{ github.event.inputs.environment }}-${{ github.event.inputs.target }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
arm:
|
|
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
|
|
environment: ${{ github.event.inputs.environment }}
|
|
timeout-minutes: 10
|
|
env:
|
|
# Target the dedicated apps-control host (apps-worker) or the shared
|
|
# control-plane host (cp-daemon). The apps-worker path arms the isolated
|
|
# Product-2 daemon on a host that sits on the apps private net (so it can
|
|
# reach the tenant DB) and runs no agent jobs — never touching the agent
|
|
# control plane.
|
|
#
|
|
# NO cross-target fallback. The naive `apps-worker && APPS || PROVISIONING`
|
|
# form silently resolves to the CP host when ELIZA_APPS_WORKER_HOST is
|
|
# unset — which would SSH the apps env onto the agent control-plane host
|
|
# (Preflight can't catch it because DEPLOY_HOST is then non-empty). The
|
|
# nested form below only ever yields the SAME-target secret or '' (the
|
|
# trailing `|| ''` forces empty, not the boolean `false`), so a missing
|
|
# same-target secret leaves DEPLOY_HOST empty and the Preflight
|
|
# `[ -n "$DEPLOY_HOST" ]` check reports the right missing-secret error.
|
|
DEPLOY_HOST: ${{ (github.event.inputs.target == 'apps-worker' && secrets.ELIZA_APPS_WORKER_HOST) || (github.event.inputs.target == 'cp-daemon' && secrets.ELIZA_PROVISIONING_HOST) || '' }}
|
|
DEPLOY_SSH_KEY: ${{ (github.event.inputs.target == 'apps-worker' && secrets.ELIZA_APPS_WORKER_SSH_KEY) || (github.event.inputs.target == 'cp-daemon' && secrets.ELIZA_PROVISIONING_SSH_KEY) || '' }}
|
|
CONTAINERS_SSH_KEY_INPUT: ${{ secrets.CONTAINERS_SSH_KEY || (github.event.inputs.target == 'apps-worker' && secrets.ELIZA_APPS_WORKER_SSH_KEY) || (github.event.inputs.target == 'cp-daemon' && secrets.ELIZA_PROVISIONING_SSH_KEY) || '' }}
|
|
SYSTEMD_UNIT: ${{ github.event.inputs.target == 'apps-worker' && 'eliza-apps-worker.service' || 'eliza-provisioning-worker.service' }}
|
|
BASE_DOMAIN: ${{ vars.APPS_BASE_DOMAIN }}
|
|
APP_NODE: ${{ github.event.inputs.app_node }}
|
|
TENANT_ADMIN_DSN: ${{ github.event.inputs.tenant_admin_dsn }}
|
|
EGRESS_PROXY: ${{ github.event.inputs.egress_proxy }}
|
|
steps:
|
|
- name: Preflight
|
|
run: |
|
|
fail=0
|
|
HOST_SECRET="${{ github.event.inputs.target == 'apps-worker' && 'ELIZA_APPS_WORKER_HOST' || 'ELIZA_PROVISIONING_HOST' }}"
|
|
KEY_SECRET="${{ github.event.inputs.target == 'apps-worker' && 'ELIZA_APPS_WORKER_SSH_KEY' || 'ELIZA_PROVISIONING_SSH_KEY' }}"
|
|
[ -n "$DEPLOY_HOST" ] || { echo "::error::missing secret $HOST_SECRET on the '${{ github.event.inputs.environment }}' environment (target=${{ github.event.inputs.target }})"; fail=1; }
|
|
[ -n "$DEPLOY_SSH_KEY" ] || { echo "::error::missing secret $KEY_SECRET"; fail=1; }
|
|
[ -n "$CONTAINERS_SSH_KEY_INPUT" ] || { echo "::error::missing container SSH key material: set secret CONTAINERS_SSH_KEY or $KEY_SECRET"; fail=1; }
|
|
[ -n "$BASE_DOMAIN" ] || { echo "::error::set vars.APPS_BASE_DOMAIN on the '${{ github.event.inputs.environment }}' environment"; fail=1; }
|
|
[ -n "$APP_NODE" ] || { echo "::error::app_node input is required (id:ip:capacity)"; fail=1; }
|
|
# Derive the Caddy admin URL from the app node IP (2nd colon field).
|
|
NODE_IP="$(echo "$APP_NODE" | cut -d: -f2)"
|
|
echo "$NODE_IP" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || { echo "::error::app_node must be id:ip:capacity with a valid IPv4"; fail=1; }
|
|
echo "CADDY_ADMIN=http://$NODE_IP:2019" >> "$GITHUB_ENV"
|
|
[ "$fail" = 0 ] || exit 1
|
|
|
|
- name: Prepare container SSH key
|
|
run: |
|
|
set -euo pipefail
|
|
tmp="$(mktemp)"
|
|
cleanup() { rm -f "$tmp"; }
|
|
trap cleanup EXIT
|
|
|
|
# docker-ssh.ts consumes CONTAINERS_SSH_KEY as base64. Accept either
|
|
# a dedicated already-base64 CONTAINERS_SSH_KEY secret or the raw
|
|
# target deploy key used by appleboy/ssh-action, then forward one
|
|
# single-line base64 value to the remote upsert script.
|
|
if printf '%s' "$CONTAINERS_SSH_KEY_INPUT" | base64 -d >"$tmp" 2>/dev/null && grep -q 'BEGIN .*PRIVATE KEY' "$tmp"; then
|
|
containers_ssh_key_b64="$(printf '%s' "$CONTAINERS_SSH_KEY_INPUT" | tr -d '\r\n')"
|
|
else
|
|
containers_ssh_key_b64="$(printf '%s' "$CONTAINERS_SSH_KEY_INPUT" | base64 -w0)"
|
|
fi
|
|
|
|
[ -n "$containers_ssh_key_b64" ] || { echo "::error::container SSH key resolved to an empty value"; exit 1; }
|
|
echo "::add-mask::$containers_ssh_key_b64"
|
|
echo "CONTAINERS_SSH_KEY_B64=$containers_ssh_key_b64" >> "$GITHUB_ENV"
|
|
|
|
# OPTIONAL: pull the tenant admin DSN straight from the apps-shared
|
|
# terraform output so the password never appears in a workflow input or in
|
|
# logs. `terraform output -raw` of a SENSITIVE value is auto-masked by the
|
|
# runner; we also ::add-mask:: it before writing to $GITHUB_ENV.
|
|
- name: Checkout (for terraform fetch)
|
|
if: ${{ github.event.inputs.fetch_dsn_from_shared_state == 'true' }}
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Terraform (for DSN fetch)
|
|
if: ${{ github.event.inputs.fetch_dsn_from_shared_state == 'true' }}
|
|
uses: hashicorp/setup-terraform@v4
|
|
with:
|
|
terraform_version: "1.10.5"
|
|
|
|
- name: Read DSN from apps-shared output
|
|
if: ${{ github.event.inputs.fetch_dsn_from_shared_state == 'true' }}
|
|
working-directory: packages/cloud/infra/cloud/terraform/hetzner/apps-shared
|
|
env:
|
|
HCLOUD_TOKEN: ${{ secrets.HCLOUD_APPS_TOKEN }}
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_STATE_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_STATE_SECRET_ACCESS_KEY }}
|
|
run: |
|
|
terraform init -input=false -backend-config=backend.hcl >/dev/null
|
|
DSN="$(terraform output -raw tenant_db_admin_dsn)"
|
|
[ -n "$DSN" ] || { echo "::error::apps-shared has no tenant_db_admin_dsn output (is it applied?)"; exit 1; }
|
|
echo "::add-mask::$DSN"
|
|
echo "TENANT_ADMIN_DSN=$DSN" >> "$GITHUB_ENV"
|
|
echo "fetched tenant admin DSN from apps-shared output (masked)"
|
|
|
|
- name: Upsert apps env + restart daemon
|
|
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2
|
|
with:
|
|
host: ${{ env.DEPLOY_HOST }}
|
|
username: deploy
|
|
key: ${{ env.DEPLOY_SSH_KEY }}
|
|
command_timeout: 5m
|
|
# Only the values; the upsert logic is fixed here (mirrors arm-apps-daemon.mjs).
|
|
envs: APP_NODE,BASE_DOMAIN,CADDY_ADMIN,TENANT_ADMIN_DSN,EGRESS_PROXY,SYSTEMD_UNIT,CONTAINERS_SSH_KEY_B64
|
|
script: |
|
|
set -euo pipefail
|
|
F=/opt/eliza/cloud/.env.local
|
|
# /opt/eliza/cloud is root-owned; the deploy user has passwordless
|
|
# sudo (same as the worker-deploy workflow), so all writes go via
|
|
# sudo — `sed -i` needs to create a temp file IN the dir, not just
|
|
# write the file, which a non-sudo deploy user can't do there.
|
|
sudo test -f "$F" || { echo "env file $F not found on host"; exit 1; }
|
|
sudo cp -n "$F" "$F.bak.arm-apps" 2>/dev/null || true
|
|
|
|
upsert() {
|
|
# upsert KEY VALUE — delete any existing def, append the fresh one.
|
|
local k="$1"; local v="$2"
|
|
[ -n "$v" ] || return 0
|
|
sudo sed -i "/^$k=/d" "$F"
|
|
printf '%s\n' "$k=\"$v\"" | sudo tee -a "$F" >/dev/null
|
|
}
|
|
|
|
# APPS_DEPLOY_ENABLED arms the APP_DEPLOY runner; without it the daemon's
|
|
# armAppsDeployBackendIfEnabled() early-returns (provisioning-worker.ts:652)
|
|
# and APP_DEPLOY jobs never get claimed (apps stuck "building").
|
|
upsert APPS_DEPLOY_ENABLED 1
|
|
upsert APPS_CONTAINERS_ENABLED 1
|
|
upsert CONTAINERS_DOCKER_NODES "$APP_NODE"
|
|
upsert CONTAINERS_SSH_USER deploy
|
|
upsert CONTAINERS_SSH_KEY "$CONTAINERS_SSH_KEY_B64"
|
|
upsert APPS_CADDY_ADMIN_URL "$CADDY_ADMIN"
|
|
upsert CONTAINERS_PUBLIC_BASE_DOMAIN "$BASE_DOMAIN"
|
|
upsert APPS_TENANT_ADMIN_DSN "$TENANT_ADMIN_DSN"
|
|
upsert CONTAINERS_EGRESS_PROXY_URL "$EGRESS_PROXY"
|
|
|
|
echo "--- apps env now on the box (secrets redacted) ---"
|
|
sudo grep -E '^(APPS_|CONTAINERS_(DOCKER_NODES|SSH_USER|SSH_KEY|PUBLIC_BASE_DOMAIN|EGRESS_PROXY_URL))' "$F" \
|
|
| sed -E 's/(DSN|KEY)=.*/\1=<redacted>/'
|
|
|
|
# Restart whichever daemon we armed: the dedicated apps-worker on the
|
|
# apps-control host, or the shared control-plane provisioning-worker.
|
|
UNIT="${SYSTEMD_UNIT:-eliza-provisioning-worker.service}"
|
|
sudo systemctl restart "$UNIT"
|
|
sleep 2
|
|
echo "--- daemon status ($UNIT) ---"
|
|
systemctl is-active "$UNIT"
|
|
journalctl -u "$UNIT" -n 10 --no-pager | tail -10 || true
|