Files
elizaos--eliza/.github/workflows/build-agent-image.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

347 lines
16 KiB
YAML

name: Build Agent Image
# Single source of truth for the cloud-agent container image used by the
# cloud sandbox provisioner. Three trigger surfaces:
# - push to develop/main → :develop / :stable + :latest, plus :sha-<short>
# - GitHub release created → release tag (e.g. v2.0.0-alpha.176)
# - workflow_dispatch → manual reruns
# The previous release-only workflow (image.yaml) duplicated this entire
# pre-build sequence; it's been deleted in favor of this consolidated build.
on:
push:
branches: [develop, main]
paths:
- "packages/app-core/deploy/Dockerfile.ci"
- "plugins/plugin-sql/**"
- "plugins/plugin-elizacloud/**"
- "plugins/plugin-local-inference/**"
- "packages/agent/**"
- "packages/server/**"
- "packages/core/**"
- "packages/app/**"
- "packages/app-core/**"
- "packages/cloud/sdk/**"
- ".github/workflows/build-agent-image.yml"
- ".dockerignore"
release:
types: [created]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# Default to least privilege. Override per-job where needed.
# cancel superseded in-flight runs for the same PR to free hosted-runner
# capacity. only cancels pull_request runs; push runs on protected branches
# (main/develop) are never cancelled mid-flight.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
build-and-push:
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
timeout-minutes: 60
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
submodules: false
show-progress: false
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo docker image prune -a -f || true
- name: Install jq
# Used by docker-ci-smoke's boot-KPI budget parser after the image is
# built. Keep it explicit so the real boot gate reports the committed
# budget instead of falling back to its baked-in default.
run: sudo apt-get install -y --no-install-recommends jq
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: "canary"
- name: Cache Bun install
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-canary-${{ hashFiles('bun.lock') }}
restore-keys: |
bun-${{ runner.os }}-canary-
bun-${{ runner.os }}-
- name: Install dependencies
# Use `--no-frozen-lockfile` so a downstream lockfile drift in
# `develop` doesn't short-circuit the install before postinstall
# gets a chance to run (which is what was happening with the
# earlier `--frozen-lockfile` + fallback dance — the frozen
# install would fail, Bun's silent retry would no-op against
# the already-extracted virtual store, and postinstall would
# never run, leaving root-level deps like `uuid`, `drizzle-orm`,
# and the patched `tsup/dist/rollup.js` unavailable to the
# per-plugin build loop). The heavy submodule fetch is guarded
# by NODE_LLAMA_CPP_SKIP_DOWNLOAD.
env:
NODE_LLAMA_CPP_SKIP_DOWNLOAD: "true"
run: |
bun install --no-frozen-lockfile
- name: Cache Turbo build outputs
# The turbo build below is the single biggest pre-docker cost (~5-6
# min from cold). Nothing persisted turbo's local task cache between
# runs, so every push rebuilt every filtered package from scratch.
# Keying on the commit SHA (with a prefix restore-key) restores the
# most recent cache and saves an updated one each run, so a typical
# source commit only rebuilds the packages it actually touched.
# The key prefix is workflow-specific: entries here are produced
# under NODE_ENV=production, which turbo does not hash, so they must
# not be replayed into other workflows' builds.
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: .turbo/cache
key: turbo-agent-image-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-agent-image-${{ runner.os }}-
- name: Apply postinstall patches
# Belt-and-suspenders for the postinstall path: even with
# `bun install` (postinstall enabled), the silent fallback that
# kicks in when frozen-lockfile drifts doesn't always re-run the
# repo postinstall in CI. Explicitly run the patches the
# per-plugin builds depend on:
# - ensure-type-package-aliases: materializes @types/* under
# node_modules/@types so `tsc --build` resolves them.
env:
NODE_LLAMA_CPP_SKIP_DOWNLOAD: "true"
run: |
node packages/app-core/scripts/ensure-type-package-aliases.mjs
- name: Build Docker workspace artifacts
# Let Turbo derive the build order and reuse task cache instead of
# hand-maintaining the core/package/plugin sequence. The explicit
# filters include the app, the agent entrypoint, and every dynamically
# loaded plugin whose dist must exist before Dockerfile.ci relinks the
# local workspace packages into the image.
env:
NODE_ENV: production
NODE_LLAMA_CPP_SKIP_DOWNLOAD: "true"
run: |
node packages/scripts/run-turbo.mjs run build --concurrency=8 \
--filter=@elizaos/app \
--filter=@elizaos/agent \
--filter=@elizaos/plugin-sql \
--filter=@elizaos/plugin-video \
--filter=@elizaos/plugin-agent-skills \
--filter=@elizaos/plugin-pdf \
--filter=@elizaos/plugin-browser \
--filter=@elizaos/plugin-capacitor-bridge \
--filter=@elizaos/plugin-coding-tools \
--filter=@elizaos/plugin-native-filesystem \
--filter=@elizaos/plugin-shell \
--filter=@elizaos/plugin-commands \
--filter=@elizaos/plugin-computeruse \
--filter=@elizaos/plugin-discord \
--filter=@elizaos/plugin-edge-tts \
--filter=@elizaos/plugin-elizacloud \
--filter=@elizaos/plugin-imessage \
--filter=@elizaos/plugin-local-inference \
--filter=@elizaos/plugin-mcp \
--filter=@elizaos/plugin-signal \
--filter=@elizaos/plugin-streaming \
--filter=@elizaos/plugin-telegram \
--filter=@elizaos/plugin-whatsapp \
--filter=@elizaos/plugin-wallet \
--filter=@elizaos/plugin-workflow \
--filter=@elizaos/plugin-x402
- name: Prune stale Turbo cache entries
# actions/cache restores + re-saves the whole cache dir, so without a
# prune the tarball grows by every commit's changed-package outputs
# forever. tar preserves mtimes, so age-based deletion bounds it:
# entries untouched for two weeks are long since superseded.
run: find .turbo/cache -type f -mtime +14 -delete 2>/dev/null || true
- name: Normalize plugin dist for Node ESM
# MUST run AFTER the Turbo build above. Some package builds still emit
# extensionless or directory specifiers (`./actions/index`,
# `./approvals`) in dist. The container loads those built `.js` files
# via import(), and neither tsx nor plain `node` resolves them. Apply
# the same rewrite the agent's own build uses to EVERY built plugin
# dist, last, so the dist that actually ships is Node-ESM-resolvable.
run: |
shopt -s nullglob
for dist in plugins/*/dist; do
pkg="$(dirname "$dist")"
node packages/scripts/rewrite-dist-relative-imports-node-esm.mjs "$pkg" || true
done
- name: Prepare CI dockerignore
# The root .dockerignore is tuned for `bun install` runs (excludes
# everything heavy). The Docker build needs a different policy:
# keep root node_modules (Dockerfile.ci's pruner relink scripts
# consume them) but strip per-package node_modules and the other
# bulky CI-only inputs. Use the canonical CI dockerignore from
# packages/app-core/deploy/.dockerignore.ci, mirroring what
# docker-ci-smoke.sh does before its `docker build`.
run: |
cp packages/app-core/deploy/.dockerignore.ci .dockerignore
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c
with:
driver-opts: |
env.BUILDKIT_STEP_LOG_MAX_SIZE=10485760
env.BUILDKIT_STEP_LOG_MAX_SPEED=10485760
# github.repository preserves the org's casing (elizaOS/eliza), but Docker
# tags must be all-lowercase. metadata-action lowercases for the push tags,
# but the raw :ci-boot-verify tag below needs an explicit lowercased image
# name or `docker build`/`docker push` fails with "repository name must be
# lowercase".
- id: image
run: echo "name=${REGISTRY}/${IMAGE_NAME,,}" >> "$GITHUB_OUTPUT"
- id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# develop push → :develop + :sha-<short>
# main push → :stable + :latest + :sha-<short>
# release → the git tag verbatim (e.g. v2.0.0-alpha.176)
# Keeping :latest on main avoids breaking the four code sites that
# still hardcode it (containers-env fallback, sandbox-provider
# fallback, two test fixtures) until they migrate to :stable.
tags: |
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=tag
type=sha,prefix=sha-,format=short
# Build the image into the local Docker daemon first (load, do NOT push).
# The boot-verification step below runs the REAL agent entrypoint against
# this exact image, and the push step ships these same loaded bytes by
# retagging the local image. This is the gate that stops a container which
# crashes on startup (missing runtime deps, broken entrypoint) from
# reaching ghcr. `provenance: false` keeps the loaded image a single-
# platform image so the retag-and-push below publishes the same content
# that was verified (no second build, no "latest yt-dlp" drift between the
# verified and published image).
- id: build-local
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with:
context: .
file: packages/app-core/deploy/Dockerfile.ci
push: false
load: true
provenance: false
tags: ${{ steps.image.outputs.name }}:ci-boot-verify
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Registry-backed build cache (checked first), with the old GHA
# cache kept as a read-only fallback. The GHA backend shares one
# repo-wide 10GB budget with every other workflow; this image's
# mode=max cache is multi-GB per run, so it was evicted before the
# next develop push — recent runs show ZERO cached layers while
# still spending 4+ min per run exporting a cache nothing ever
# read (which is also why cache-to no longer writes to gha). The
# ghcr :buildcache ref persists indefinitely and dedupes by blob
# digest, so stable layers (apt/tailscale/yt-dlp/tsx base, and the
# manifest-keyed runtime-deps npm closure) hit reliably and the
# per-run cache export only uploads blobs ghcr doesn't already
# have. image-manifest+oci-mediatypes make the cache manifest a
# ghcr-compatible OCI artifact.
cache-from: |
type=registry,ref=${{ steps.image.outputs.name }}:buildcache
type=gha
cache-to: type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=max,image-manifest=true,oci-mediatypes=true
- name: Verify image boots (real agent entrypoint)
# Runs the production entrypoint (node --import
# /opt/tsx/node_modules/tsx/dist/loader.mjs packages/agent/dist/bin.js
# start) inside the just-built image and fails RED if the agent crashes
# on startup, exits non-zero, logs a known crash signature (Cannot find
# package/module, Failed to start, crashed during init, ...), never
# serves /api/health within the timeout, or crashes during the
# post-health stability window. A failure here blocks the push below, so
# a non-bootable image is never published.
env:
DOCKER_IMAGE: ${{ steps.image.outputs.name }}:ci-boot-verify
BOOT_VERIFY_ONLY: "true"
SMOKE_TIMEOUT_SEC: "180"
BOOT_STABLE_WINDOW_SEC: "15"
SMOKE_PORT: "32138"
CONTAINER_PORT: "42138"
# Enforce the boot-KPI cold-readyMs budget (#8812 item 5). The budget
# (boot.coldReadyMs, 25000ms) carries ~5x headroom over the ~5s real
# cold boot, and emit_boot_kpi downgrades a breach to a warning when the
# runner is heavily contended, so this only fails on a genuine multi-x
# boot-time regression — not on transient runner load.
BOOT_KPI_ENFORCE: "1"
run: bash packages/app-core/scripts/docker-ci-smoke.sh --boot-verify-only
# Push the EXACT verified image. This runs only if the boot verification
# above succeeded (steps run in order; a failed step fails the job). We
# retag the locally-loaded :ci-boot-verify image to each computed tag and
# `docker push` it, so the published bytes are byte-for-byte the image
# that just booted cleanly. No rebuild means no chance of shipping a
# different image than the one that was verified.
- id: push
env:
LOCAL_IMAGE: ${{ steps.image.outputs.name }}:ci-boot-verify
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
first_tag=""
while IFS= read -r tag; do
[ -z "$tag" ] && continue
[ -z "$first_tag" ] && first_tag="$tag"
echo "Tagging $LOCAL_IMAGE as $tag and pushing"
docker tag "$LOCAL_IMAGE" "$tag"
docker push "$tag"
done <<< "$TAGS"
# Resolve the published digest from a pushed tag (its RepoDigests is
# populated by the push above) for the attestation step.
digest=""
if [ -n "$first_tag" ]; then
digest="$(docker inspect --format '{{ index .RepoDigests 0 }}' "$first_tag" 2>/dev/null | sed 's/.*@//' || true)"
fi
echo "digest=$digest" >> "$GITHUB_OUTPUT"
- name: Generate artifact attestation
if: ${{ steps.push.outputs.digest != '' }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373
with:
subject-name: ${{ steps.image.outputs.name }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
- name: Make Docker image public
# Idempotent — patching to public when already public is a no-op.
# Previously lived in image.yaml; folded in here on consolidation.
run: |
curl \
-X PATCH \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/user/packages/container/${{ env.IMAGE_NAME }}/visibility \
-d '{"visibility":"public"}'