mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-06 14:33:11 +08:00
feat: add Docker CI to build all-in-one image with agent-server + automation + frontend (#634)
* feat: add Docker CI to build all-in-one image with agent-server + automation + frontend
Adds a GitHub Actions workflow (.github/workflows/docker.yml) that builds and
publishes ghcr.io/openhands/agent-canvas — a single Docker image combining:
1. Agent Server (ghcr.io/openhands/agent-server base image from SDK repo)
2. Automation server (pip-installed from openhands-automation)
3. agent-canvas frontend (static build from this repo)
The automation server is pip-installed rather than copied from its Docker image
because both services share openhands-sdk, fastapi, uvicorn, pydantic, httpx
etc. — installing into the agent-server's Python 3.13 deduplicates all shared
packages. Only automation-specific deps (asyncpg, sqlalchemy, boto3, …) are
added on top.
An entrypoint script starts all three services and a static-server proxy that
unifies them behind a single port (default 8000):
/api/automation/* → automation backend (:18001)
/api/* → agent-server (:18000)
/* → static frontend + SPA fallback
Workflow triggers:
- Push to main: builds and pushes with branch + SHA tags
- v* tags (releases): also pushes semver tags (1.2.3, 1.2, 1, latest)
- PRs: builds, pushes SHA-tagged image, updates PR description with
pull/run instructions (same pattern as the SDK repo)
- workflow_dispatch: supports overriding base image and automation version
Files added:
- docker/Dockerfile (multi-stage: frontend build + agent-server base)
- docker/entrypoint.sh (process manager for all three services)
- .dockerignore
- .github/workflows/docker.yml
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: build multi-arch Docker images (amd64 + arm64)
Adds QEMU setup for cross-compilation and defaults the platform matrix
to linux/amd64,linux/arm64 so the image works on both Intel and Apple
Silicon machines.
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: rewrite Docker workflow to match SDK repo structure
Replace the single-job QEMU approach with the same architecture-matrix
pattern used by the SDK repo's server.yml:
1. build-and-push-image — matrix over {amd64, arm64} with native runners
(ubuntu-24.04 for amd64, ubuntu-24.04-arm for arm64). Each job pushes
arch-suffixed tags (e.g. sha-abc1234-amd64) and uploads build-info
artifacts.
2. merge-manifests — downloads both arch build-infos, strips the -amd64
suffix from amd64 tags to derive manifest tags, and creates multi-arch
manifests via `docker buildx imagetools create`.
3. consolidate-build-info — aggregates all build-info and manifest-info
artifacts into a single JSON summary (PR-only).
4. update-pr-description — renders the summary into the PR body between
AGENT_CANVAS_DOCKER_START/END markers.
Native runners avoid the 3-5× slowdown of QEMU emulation for arm64
builds.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: sanitize branch names in Docker tags (/ is not allowed)
Branch names like 'feat/docker-ci' produce invalid Docker tags because
'/' is forbidden in tag names. Replace '/' with '-' so the tag becomes
'feat-docker-ci-amd64'.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: default automation to SQLite and fix wait blocking proxy startup
Two bugs:
1. The automation server defaults to PostgreSQL on localhost, which
doesn't exist in the all-in-one container. Default AUTOMATION_DB_URL
to sqlite+aiosqlite:// so it works out of the box. Users can override
with a real Postgres URL for production.
2. The bare 'wait' command waited for ALL background children — including
the long-running agent-server and automation processes — so the
static-server/proxy on port 8000 never started. Fix by waiting only
for the wait_for_port subshell PIDs.
Verified locally: all three services start, endpoints respond correctly,
no more scheduler ConnectionRefusedError.
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: add VOLUME directives for persistence and project mounts
Declare /home/openhands/.openhands (settings, secrets, conversations,
automation SQLite DB) and /projects (user code) as Docker volumes so
data survives container restarts by default. Users should bind-mount
these for durable persistence:
docker run -v ~/.openhands:/home/openhands/.openhands \
-v ~/projects:/projects \
-p 8000:8000 ghcr.io/openhands/agent-canvas
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: set OH_SECRET_KEY default and pre-create persistence dirs
Three issues fixed:
1. OH_SECRET_KEY was not set → agent-server refused to return encrypted
secrets → conversation creation failed with 503. Set the same static
default used by dev-safe.mjs / dev-docker.mjs.
2. Persistence dirs (conversations, bash_events, automation DB) were not
pre-created → the openhands user got PermissionError when the VOLUME
directive created them as root. Pre-create with correct ownership
before the USER switch in the Dockerfile.
3. Set OH_PERSISTENCE_DIR, OH_CONVERSATIONS_PATH, OH_BASH_EVENTS_DIR
defaults in the entrypoint (matching dev-docker.mjs) so data lands
under the well-known ~/.openhands tree.
Verified locally: all three services start clean, no warnings about
OH_SECRET_KEY, SQLite migrations apply successfully.
Co-authored-by: openhands <openhands@all-hands.dev>
* chore: merge main and remove stale dev-docker.mjs references
Main removed scripts/dev-docker.mjs (Docker is no longer a dependency of
the npm package flow). Update comments in docker.yml, entrypoint.sh, and
AGENTS.md that referenced the deleted file.
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: centralize config into config/defaults.json (single source of truth)
All version pins, port defaults, persistence paths, package names, and
the dev secret key now live in config/defaults.json. Consumers read from
it instead of hardcoding values:
- scripts/dev-safe.mjs: reads via JSON.parse(readFileSync(...))
- scripts/dev-with-automation.mjs: same
- scripts/check-sdk-version-sync.mjs: same (no longer regex-parses JS)
- docker/Dockerfile: config-gen build stage converts JSON to
/opt/agent-canvas/defaults.env (shell-sourceable)
- docker/entrypoint.sh: sources defaults.env at startup; also adds
session API key auto-generation so the image doesn't run wide-open
- .github/workflows/docker.yml: reads versions from JSON in a setup
step (no more hardcoded env vars)
To bump a version, edit config/defaults.json only.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: address PR review feedback (#634)
- Fix PID tracking bug: move PIDS+=($!) inside if/elif branches so the
else (automation-not-found) path doesn't add a stale PID
- chmod 600 session API key file to prevent credential leak
- Warn when using insecure default OH_SECRET_KEY in Docker entrypoint
- Add try/catch + field validation for config/defaults.json loading in
check-sdk-version-sync.mjs
- Fix semver tag parsing: strip pre-release/build metadata, only create
abbreviated tags (major.minor, major, latest) for stable releases
- Sanitize branch names for Docker tags (tr invalid chars, strip leading
dot/dash) to handle branches with #, @, spaces, etc.
- Add arch validation before manifest merge (assert both amd64.json and
arm64.json exist)
- Remove $schema reference to non-existent defaults.schema.json
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: remove hardcoded version defaults from Dockerfile
Replace hardcoded ARG defaults (AGENT_SERVER_IMAGE, AUTOMATION_VERSION)
with empty ARGs. Values are always derived from config/defaults.json:
- CI: reads JSON in the workflow config step, passes --build-arg
- Local: new scripts/docker-build.mjs helper reads JSON and invokes
docker build with the correct --build-arg values
Added npm run build:docker convenience script.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: stabilize snapshot tests and auto-generate Docker secret key
Two fixes:
1. **Flaky snapshot tests**: The 'Local pagination fixture' mock conversation
used a fixed absolute timestamp (PAGINATION_BASE_TIME = May 13, 2026) for
its created_at/updated_at, while 'Errored Project' used a relative
timestamp (now - 7d). As real time progressed past the crossover point,
their sort order in the sidebar flipped, causing 30/73 snapshot diffs on
every PR. Fix: use relative timestamps (now - 6d) for the pagination
fixture's conversation listing fields. The internal event timestamps
(used by pagination tests) still use PAGINATION_BASE_TIME — only the
sidebar ordering is affected.
2. **Docker OH_SECRET_KEY**: The entrypoint used a static insecure default
for OH_SECRET_KEY and warned about it. Now mirrors the session API key
pattern: auto-generate a cryptographic random key on first run, persist
it to ~/.openhands/agent-canvas/secret-key.txt, and reuse on restart.
Users can still override via the OH_SECRET_KEY env var. Removed the
now-unused CONFIG_SECRET_KEY from the Docker defaults.env generation.
Also deduped STATE_DIR computation (was repeated for session key path).
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md with mock timestamp and Docker secret key notes
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: include canvas_ui tool in Docker image
The Docker image was missing the tools/ directory and OH_EXTRA_PYTHON_PATH,
so the agent-server couldn't import canvas_ui_tool.py when the frontend
sent canvas_ui in the conversation tools list. This caused:
HTTP 500: ToolDefinition 'canvas_ui' is not registered
Fix: COPY tools/ into the image and set OH_EXTRA_PYTHON_PATH in the
entrypoint, matching what scripts/dev-safe.mjs already does for local dev.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
committed by
GitHub
co-authored by
openhands
parent
1a6e629fda
commit
979e64fe19
@@ -0,0 +1,39 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs (rebuilt in Docker)
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.openhands/
|
||||||
|
.agents/
|
||||||
|
.pr/
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
__tests__/
|
||||||
|
tests/
|
||||||
|
__mocks__/
|
||||||
|
test-results/
|
||||||
|
playwright-report*/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# IDE / OS
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.sample
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
artifacts/
|
||||||
|
*.tgz
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
---
|
||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
agent_server_image:
|
||||||
|
description: Agent Server base image (ghcr.io/openhands/agent-server:TAG)
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
automation_version:
|
||||||
|
description: Automation server version (pip)
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
image:
|
||||||
|
description: GHCR image name
|
||||||
|
type: string
|
||||||
|
default: ghcr.io/openhands/agent-canvas
|
||||||
|
|
||||||
|
# Cancel redundant runs for the same branch/PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: ${{ inputs.image != '' && inputs.image || 'ghcr.io/openhands/agent-canvas' }}
|
||||||
|
# Use the PR head SHA for PR events so tags point at the actual code.
|
||||||
|
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
RELEVANT_REF: ${{ github.head_ref != '' && format('refs/heads/{0}', github.head_ref) || github.ref }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Build & Push (per-architecture, native runners)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
build-and-push-image:
|
||||||
|
name: Build & Push (${{ matrix.arch }})
|
||||||
|
# Skip fork PRs — they cannot authenticate to GHCR.
|
||||||
|
if: >
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event_name == 'pull_request' &&
|
||||||
|
!github.event.pull_request.head.repo.fork)
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
runner: ubuntu-24.04
|
||||||
|
platform: linux/amd64
|
||||||
|
- arch: arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
timeout-minutes: 45
|
||||||
|
|
||||||
|
env:
|
||||||
|
ARCH: ${{ matrix.arch }}
|
||||||
|
PLATFORM: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || '' }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Read defaults from config/defaults.json
|
||||||
|
id: config
|
||||||
|
run: |
|
||||||
|
# Single source of truth for version pins — no hardcoded values in this workflow.
|
||||||
|
AGENT_SERVER_VERSION=$(node -p "require('./config/defaults.json').versions.agentServer")
|
||||||
|
AGENT_SERVER_IMAGE_BASE=$(node -p "require('./config/defaults.json').images.agentServer")
|
||||||
|
AUTOMATION_VERSION=$(node -p "require('./config/defaults.json').versions.automation")
|
||||||
|
echo "agent_server_version=$AGENT_SERVER_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "default_agent_server_image=${AGENT_SERVER_IMAGE_BASE}:${AGENT_SERVER_VERSION}-python" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "default_automation_version=$AUTOMATION_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Compute metadata and tags
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
SHORT_SHA=$(echo "$RELEVANT_SHA" | cut -c1-7)
|
||||||
|
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Resolve agent-server base image (input override > config/defaults.json)
|
||||||
|
if [ -n "${{ inputs.agent_server_image }}" ]; then
|
||||||
|
echo "agent_server_image=${{ inputs.agent_server_image }}" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "agent_server_image=${{ steps.config.outputs.default_agent_server_image }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve automation version (input override > config/defaults.json)
|
||||||
|
if [ -n "${{ inputs.automation_version }}" ]; then
|
||||||
|
echo "automation_version=${{ inputs.automation_version }}" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "automation_version=${{ steps.config.outputs.default_automation_version }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build arch-suffixed tags (e.g., sha-abc1234-amd64)
|
||||||
|
TAGS=""
|
||||||
|
add_tag() { TAGS="${TAGS:+${TAGS},}${IMAGE}:${1}-${ARCH}"; }
|
||||||
|
|
||||||
|
# SHA tags (always)
|
||||||
|
add_tag "sha-${SHORT_SHA}"
|
||||||
|
|
||||||
|
# Branch / PR / tag-based tags
|
||||||
|
if [[ "$RELEVANT_REF" == refs/heads/* ]]; then
|
||||||
|
# Sanitize branch name for Docker tag safety:
|
||||||
|
# replace any char outside [a-zA-Z0-9._-] with -, strip leading/trailing .-
|
||||||
|
BRANCH="${RELEVANT_REF#refs/heads/}"
|
||||||
|
BRANCH=$(echo "$BRANCH" | tr -c 'a-zA-Z0-9._-' '-' | sed 's/^[-.]//; s/[-.]*$//')
|
||||||
|
add_tag "$BRANCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
add_tag "pr-${{ github.event.pull_request.number }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$RELEVANT_REF" == refs/tags/v* ]]; then
|
||||||
|
VERSION="${RELEVANT_REF#refs/tags/v}"
|
||||||
|
add_tag "$VERSION"
|
||||||
|
|
||||||
|
# Strip pre-release/build metadata for major.minor.patch derivation
|
||||||
|
VERSION_BASE="${VERSION%%-*}"
|
||||||
|
VERSION_BASE="${VERSION_BASE%%+*}"
|
||||||
|
|
||||||
|
# Only create abbreviated + latest tags for stable (non-pre-release) versions
|
||||||
|
if [[ "$VERSION" != *"-"* ]] && [[ "$VERSION" != *"+"* ]]; then
|
||||||
|
# major.minor
|
||||||
|
MINOR="${VERSION_BASE%.*}"
|
||||||
|
if [ "$MINOR" != "$VERSION_BASE" ]; then
|
||||||
|
add_tag "$MINOR"
|
||||||
|
fi
|
||||||
|
# major
|
||||||
|
MAJOR="${VERSION_BASE%%.*}"
|
||||||
|
if [ "$MAJOR" != "$VERSION_BASE" ] && [ "$MAJOR" != "$MINOR" ]; then
|
||||||
|
add_tag "$MAJOR"
|
||||||
|
fi
|
||||||
|
add_tag "latest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "=== Build outputs ==="
|
||||||
|
echo "Short SHA: $SHORT_SHA"
|
||||||
|
echo "Tags: $TAGS"
|
||||||
|
echo "===================="
|
||||||
|
|
||||||
|
- name: Build & Push (${{ matrix.arch }})
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.prep.outputs.tags }}
|
||||||
|
build-args: |
|
||||||
|
AGENT_SERVER_IMAGE=${{ steps.prep.outputs.agent_server_image }}
|
||||||
|
AUTOMATION_VERSION=${{ steps.prep.outputs.automation_version }}
|
||||||
|
OPENHANDS_BUILD_GIT_SHA=${{ env.RELEVANT_SHA }}
|
||||||
|
OPENHANDS_BUILD_GIT_REF=${{ env.RELEVANT_REF }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
|
||||||
|
- name: Summary (${{ matrix.arch }})
|
||||||
|
run: |
|
||||||
|
echo "Image: ${{ env.IMAGE }}"
|
||||||
|
echo "Architecture: ${{ matrix.arch }}"
|
||||||
|
echo "Platform: ${{ matrix.platform }}"
|
||||||
|
echo "Short SHA: ${{ steps.prep.outputs.short_sha }}"
|
||||||
|
echo "Tags: ${{ steps.prep.outputs.tags }}"
|
||||||
|
echo "Build digest: ${{ steps.build.outputs.digest }}"
|
||||||
|
|
||||||
|
- name: Save build info for consolidation
|
||||||
|
run: |
|
||||||
|
mkdir -p build-info
|
||||||
|
jq -n \
|
||||||
|
--arg arch "${{ matrix.arch }}" \
|
||||||
|
--arg image "${{ env.IMAGE }}" \
|
||||||
|
--arg short_sha "${{ steps.prep.outputs.short_sha }}" \
|
||||||
|
--arg tags "${{ steps.prep.outputs.tags }}" \
|
||||||
|
--arg agent_server_image "${{ steps.prep.outputs.agent_server_image }}" \
|
||||||
|
--arg automation_version "${{ steps.prep.outputs.automation_version }}" \
|
||||||
|
--arg platform "${{ matrix.platform }}" \
|
||||||
|
--arg git_sha "${{ env.RELEVANT_SHA }}" \
|
||||||
|
--arg git_ref "${{ env.RELEVANT_REF }}" \
|
||||||
|
'{arch: $arch, image: $image, short_sha: $short_sha, tags: $tags, agent_server_image: $agent_server_image, automation_version: $automation_version, platform: $platform, git_sha: $git_sha, git_ref: $git_ref}' \
|
||||||
|
> "build-info/${{ matrix.arch }}.json"
|
||||||
|
cat "build-info/${{ matrix.arch }}.json"
|
||||||
|
|
||||||
|
- name: Upload build info artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: build-info-${{ matrix.arch }}
|
||||||
|
path: build-info/${{ matrix.arch }}.json
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Merge Multi-Arch Manifests
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
merge-manifests:
|
||||||
|
name: Merge Multi-Arch Manifests
|
||||||
|
needs: build-and-push-image
|
||||||
|
if: >
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event_name == 'pull_request' &&
|
||||||
|
!github.event.pull_request.head.repo.fork)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download build info artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: build-info-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: build-info
|
||||||
|
|
||||||
|
- name: Extract SHORT_SHA from build info
|
||||||
|
id: get_sha
|
||||||
|
run: |
|
||||||
|
SHORT_SHA=$(jq -r '.short_sha' build-info/amd64.json)
|
||||||
|
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Using SHORT_SHA: $SHORT_SHA"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create and push multi-arch manifests
|
||||||
|
id: create_manifests
|
||||||
|
run: |
|
||||||
|
# Validate both architectures built successfully
|
||||||
|
if [[ ! -f build-info/amd64.json ]] || [[ ! -f build-info/arm64.json ]]; then
|
||||||
|
echo "::error::Missing architecture builds (need both amd64.json and arm64.json)"
|
||||||
|
echo "Available: $(ls -1 build-info/*.json 2>/dev/null || echo 'none')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHORT_SHA=${{ steps.get_sha.outputs.short_sha }}
|
||||||
|
AMD64_TAGS_CSV=$(jq -r '.tags' build-info/amd64.json)
|
||||||
|
declare -A SEEN_MANIFEST_TAGS=()
|
||||||
|
MANIFEST_TAGS=()
|
||||||
|
|
||||||
|
create_manifest() {
|
||||||
|
local manifest_tag=$1
|
||||||
|
local source_tag=${2:-$1}
|
||||||
|
|
||||||
|
echo "Creating multi-arch manifest: ${IMAGE}:${manifest_tag}"
|
||||||
|
docker buildx imagetools create -t "${IMAGE}:${manifest_tag}" \
|
||||||
|
"${IMAGE}:${source_tag}-amd64" \
|
||||||
|
"${IMAGE}:${source_tag}-arm64"
|
||||||
|
|
||||||
|
echo "Inspecting multi-arch manifest:"
|
||||||
|
docker buildx imagetools inspect "${IMAGE}:${manifest_tag}"
|
||||||
|
echo "✓ Multi-arch manifest created: ${IMAGE}:${manifest_tag}"
|
||||||
|
}
|
||||||
|
|
||||||
|
IFS=',' read -ra AMD64_TAGS <<< "$AMD64_TAGS_CSV"
|
||||||
|
for AMD64_IMAGE_TAG in "${AMD64_TAGS[@]}"; do
|
||||||
|
if [ -z "$AMD64_IMAGE_TAG" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG_NAME=${AMD64_IMAGE_TAG#${IMAGE}:}
|
||||||
|
if [ "$TAG_NAME" = "$AMD64_IMAGE_TAG" ] || [[ ! "$TAG_NAME" == *-amd64 ]]; then
|
||||||
|
echo "Skipping unexpected architecture tag: $AMD64_IMAGE_TAG"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST_TAG=${TAG_NAME%-amd64}
|
||||||
|
if [ -n "${SEEN_MANIFEST_TAGS[$MANIFEST_TAG]+x}" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
SEEN_MANIFEST_TAGS[$MANIFEST_TAG]=1
|
||||||
|
MANIFEST_TAGS+=("$MANIFEST_TAG")
|
||||||
|
create_manifest "$MANIFEST_TAG"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Preserve a latest alias on main pushes.
|
||||||
|
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||||
|
LATEST_TAG="latest"
|
||||||
|
create_manifest "$LATEST_TAG" "main"
|
||||||
|
MANIFEST_TAGS+=("$LATEST_TAG")
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST_TAG_CSV=$(IFS=,; echo "${MANIFEST_TAGS[*]}")
|
||||||
|
echo "manifest_tags=$MANIFEST_TAG_CSV" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Save manifest info for consolidation
|
||||||
|
mkdir -p manifest-info
|
||||||
|
jq -n \
|
||||||
|
--arg image "${{ env.IMAGE }}" \
|
||||||
|
--arg short_sha "$SHORT_SHA" \
|
||||||
|
--arg manifest_tags "$MANIFEST_TAG_CSV" \
|
||||||
|
'{image: $image, short_sha: $short_sha, manifest_tags: $manifest_tags}' \
|
||||||
|
> manifest-info/manifests.json
|
||||||
|
cat manifest-info/manifests.json
|
||||||
|
|
||||||
|
- name: Upload manifest info artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: manifest-info
|
||||||
|
path: manifest-info/manifests.json
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Consolidate Build Information
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
consolidate-build-info:
|
||||||
|
name: Consolidate Build Information
|
||||||
|
needs: [build-and-push-image, merge-manifests]
|
||||||
|
if: github.event_name == 'pull_request' && always() && (needs.build-and-push-image.result == 'success' || needs.build-and-push-image.result == 'failure')
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
build_summary: ${{ steps.consolidate.outputs.build_summary }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download build info artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: build-info-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: build-info
|
||||||
|
|
||||||
|
- name: Download manifest info artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: manifest-info
|
||||||
|
path: manifest-info
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Consolidate build information from artifacts
|
||||||
|
id: consolidate
|
||||||
|
run: |
|
||||||
|
echo "Processing build info artifacts..."
|
||||||
|
ls -la build-info/
|
||||||
|
|
||||||
|
IMAGE=""
|
||||||
|
SHORT_SHA=""
|
||||||
|
ALL_TAGS=""
|
||||||
|
AGENT_SERVER_IMAGE=""
|
||||||
|
AUTOMATION_VERSION=""
|
||||||
|
GIT_SHA=""
|
||||||
|
GIT_REF=""
|
||||||
|
ARCHS=""
|
||||||
|
|
||||||
|
for info_file in build-info/*.json; do
|
||||||
|
if [[ ! -f "$info_file" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "=== Processing $info_file ==="
|
||||||
|
cat "$info_file"
|
||||||
|
|
||||||
|
ARCH=$(jq -r '.arch' "$info_file")
|
||||||
|
FILE_IMAGE=$(jq -r '.image' "$info_file")
|
||||||
|
FILE_SHA=$(jq -r '.short_sha' "$info_file")
|
||||||
|
FILE_TAGS=$(jq -r '.tags' "$info_file")
|
||||||
|
|
||||||
|
if [[ -z "$IMAGE" ]]; then
|
||||||
|
IMAGE="$FILE_IMAGE"
|
||||||
|
SHORT_SHA="$FILE_SHA"
|
||||||
|
AGENT_SERVER_IMAGE=$(jq -r '.agent_server_image' "$info_file")
|
||||||
|
AUTOMATION_VERSION=$(jq -r '.automation_version' "$info_file")
|
||||||
|
GIT_SHA=$(jq -r '.git_sha' "$info_file")
|
||||||
|
GIT_REF=$(jq -r '.git_ref' "$info_file")
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCHS="${ARCHS:+${ARCHS}, }${ARCH}"
|
||||||
|
|
||||||
|
if [[ -n "$FILE_TAGS" ]]; then
|
||||||
|
TAG_LIST=$(echo "$FILE_TAGS" | tr ',' '\n')
|
||||||
|
ALL_TAGS="${ALL_TAGS:+${ALL_TAGS}
|
||||||
|
}${TAG_LIST}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add manifest tags
|
||||||
|
if [[ -f "manifest-info/manifests.json" ]]; then
|
||||||
|
MANIFEST_TAG_CSV=$(jq -r '.manifest_tags' manifest-info/manifests.json)
|
||||||
|
MANIFEST_TAG_LIST=$(echo "$MANIFEST_TAG_CSV" | tr ',' '\n' | sed "s|^|${IMAGE}:|")
|
||||||
|
ALL_TAGS="${ALL_TAGS:+${ALL_TAGS}
|
||||||
|
}${MANIFEST_TAG_LIST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUILD_SUMMARY=$(jq -n \
|
||||||
|
--arg image "$IMAGE" \
|
||||||
|
--arg short_sha "$SHORT_SHA" \
|
||||||
|
--arg all_tags "$ALL_TAGS" \
|
||||||
|
--arg archs "$ARCHS" \
|
||||||
|
--arg agent_server_image "$AGENT_SERVER_IMAGE" \
|
||||||
|
--arg automation_version "$AUTOMATION_VERSION" \
|
||||||
|
--arg git_sha "$GIT_SHA" \
|
||||||
|
--arg git_ref "$GIT_REF" \
|
||||||
|
--arg ghcr_url "https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas" \
|
||||||
|
'{image: $image, short_sha: $short_sha, all_tags: $all_tags, architectures: $archs, agent_server_image: $agent_server_image, automation_version: $automation_version, git_sha: $git_sha, git_ref: $git_ref, ghcr_package_url: $ghcr_url}')
|
||||||
|
|
||||||
|
echo "Consolidated build summary:"
|
||||||
|
echo "$BUILD_SUMMARY" | jq .
|
||||||
|
|
||||||
|
{
|
||||||
|
echo 'build_summary<<EOF'
|
||||||
|
echo "$BUILD_SUMMARY"
|
||||||
|
echo 'EOF'
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Update PR description with image info (PRs only)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
update-pr-description:
|
||||||
|
name: Update PR description with Docker image
|
||||||
|
needs: consolidate-build-info
|
||||||
|
if: github.event_name == 'pull_request' && needs.consolidate-build-info.result == 'success'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Generate PR description from build summary
|
||||||
|
id: generate_description
|
||||||
|
run: |
|
||||||
|
BUILD_SUMMARY='${{ needs.consolidate-build-info.outputs.build_summary }}'
|
||||||
|
echo "Build summary received:"
|
||||||
|
echo "$BUILD_SUMMARY" | jq .
|
||||||
|
|
||||||
|
IMAGE=$(echo "$BUILD_SUMMARY" | jq -r '.image')
|
||||||
|
SHORT_SHA=$(echo "$BUILD_SUMMARY" | jq -r '.short_sha')
|
||||||
|
GHCR_URL=$(echo "$BUILD_SUMMARY" | jq -r '.ghcr_package_url')
|
||||||
|
ALL_TAGS=$(echo "$BUILD_SUMMARY" | jq -r '.all_tags')
|
||||||
|
ARCHS=$(echo "$BUILD_SUMMARY" | jq -r '.architectures')
|
||||||
|
AGENT_SERVER_IMAGE=$(echo "$BUILD_SUMMARY" | jq -r '.agent_server_image')
|
||||||
|
AUTOMATION_VERSION=$(echo "$BUILD_SUMMARY" | jq -r '.automation_version')
|
||||||
|
GIT_SHA=$(echo "$BUILD_SUMMARY" | jq -r '.git_sha')
|
||||||
|
|
||||||
|
PR_CONTENT=$(cat << EOF
|
||||||
|
|
||||||
|
<!-- AGENT_CANVAS_DOCKER_START -->
|
||||||
|
---
|
||||||
|
**🐳 Docker images for this PR**
|
||||||
|
|
||||||
|
• **GHCR package:** ${GHCR_URL}
|
||||||
|
|
||||||
|
| Component | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Image** | \`${IMAGE}\` |
|
||||||
|
| **Architectures** | ${ARCHS} |
|
||||||
|
| **Agent Server** | \`${AGENT_SERVER_IMAGE}\` |
|
||||||
|
| **Automation** | \`openhands-automation==${AUTOMATION_VERSION}\` |
|
||||||
|
| **Commit** | \`${GIT_SHA}\` |
|
||||||
|
|
||||||
|
**Pull (multi-arch manifest)**
|
||||||
|
\`\`\`bash
|
||||||
|
# Multi-arch manifest — Docker automatically pulls the correct architecture
|
||||||
|
docker pull ${IMAGE}:sha-${SHORT_SHA}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Run**
|
||||||
|
\`\`\`bash
|
||||||
|
docker run -it --rm \\
|
||||||
|
-p 8000:8000 \\
|
||||||
|
${IMAGE}:sha-${SHORT_SHA}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**All tags pushed for this build**
|
||||||
|
\`\`\`
|
||||||
|
${ALL_TAGS}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**About Multi-Architecture Support**
|
||||||
|
- Each tag (e.g., \`sha-${SHORT_SHA}\`) is a **multi-arch manifest** supporting both **amd64** and **arm64**
|
||||||
|
- Docker automatically pulls the correct architecture for your platform
|
||||||
|
- Individual architecture tags (e.g., \`sha-${SHORT_SHA}-amd64\`) are also available if needed
|
||||||
|
<!-- AGENT_CANVAS_DOCKER_END -->
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo 'pr_content<<EOF'
|
||||||
|
echo "$PR_CONTENT"
|
||||||
|
echo 'EOF'
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Update PR description with docker image details
|
||||||
|
uses: nefrob/pr-description@v1.2.0
|
||||||
|
with:
|
||||||
|
content: ${{ steps.generate_description.outputs.pr_content }}
|
||||||
|
regex: "<!-- AGENT_CANVAS_DOCKER_START -->.*?<!-- AGENT_CANVAS_DOCKER_END -->"
|
||||||
|
regexFlags: s
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -119,6 +119,7 @@ you are running inside of — NOT the automation backend.
|
|||||||
- Snapshots are organized by `{snapshotDir}/{testFilePath}/{projectName}/{arg}.png` (configured in `playwright.config.ts`).
|
- Snapshots are organized by `{snapshotDir}/{testFilePath}/{projectName}/{arg}.png` (configured in `playwright.config.ts`).
|
||||||
- **Conversation page snapshot tests**: The dev server uses MSW service workers for API mocking. For conversation-page tests, rely on MSW's pre-defined mock conversations (IDs "1", "2", "3" in `src/mocks/conversation-handlers.ts`) rather than fighting Playwright route interception. MSW's service worker intercepts requests before Playwright `page.route()` can; Playwright route interceptors only see requests that escape the service worker. Stub WebSocket via `page.addInitScript()` and inject events into the Zustand store via the exposed `window.__OH_EVENT_STORE__` API. Use `test.describe.configure({ mode: "serial" })` for conversation tests since the WebSocket stub + heavier page setup can cause intermittent failures in parallel mode.
|
- **Conversation page snapshot tests**: The dev server uses MSW service workers for API mocking. For conversation-page tests, rely on MSW's pre-defined mock conversations (IDs "1", "2", "3" in `src/mocks/conversation-handlers.ts`) rather than fighting Playwright route interception. MSW's service worker intercepts requests before Playwright `page.route()` can; Playwright route interceptors only see requests that escape the service worker. Stub WebSocket via `page.addInitScript()` and inject events into the Zustand store via the exposed `window.__OH_EVENT_STORE__` API. Use `test.describe.configure({ mode: "serial" })` for conversation tests since the WebSocket stub + heavier page setup can cause intermittent failures in parallel mode.
|
||||||
- **Baseline generation for CI**: Baselines generated locally will NOT match CI (different OS, fonts, rendering). Baselines are regenerated automatically on every push to `main`. After adding new snapshot tests, open a PR — the new snapshots will be shown as "🆕 New" in the PR comment and become the baseline when the PR merges. To force-refresh baselines from main without waiting for a code push, trigger the "Snapshot Tests" workflow manually with `force_update=true`.
|
- **Baseline generation for CI**: Baselines generated locally will NOT match CI (different OS, fonts, rendering). Baselines are regenerated automatically on every push to `main`. After adding new snapshot tests, open a PR — the new snapshots will be shown as "🆕 New" in the PR comment and become the baseline when the PR merges. To force-refresh baselines from main without waiting for a code push, trigger the "Snapshot Tests" workflow manually with `force_update=true`.
|
||||||
|
- **Mock conversation timestamps must ALL be `now`-relative**: `src/mocks/conversation-handlers.ts` defines mock conversations sorted by `updated_at` descending in the sidebar. Every conversation's `created_at`/`updated_at` must use `now - X * days` (relative to module-load time), never a fixed absolute date like `PAGINATION_BASE_TIME`. Mixing the two strategies causes a sort-order crossover as real time passes — the fixed-date conversation ages past a relative one and they swap positions, breaking every snapshot that includes the sidebar. `PAGINATION_BASE_TIME` is kept only for internal event timestamps used by pagination tests; conversation listing timestamps are decoupled from it.
|
||||||
- **MSW handler state is PAGE-level JS, not service-worker state**: In MSW 2.x browser mode the request handlers (including mutable Maps like `automations`) are compiled into the client bundle and run in the main thread. `page.reload()` re-initialises all module-level state (e.g. `const automations = new Map(...)` runs fresh on every page load). Tests that need to show an "empty list" state must NOT call `page.reload()` after deleting items. Instead: make the DELETE fetches from `page.evaluate()` (which DO go through MSW), then call `window.__TEST_INVALIDATE_QUERIES__()` (exposed in mock mode by `entry.client.tsx`) to trigger React Query's cache invalidation in-place without a reload. Example: the automations empty-state snapshot test in `tests/e2e/snapshots/automations.snapshot.spec.ts`.
|
- **MSW handler state is PAGE-level JS, not service-worker state**: In MSW 2.x browser mode the request handlers (including mutable Maps like `automations`) are compiled into the client bundle and run in the main thread. `page.reload()` re-initialises all module-level state (e.g. `const automations = new Map(...)` runs fresh on every page load). Tests that need to show an "empty list" state must NOT call `page.reload()` after deleting items. Instead: make the DELETE fetches from `page.evaluate()` (which DO go through MSW), then call `window.__TEST_INVALIDATE_QUERIES__()` (exposed in mock mode by `entry.client.tsx`) to trigger React Query's cache invalidation in-place without a reload. Example: the automations empty-state snapshot test in `tests/e2e/snapshots/automations.snapshot.spec.ts`.
|
||||||
|
|
||||||
## Live End-to-End Test Framework
|
## Live End-to-End Test Framework
|
||||||
@@ -337,7 +338,7 @@ return new ConversationClient(getAgentServerClientOptions()).someMethod(...);
|
|||||||
- `/api/*`, `/sockets`, etc. → agent server (:18000)
|
- `/api/*`, `/sockets`, etc. → agent server (:18000)
|
||||||
- `/*` (default) → frontend server (:3001), either Vite or static depending on launcher mode
|
- `/*` (default) → frontend server (:3001), either Vite or static depending on launcher mode
|
||||||
- Environment variables: `PORT` (ingress port, default: 8000), `OH_AUTOMATION_GIT_REF` (git ref, overrides default version), `OH_AUTOMATION_VERSION` (default: `1.0.0a3`), `AUTOMATION_LOCAL_API_KEY` (optional, use a fixed key; default: persisted generated key), `OH_AUTOMATION_API_KEY_PATH` (override the persisted default key path)
|
- Environment variables: `PORT` (ingress port, default: 8000), `OH_AUTOMATION_GIT_REF` (git ref, overrides default version), `OH_AUTOMATION_VERSION` (default: `1.0.0a3`), `AUTOMATION_LOCAL_API_KEY` (optional, use a fixed key; default: persisted generated key), `OH_AUTOMATION_API_KEY_PATH` (override the persisted default key path)
|
||||||
- `scripts/check-sdk-version-sync.mjs` checks the released `openhands-automation` package against `DEFAULT_AUTOMATION_SDK_VERSION` in `scripts/dev-with-automation.mjs`; that value may intentionally lag `DEFAULT_AGENT_SERVER_VERSION` while automation has not yet published a matching release.
|
- `scripts/check-sdk-version-sync.mjs` checks the released `openhands-automation` package against `versions.automationSdk` in `config/defaults.json`; that value may intentionally lag `versions.agentServer` while automation has not yet published a matching release.
|
||||||
- Access points: `http://localhost:8000/` (main UI), `http://localhost:8000/api/automation/docs` (API docs)
|
- Access points: `http://localhost:8000/` (main UI), `http://localhost:8000/api/automation/docs` (API docs)
|
||||||
- Security: `AUTOMATION_LOCAL_API_KEY` defaults to a generated key persisted across restarts because static frontend builds bake it into `VITE_AUTOMATION_API_KEY`. Set the env var explicitly to rotate or pin it. The cipher key (`OH_SECRET_KEY`) keeps a static default for local dev since it's used for encrypting/decrypting persisted settings values.
|
- Security: `AUTOMATION_LOCAL_API_KEY` defaults to a generated key persisted across restarts because static frontend builds bake it into `VITE_AUTOMATION_API_KEY`. Set the env var explicitly to rotate or pin it. The cipher key (`OH_SECRET_KEY`) keeps a static default for local dev since it's used for encrypting/decrypting persisted settings values.
|
||||||
- `scripts/ingress.mjs` is a standalone HTTP reverse proxy that can be used independently to route traffic to multiple backends based on URL path prefix.
|
- `scripts/ingress.mjs` is a standalone HTTP reverse proxy that can be used independently to route traffic to multiple backends based on URL path prefix.
|
||||||
@@ -438,4 +439,12 @@ return new ConversationClient(getAgentServerClientOptions()).someMethod(...);
|
|||||||
|
|
||||||
- ESLint config (flat, ESLint 9): the project uses `eslint.config.js` (not `.eslintrc`) and runs on `eslint@9.x`, not 10. The constraint pinning us below 10 is `eslint-plugin-react@7.37.x`, which still calls `context.getFilename()` at rule-load time — that API was removed in ESLint 10 and `@eslint/compat`'s `fixupPluginRules` does NOT shim it. Don't try to bump eslint past 9 until eslint-plugin-react ships a v10-compatible release. Import rules come from `eslint-plugin-import-x` (the maintained fork of `eslint-plugin-import`) but are registered under both `import-x/` and `import/` prefixes via `plugins: { import: importXPlugin, ... }` so existing `// eslint-disable-next-line import/...` directives keep working. `linterOptions.reportUnusedDisableDirectives` is set to `"warn"` (not "off") so stale airbnb-era disable comments still surface in lint output without failing CI. The TS-overrides block has an `ignores: ["src/hooks/query/query-keys.ts"]` so the `no-restricted-syntax` rule banning raw `["settings", ...]` query keys doesn't fire on the file that defines the helpers themselves. No `.npmrc` / `legacy-peer-deps` flag is needed — all our plugins declare ESLint 9 peer compatibility.
|
- ESLint config (flat, ESLint 9): the project uses `eslint.config.js` (not `.eslintrc`) and runs on `eslint@9.x`, not 10. The constraint pinning us below 10 is `eslint-plugin-react@7.37.x`, which still calls `context.getFilename()` at rule-load time — that API was removed in ESLint 10 and `@eslint/compat`'s `fixupPluginRules` does NOT shim it. Don't try to bump eslint past 9 until eslint-plugin-react ships a v10-compatible release. Import rules come from `eslint-plugin-import-x` (the maintained fork of `eslint-plugin-import`) but are registered under both `import-x/` and `import/` prefixes via `plugins: { import: importXPlugin, ... }` so existing `// eslint-disable-next-line import/...` directives keep working. `linterOptions.reportUnusedDisableDirectives` is set to `"warn"` (not "off") so stale airbnb-era disable comments still surface in lint output without failing CI. The TS-overrides block has an `ignores: ["src/hooks/query/query-keys.ts"]` so the `no-restricted-syntax` rule banning raw `["settings", ...]` query keys doesn't fire on the file that defines the helpers themselves. No `.npmrc` / `legacy-peer-deps` flag is needed — all our plugins declare ESLint 9 peer compatibility.
|
||||||
|
|
||||||
|
- **Centralized config**: `config/defaults.json` is the single source of truth for version pins (agent-server, automation, automation SDK), port defaults, persistence paths, package names, and the dev secret key. All consumers read from this file:
|
||||||
|
- JS scripts (`dev-safe.mjs`, `dev-with-automation.mjs`, `check-sdk-version-sync.mjs`) read it via `JSON.parse(readFileSync(...))`.
|
||||||
|
- Docker: a `config-gen` build stage converts the JSON to `/opt/agent-canvas/defaults.env` (shell-sourceable); `entrypoint.sh` sources it at startup.
|
||||||
|
- CI workflow: a `Read defaults from config/defaults.json` step uses `node -p` to extract values into `$GITHUB_OUTPUT`.
|
||||||
|
- Dockerfile ARG defaults are kept as fallbacks for local `docker build` without the CI workflow; CI always passes `--build-arg` overrides from the JSON.
|
||||||
|
- To bump a version, edit `config/defaults.json` only — the JS scripts, Docker build, and CI workflow all derive their values from it.
|
||||||
|
- Docker all-in-one image: `.github/workflows/docker.yml` builds and publishes `ghcr.io/openhands/agent-canvas` — a combined image that bundles the agent-server (from `ghcr.io/openhands/agent-server`), the automation server (`openhands-automation` via pip), and the agent-canvas frontend (static build). The Dockerfile lives at `docker/Dockerfile`, the entrypoint at `docker/entrypoint.sh`. The workflow structure mirrors the SDK repo's `server.yml`: a `build-and-push-image` matrix job (2 × arch: amd64 on `ubuntu-24.04`, arm64 on `ubuntu-24.04-arm`) pushes arch-suffixed tags, then `merge-manifests` creates multi-arch manifests via `docker buildx imagetools create`, then `consolidate-build-info` aggregates artifacts, and `update-pr-description` updates the PR body (using `<!-- AGENT_CANVAS_DOCKER_START -->` / `<!-- AGENT_CANVAS_DOCKER_END -->` markers). The workflow triggers on push to main, `v*` tags (releases), PRs, and `workflow_dispatch`. On release tags it also pushes semver tags (e.g. `1.2.3`, `1.2`, `1`, `latest`). Fork PRs are skipped (no GHCR auth). The image exposes port 8000 as a unified entry point: `/api/automation/*` → automation (:18001), `/api/*` → agent-server (:18000), `/*` → static frontend. The entrypoint auto-generates **both** the session API key and `OH_SECRET_KEY` (persisted to `~/.openhands/agent-canvas/session-api-key.txt` and `secret-key.txt` respectively) when none is provided, so the image runs secure by default. Users can override either via env var (`OH_SECRET_KEY`, `SESSION_API_KEY` / `OH_SESSION_API_KEYS_0`). Unlike `scripts/dev-safe.mjs` (which uses a static default secret key for local dev convenience), the Docker entrypoint never falls back to a known default.
|
||||||
|
|
||||||
- Cloud conversation resume gating: when a cloud conversation is closed from the UI (`pauseCloudSandbox` is called), the conversation's `conversation_url` is NOT cleared -- it still points to the old sandbox host. `WebSocketProviderWrapper` must suppress the URL (pass `null` to `ConversationWebSocketProvider`) while `sandbox_status === "PAUSED"`, otherwise the WebSocket immediately tries the stale URL before the sandbox wakes. Symmetrically, `useActiveConversation`'s refetch interval must fast-poll (3 s) on both `!conversation_url` AND `sandbox_status === "PAUSED"` -- checking only the missing URL would leave the hook on the 30 s interval while the sandbox is resuming. The resume sequence: navigate -> sandbox PAUSED detected -> `resumeCloudSandbox` called (in `conversation.tsx`) -> fast-poll detects RUNNING -> `conversationUrl` unblocked -> WebSocket connects.
|
- Cloud conversation resume gating: when a cloud conversation is closed from the UI (`pauseCloudSandbox` is called), the conversation's `conversation_url` is NOT cleared -- it still points to the old sandbox host. `WebSocketProviderWrapper` must suppress the URL (pass `null` to `ConversationWebSocketProvider`) while `sandbox_status === "PAUSED"`, otherwise the WebSocket immediately tries the stale URL before the sandbox wakes. Symmetrically, `useActiveConversation`'s refetch interval must fast-poll (3 s) on both `!conversation_url` AND `sandbox_status === "PAUSED"` -- checking only the missing URL would leave the hook on the 30 s interval while the sandbox is resuming. The resume sequence: navigate -> sandbox PAUSED detected -> `resumeCloudSandbox` called (in `conversation.tsx`) -> fast-poll detects RUNNING -> `conversationUrl` unblocked -> WebSocket connects.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Single source of truth for version pins, ports, paths, and defaults shared across the npm and Docker install paths. Read by scripts/dev-safe.mjs, scripts/dev-with-automation.mjs, docker/entrypoint.sh (via generated defaults.env), and .github/workflows/docker.yml.",
|
||||||
|
|
||||||
|
"versions": {
|
||||||
|
"agentServer": "1.22.1",
|
||||||
|
"automation": "1.0.0a3",
|
||||||
|
"automationSdk": "1.22.1"
|
||||||
|
},
|
||||||
|
|
||||||
|
"images": {
|
||||||
|
"agentServer": "ghcr.io/openhands/agent-server",
|
||||||
|
"agentCanvas": "ghcr.io/openhands/agent-canvas"
|
||||||
|
},
|
||||||
|
|
||||||
|
"ports": {
|
||||||
|
"agentServer": 18000,
|
||||||
|
"automation": 18001,
|
||||||
|
"proxy": 8000
|
||||||
|
},
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"stateSubdir": "agent-canvas",
|
||||||
|
"conversations": "agent-canvas/conversations",
|
||||||
|
"bashEvents": "agent-canvas/bash_events",
|
||||||
|
"automationDb": "automation/automations.db"
|
||||||
|
},
|
||||||
|
|
||||||
|
"packages": {
|
||||||
|
"agentServer": "openhands-agent-server",
|
||||||
|
"automation": "openhands-automation",
|
||||||
|
"tools": "openhands-tools",
|
||||||
|
"workspace": "openhands-workspace"
|
||||||
|
},
|
||||||
|
|
||||||
|
"defaults": {
|
||||||
|
"secretKey": "openhands-dev-secret-key-change-in-prod"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# agent-canvas all-in-one Docker image
|
||||||
|
#
|
||||||
|
# Combines three services into a single image:
|
||||||
|
# 1. Agent Server — from ghcr.io/openhands/agent-server (upstream SDK image)
|
||||||
|
# 2. Automation — installed via pip from openhands-automation
|
||||||
|
# 3. Frontend — agent-canvas static build served by Node.js
|
||||||
|
#
|
||||||
|
# The entrypoint starts all three services and an ingress proxy that unifies
|
||||||
|
# them behind a single port (default 8000):
|
||||||
|
# /api/automation/* → automation backend (:18001)
|
||||||
|
# /api/*, /sockets → agent server (:18000)
|
||||||
|
# /* (default) → static frontend + SPA fallback
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ── Build args ────────────────────────────────────────────────────────────────
|
||||||
|
# No hardcoded defaults — values are derived from config/defaults.json.
|
||||||
|
# CI passes these via --build-arg; for local builds use:
|
||||||
|
# node scripts/docker-build.mjs (recommended, reads JSON for you)
|
||||||
|
# docker build --build-arg AGENT_SERVER_IMAGE=... --build-arg AUTOMATION_VERSION=... -f docker/Dockerfile .
|
||||||
|
ARG AGENT_SERVER_IMAGE
|
||||||
|
ARG AUTOMATION_VERSION
|
||||||
|
|
||||||
|
# ── Stage 1: Build frontend ──────────────────────────────────────────────────
|
||||||
|
FROM node:24-slim AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Cache-friendly: package files first
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy everything needed for the build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the static frontend
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Stage 1b: Generate shell-sourceable defaults from config/defaults.json ──
|
||||||
|
# This avoids needing jq/python at container runtime to parse the JSON.
|
||||||
|
FROM node:24-slim AS config-gen
|
||||||
|
COPY config/defaults.json /tmp/defaults.json
|
||||||
|
RUN node -e " \
|
||||||
|
const c = JSON.parse(require('fs').readFileSync('/tmp/defaults.json','utf-8')); \
|
||||||
|
const lines = [ \
|
||||||
|
'CONFIG_AGENT_SERVER_PORT=' + c.ports.agentServer, \
|
||||||
|
'CONFIG_AUTOMATION_PORT=' + c.ports.automation, \
|
||||||
|
'CONFIG_PROXY_PORT=' + c.ports.proxy, \
|
||||||
|
'CONFIG_STATE_SUBDIR=' + c.paths.stateSubdir, \
|
||||||
|
'CONFIG_CONVERSATIONS=' + c.paths.conversations, \
|
||||||
|
'CONFIG_BASH_EVENTS=' + c.paths.bashEvents, \
|
||||||
|
'CONFIG_AUTOMATION_DB=' + c.paths.automationDb, \
|
||||||
|
]; \
|
||||||
|
require('fs').writeFileSync('/tmp/defaults.env', lines.join('\n') + '\n'); \
|
||||||
|
"
|
||||||
|
|
||||||
|
# ── Stage 2: Combined image ──────────────────────────────────────────────────
|
||||||
|
FROM ${AGENT_SERVER_IMAGE} AS final
|
||||||
|
|
||||||
|
ARG AUTOMATION_VERSION
|
||||||
|
ARG OPENHANDS_BUILD_GIT_SHA=unknown
|
||||||
|
ARG OPENHANDS_BUILD_GIT_REF=unknown
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="agent-canvas"
|
||||||
|
LABEL org.opencontainers.image.description="All-in-one agent-canvas: Agent Server + Automation + Frontend"
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/OpenHands/agent-canvas"
|
||||||
|
LABEL org.opencontainers.image.revision="${OPENHANDS_BUILD_GIT_SHA}"
|
||||||
|
|
||||||
|
ENV AGENT_CANVAS_BUILD_GIT_SHA=${OPENHANDS_BUILD_GIT_SHA}
|
||||||
|
ENV AGENT_CANVAS_BUILD_GIT_REF=${OPENHANDS_BUILD_GIT_REF}
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Install system deps required by automation's transitive dependencies
|
||||||
|
# (asyncpg needs libpq, which the agent-server base image may not include).
|
||||||
|
RUN if command -v apt-get >/dev/null 2>&1; then \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends libpq-dev && \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install automation server via pip.
|
||||||
|
# Shared deps (openhands-sdk, fastapi, uvicorn, pydantic, httpx, …) are
|
||||||
|
# already satisfied by the agent-server base image, so only automation-
|
||||||
|
# specific packages (asyncpg, sqlalchemy, boto3, gcloud, …) are added.
|
||||||
|
RUN uv pip install --system "openhands-automation==${AUTOMATION_VERSION}" 2>/dev/null \
|
||||||
|
|| pip install --no-cache-dir "openhands-automation==${AUTOMATION_VERSION}"
|
||||||
|
|
||||||
|
# Copy the frontend build output.
|
||||||
|
# react-router.config.ts unpacks build/client/ into build/ for non-Vercel builds.
|
||||||
|
COPY --from=frontend-build /build/build /opt/agent-canvas/frontend
|
||||||
|
|
||||||
|
# Copy the static-server script (serves frontend + proxies to backends)
|
||||||
|
COPY scripts/static-server.mjs /opt/agent-canvas/static-server.mjs
|
||||||
|
|
||||||
|
# Copy custom tools (e.g. canvas_ui_tool.py) so the agent-server can import
|
||||||
|
# them via tool_module_qualnames. OH_EXTRA_PYTHON_PATH is set in entrypoint.sh.
|
||||||
|
COPY tools/ /opt/agent-canvas/tools/
|
||||||
|
|
||||||
|
# Copy generated defaults.env (from config/defaults.json via config-gen stage)
|
||||||
|
COPY --from=config-gen /tmp/defaults.env /opt/agent-canvas/defaults.env
|
||||||
|
|
||||||
|
# Copy the entrypoint
|
||||||
|
COPY docker/entrypoint.sh /opt/agent-canvas/entrypoint.sh
|
||||||
|
RUN chmod +x /opt/agent-canvas/entrypoint.sh
|
||||||
|
|
||||||
|
# Pre-create persistence directories with correct ownership so the
|
||||||
|
# openhands user can write to them even when Docker creates anonymous
|
||||||
|
# volumes (which default to root).
|
||||||
|
RUN mkdir -p /home/openhands/.openhands/agent-canvas/conversations \
|
||||||
|
/home/openhands/.openhands/agent-canvas/bash_events \
|
||||||
|
/home/openhands/.openhands/automation \
|
||||||
|
/projects && \
|
||||||
|
chown -R openhands:openhands /home/openhands/.openhands /projects
|
||||||
|
|
||||||
|
USER openhands
|
||||||
|
|
||||||
|
# Persistence volumes:
|
||||||
|
# /home/openhands/.openhands — settings, secrets, conversations, automation DB
|
||||||
|
# /projects — user code the agent can read/edit
|
||||||
|
# Bind-mount these for data to survive container restarts:
|
||||||
|
# docker run -v ~/.openhands:/home/openhands/.openhands -v ~/projects:/projects ...
|
||||||
|
VOLUME ["/home/openhands/.openhands", "/projects"]
|
||||||
|
|
||||||
|
# The entrypoint starts all services and the ingress proxy.
|
||||||
|
# Port 8000 is the unified entry point.
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["tini", "--", "/opt/agent-canvas/entrypoint.sh"]
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# agent-canvas all-in-one entrypoint
|
||||||
|
#
|
||||||
|
# Starts three services:
|
||||||
|
# 1. Agent Server on port $AGENT_SERVER_PORT (default 18000)
|
||||||
|
# 2. Automation on port $AUTOMATION_PORT (default 18001)
|
||||||
|
# 3. Static server on port $PORT (default 8000)
|
||||||
|
# Routes /api/automation/* → automation, /api/* → agent-server,
|
||||||
|
# and serves the frontend static build for everything else.
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# PORT – Unified entry point port (default: 8000)
|
||||||
|
# AGENT_SERVER_PORT – Internal agent-server port (default: 18000)
|
||||||
|
# AUTOMATION_PORT – Internal automation port (default: 18001)
|
||||||
|
# OH_SECRET_KEY – Secret key for settings encryption (auto-generated
|
||||||
|
# and persisted if not provided)
|
||||||
|
# OPENHANDS_AUTOMATION_API_KEY – API key for automation backend auth
|
||||||
|
# Any agent-server or automation env vars are passed through.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
log() { printf '[agent-canvas] %s\n' "$*"; }
|
||||||
|
log_error() { printf '[agent-canvas] ERROR: %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
# ── Load centralized defaults (generated from config/defaults.json at build) ─
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
if [ -f /opt/agent-canvas/defaults.env ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /opt/agent-canvas/defaults.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORT="${PORT:-${CONFIG_PROXY_PORT:-8000}}"
|
||||||
|
AGENT_SERVER_PORT="${AGENT_SERVER_PORT:-${CONFIG_AGENT_SERVER_PORT:-18000}}"
|
||||||
|
AUTOMATION_PORT="${AUTOMATION_PORT:-${CONFIG_AUTOMATION_PORT:-18001}}"
|
||||||
|
|
||||||
|
# Persistence paths — keep settings, conversations, bash history under a
|
||||||
|
# single well-known directory that the VOLUME directive exposes.
|
||||||
|
OPENHANDS_DIR="${HOME}/.openhands"
|
||||||
|
STATE_DIR="${OPENHANDS_DIR}/${CONFIG_STATE_SUBDIR:-agent-canvas}"
|
||||||
|
export OH_PERSISTENCE_DIR="${OH_PERSISTENCE_DIR:-${OPENHANDS_DIR}}"
|
||||||
|
export OH_CONVERSATIONS_PATH="${OH_CONVERSATIONS_PATH:-${OPENHANDS_DIR}/${CONFIG_CONVERSATIONS:-agent-canvas/conversations}}"
|
||||||
|
export OH_BASH_EVENTS_DIR="${OH_BASH_EVENTS_DIR:-${OPENHANDS_DIR}/${CONFIG_BASH_EVENTS:-agent-canvas/bash_events}}"
|
||||||
|
|
||||||
|
# OH_SECRET_KEY is required for settings/secrets encryption. Without it the
|
||||||
|
# agent-server refuses to return encrypted secrets → conversation creation
|
||||||
|
# fails with a 503. Auto-generate and persist (just like the session API key)
|
||||||
|
# so the image never runs with a known default.
|
||||||
|
SECRET_KEY_FILE="${STATE_DIR}/secret-key.txt"
|
||||||
|
if [ -z "${OH_SECRET_KEY:-}" ]; then
|
||||||
|
if [ -f "$SECRET_KEY_FILE" ]; then
|
||||||
|
OH_SECRET_KEY="$(cat "$SECRET_KEY_FILE")"
|
||||||
|
else
|
||||||
|
OH_SECRET_KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||||
|
mkdir -p "$(dirname "$SECRET_KEY_FILE")"
|
||||||
|
printf '%s' "$OH_SECRET_KEY" > "$SECRET_KEY_FILE"
|
||||||
|
chmod 600 "$SECRET_KEY_FILE"
|
||||||
|
log "Generated OH_SECRET_KEY (persisted to $SECRET_KEY_FILE)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export OH_SECRET_KEY
|
||||||
|
|
||||||
|
# Session API key — generate one if not provided so the image doesn't run
|
||||||
|
# wide-open by default. Persisted so restarts reuse the same key.
|
||||||
|
SESSION_KEY_FILE="${STATE_DIR}/session-api-key.txt"
|
||||||
|
if [ -z "${OH_SESSION_API_KEYS_0:-}" ] && [ -z "${SESSION_API_KEY:-}" ]; then
|
||||||
|
if [ -f "$SESSION_KEY_FILE" ]; then
|
||||||
|
SESSION_API_KEY="$(cat "$SESSION_KEY_FILE")"
|
||||||
|
else
|
||||||
|
SESSION_API_KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||||
|
mkdir -p "$(dirname "$SESSION_KEY_FILE")"
|
||||||
|
printf '%s' "$SESSION_API_KEY" > "$SESSION_KEY_FILE"
|
||||||
|
chmod 600 "$SESSION_KEY_FILE"
|
||||||
|
log "Generated session API key (persisted to $SESSION_KEY_FILE)"
|
||||||
|
fi
|
||||||
|
export OH_SESSION_API_KEYS_0="$SESSION_API_KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# AGENT_SERVER_URL — needed by automation sandbox callbacks.
|
||||||
|
export AGENT_SERVER_URL="${AGENT_SERVER_URL:-http://127.0.0.1:${AGENT_SERVER_PORT}}"
|
||||||
|
|
||||||
|
# Make custom tools (e.g. canvas_ui_tool.py) importable by the agent-server
|
||||||
|
# via tool_module_qualnames. Matches what scripts/dev-safe.mjs does with
|
||||||
|
# OH_EXTRA_PYTHON_PATH: config.canvasToolsDir.
|
||||||
|
export OH_EXTRA_PYTHON_PATH="${OH_EXTRA_PYTHON_PATH:-/opt/agent-canvas/tools}"
|
||||||
|
|
||||||
|
# Track child PIDs so we can clean up on exit.
|
||||||
|
PIDS=()
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
log "Shutting down..."
|
||||||
|
for pid in "${PIDS[@]}"; do
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
wait 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT SIGINT SIGTERM
|
||||||
|
|
||||||
|
# ── 1. Start Agent Server ────────────────────────────────────────────────────
|
||||||
|
log "Starting agent-server on port $AGENT_SERVER_PORT..."
|
||||||
|
|
||||||
|
if command -v openhands-agent-server >/dev/null 2>&1; then
|
||||||
|
# Binary build (production image)
|
||||||
|
openhands-agent-server --port "$AGENT_SERVER_PORT" &
|
||||||
|
elif [ -x /agent-server/.venv/bin/python ]; then
|
||||||
|
# Source build (development image)
|
||||||
|
/agent-server/.venv/bin/python -m openhands.agent_server --port "$AGENT_SERVER_PORT" &
|
||||||
|
else
|
||||||
|
log_error "Cannot find agent-server binary or source venv."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PIDS+=($!)
|
||||||
|
|
||||||
|
# ── 2. Start Automation Server ───────────────────────────────────────────────
|
||||||
|
log "Starting automation server on port $AUTOMATION_PORT..."
|
||||||
|
|
||||||
|
# Disable the automation's own frontend — agent-canvas provides the UI.
|
||||||
|
export AUTOMATION_FRONTEND_DIR=""
|
||||||
|
|
||||||
|
# Default to SQLite so the automation server works out of the box without
|
||||||
|
# an external PostgreSQL instance. Users can override AUTOMATION_DB_URL to
|
||||||
|
# point at a real Postgres for production deployments.
|
||||||
|
if [ -z "${AUTOMATION_DB_URL:-}" ]; then
|
||||||
|
AUTOMATION_DB_FILE="${OPENHANDS_DIR}/${CONFIG_AUTOMATION_DB:-automation/automations.db}"
|
||||||
|
mkdir -p "$(dirname "$AUTOMATION_DB_FILE")"
|
||||||
|
export AUTOMATION_DB_URL="sqlite+aiosqlite:///${AUTOMATION_DB_FILE}"
|
||||||
|
log "Using SQLite database: $AUTOMATION_DB_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The automation server uses uvicorn. Set AUTOMATION_PORT via its CLI.
|
||||||
|
if command -v uvicorn >/dev/null 2>&1; then
|
||||||
|
uvicorn openhands.automation.app:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port "$AUTOMATION_PORT" &
|
||||||
|
PIDS+=($!)
|
||||||
|
elif python -c "import openhands.automation" 2>/dev/null; then
|
||||||
|
python -m uvicorn openhands.automation.app:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port "$AUTOMATION_PORT" &
|
||||||
|
PIDS+=($!)
|
||||||
|
else
|
||||||
|
log "WARNING: Automation server not found, skipping."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Wait for backends to be ready ─────────────────────────────────────────
|
||||||
|
wait_for_port() {
|
||||||
|
local port=$1 name=$2 max_wait=${3:-30}
|
||||||
|
local elapsed=0
|
||||||
|
while ! (echo >/dev/tcp/127.0.0.1/"$port") 2>/dev/null; do
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
if [ "$elapsed" -ge "$max_wait" ]; then
|
||||||
|
log "WARNING: $name on port $port did not become ready within ${max_wait}s"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "$name is ready on port $port"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_port "$AGENT_SERVER_PORT" "Agent Server" 60 &
|
||||||
|
WAIT_PID1=$!
|
||||||
|
wait_for_port "$AUTOMATION_PORT" "Automation Server" 60 &
|
||||||
|
WAIT_PID2=$!
|
||||||
|
wait "$WAIT_PID1" "$WAIT_PID2"
|
||||||
|
|
||||||
|
# ── 4. Start static server (frontend + proxy) ────────────────────────────────
|
||||||
|
log "Starting frontend + proxy on port $PORT..."
|
||||||
|
|
||||||
|
node /opt/agent-canvas/static-server.mjs \
|
||||||
|
--port "$PORT" \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--dir /opt/agent-canvas/frontend \
|
||||||
|
--route "/api/automation=http://127.0.0.1:${AUTOMATION_PORT}" \
|
||||||
|
--route "/api=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/server_info=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/sockets=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/alive=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/health=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/ready=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/docs=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/redoc=http://127.0.0.1:${AGENT_SERVER_PORT}" \
|
||||||
|
--route "/openapi.json=http://127.0.0.1:${AGENT_SERVER_PORT}" &
|
||||||
|
PIDS+=($!)
|
||||||
|
|
||||||
|
log "All services started. Unified entry point: http://0.0.0.0:${PORT}/"
|
||||||
|
|
||||||
|
# Wait for any child to exit. If one dies, the trap will clean up the rest.
|
||||||
|
wait -n "${PIDS[@]}" 2>/dev/null
|
||||||
|
EXIT_CODE=$?
|
||||||
|
log_error "A service exited with code $EXIT_CODE"
|
||||||
|
exit "$EXIT_CODE"
|
||||||
+2
-1
@@ -94,7 +94,8 @@
|
|||||||
"typecheck:staged": "react-router typegen && npx tsc --noEmit --skipLibCheck",
|
"typecheck:staged": "react-router typegen && npx tsc --noEmit --skipLibCheck",
|
||||||
"check-translation-completeness": "node scripts/check-translation-completeness.cjs",
|
"check-translation-completeness": "node scripts/check-translation-completeness.cjs",
|
||||||
"build:app": "npm run make-i18n && react-router build",
|
"build:app": "npm run make-i18n && react-router build",
|
||||||
"build:lib": "npm run make-i18n && react-router typegen && cross-env BUILD_LIB=true VITE_APP_ENV=production vite build && tsc -p tsconfig.lib.json"
|
"build:lib": "npm run make-i18n && react-router typegen && cross-env BUILD_LIB=true VITE_APP_ENV=production vite build && tsc -p tsconfig.lib.json",
|
||||||
|
"build:docker": "node scripts/docker-build.mjs"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{ts,tsx,js}": [
|
"src/**/*.{ts,tsx,js}": [
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
* - openhands-agent-server
|
* - openhands-agent-server
|
||||||
*
|
*
|
||||||
* This script checks the RELEASED PyPI version of openhands-automation (as specified
|
* This script checks the RELEASED PyPI version of openhands-automation (as specified
|
||||||
* by DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs), not the main branch.
|
* by versions.automation in config/defaults.json), not the main branch.
|
||||||
* DEFAULT_AUTOMATION_SDK_VERSION records the SDK dependency version for that
|
* versions.automationSdk records the SDK dependency version for that
|
||||||
* released automation package and may intentionally lag DEFAULT_AGENT_SERVER_VERSION.
|
* released automation package and may intentionally lag versions.agentServer.
|
||||||
*
|
*
|
||||||
* This script is run in CI to catch version drift between projects.
|
* This script is run in CI to catch version drift between projects.
|
||||||
*
|
*
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
* node scripts/check-sdk-version-sync.mjs --check-pypi
|
* node scripts/check-sdk-version-sync.mjs --check-pypi
|
||||||
*
|
*
|
||||||
* Environment variables:
|
* Environment variables:
|
||||||
* EXPECTED_SDK_VERSION - Override the expected version (instead of reading from dev-with-automation.mjs)
|
* EXPECTED_SDK_VERSION - Override the expected version (instead of reading from config/defaults.json)
|
||||||
* AUTOMATION_PACKAGE_NAME - Override the automation package name (default: openhands-automation)
|
* AUTOMATION_PACKAGE_NAME - Override the automation package name (default: openhands-automation)
|
||||||
* AUTOMATION_PACKAGE_VERSION - Override the automation package version (instead of reading from dev-with-automation.mjs)
|
* AUTOMATION_PACKAGE_VERSION - Override the automation package version (instead of reading from config/defaults.json)
|
||||||
*
|
*
|
||||||
* Options:
|
* Options:
|
||||||
* --check-pypi Also check the latest SDK version on PyPI
|
* --check-pypi Also check the latest SDK version on PyPI
|
||||||
@@ -56,10 +56,9 @@ SDK Version Sync Check
|
|||||||
Verifies that the released openhands-automation package on PyPI uses the
|
Verifies that the released openhands-automation package on PyPI uses the
|
||||||
SDK version expected for that automation release.
|
SDK version expected for that automation release.
|
||||||
|
|
||||||
The automation version is read from DEFAULT_AUTOMATION_VERSION in
|
The automation version is read from config/defaults.json (versions.automation).
|
||||||
dev-with-automation.mjs (currently used for local development). The expected
|
The expected SDK dependency version is read from versions.automationSdk,
|
||||||
SDK dependency version is read from DEFAULT_AUTOMATION_SDK_VERSION when present,
|
falling back to versions.agentServer for older configs.
|
||||||
falling back to DEFAULT_AGENT_SERVER_VERSION for older configs.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
node scripts/check-sdk-version-sync.mjs [options]
|
node scripts/check-sdk-version-sync.mjs [options]
|
||||||
@@ -69,9 +68,9 @@ Options:
|
|||||||
--help, -h Show this help
|
--help, -h Show this help
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
EXPECTED_SDK_VERSION Override the expected SDK version (instead of reading from dev-with-automation.mjs)
|
EXPECTED_SDK_VERSION Override the expected SDK version (instead of reading from config/defaults.json)
|
||||||
AUTOMATION_PACKAGE_NAME Override the automation package name (default: openhands-automation)
|
AUTOMATION_PACKAGE_NAME Override the automation package name (default: openhands-automation)
|
||||||
AUTOMATION_PACKAGE_VERSION Override the automation package version (instead of reading from dev-with-automation.mjs)
|
AUTOMATION_PACKAGE_VERSION Override the automation package version (instead of reading from config/defaults.json)
|
||||||
|
|
||||||
Triggering from other repos:
|
Triggering from other repos:
|
||||||
The automation repo or SDK repo can trigger this check via GitHub repository_dispatch:
|
The automation repo or SDK repo can trigger this check via GitHub repository_dispatch:
|
||||||
@@ -143,27 +142,31 @@ function sleep(ms) {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Centralized config ──────────────────────────────────────────────────────
|
||||||
|
let SHARED_DEFAULTS;
|
||||||
|
try {
|
||||||
|
SHARED_DEFAULTS = JSON.parse(
|
||||||
|
readFileSync(join(projectRoot, "config", "defaults.json"), "utf-8"),
|
||||||
|
);
|
||||||
|
if (!SHARED_DEFAULTS.versions?.agentServer || !SHARED_DEFAULTS.versions?.automationSdk) {
|
||||||
|
throw new Error("missing required fields: versions.agentServer, versions.automationSdk");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${colors.red}Failed to load config/defaults.json: ${err.message}${colors.reset}`);
|
||||||
|
console.error("Ensure the file exists and contains valid JSON with required fields.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the default agent-server SDK version from dev-safe.mjs.
|
* Read the default agent-server SDK version from config/defaults.json.
|
||||||
*/
|
*/
|
||||||
function getDefaultAgentServerVersion() {
|
function getDefaultAgentServerVersion() {
|
||||||
const devSafePath = join(projectRoot, "scripts", "dev-safe.mjs");
|
return { version: SHARED_DEFAULTS.versions.agentServer, source: "config/defaults.json" };
|
||||||
const content = readFileSync(devSafePath, "utf8");
|
|
||||||
|
|
||||||
const match = content.match(
|
|
||||||
/const DEFAULT_AGENT_SERVER_VERSION = "([^"]+)"/,
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(
|
|
||||||
"Could not find DEFAULT_AGENT_SERVER_VERSION in dev-safe.mjs",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { version: match[1], source: "dev-safe.mjs" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the expected automation SDK dependency version from environment,
|
* Read the expected automation SDK dependency version from environment
|
||||||
* dev-with-automation.mjs, or dev-safe.mjs.
|
* or config/defaults.json.
|
||||||
*/
|
*/
|
||||||
function getExpectedVersion() {
|
function getExpectedVersion() {
|
||||||
// Allow override via environment variable (useful for CI triggers).
|
// Allow override via environment variable (useful for CI triggers).
|
||||||
@@ -172,21 +175,12 @@ function getExpectedVersion() {
|
|||||||
return { version: envVersion.trim(), source: "EXPECTED_SDK_VERSION env var" };
|
return { version: envVersion.trim(), source: "EXPECTED_SDK_VERSION env var" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const devAutomationPath = join(projectRoot, "scripts", "dev-with-automation.mjs");
|
|
||||||
const content = readFileSync(devAutomationPath, "utf8");
|
|
||||||
const match = content.match(
|
|
||||||
/const DEFAULT_AUTOMATION_SDK_VERSION = "([^"]+)"/,
|
|
||||||
);
|
|
||||||
if (match) {
|
|
||||||
return {
|
return {
|
||||||
version: match[1],
|
version: SHARED_DEFAULTS.versions.automationSdk,
|
||||||
source: "DEFAULT_AUTOMATION_SDK_VERSION in dev-with-automation.mjs",
|
source: "config/defaults.json (versions.automationSdk)",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDefaultAgentServerVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the latest version of a package from PyPI
|
* Fetch the latest version of a package from PyPI
|
||||||
*/
|
*/
|
||||||
@@ -205,7 +199,7 @@ async function fetchPyPIVersion(packageName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the automation version from env var or dev-with-automation.mjs
|
* Read the automation version from env var or config/defaults.json
|
||||||
*/
|
*/
|
||||||
function getAutomationVersion() {
|
function getAutomationVersion() {
|
||||||
// Allow override via environment variable
|
// Allow override via environment variable
|
||||||
@@ -214,18 +208,10 @@ function getAutomationVersion() {
|
|||||||
return { version: envVersion.trim(), source: "AUTOMATION_PACKAGE_VERSION env var" };
|
return { version: envVersion.trim(), source: "AUTOMATION_PACKAGE_VERSION env var" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const devAutomationPath = join(projectRoot, "scripts", "dev-with-automation.mjs");
|
return {
|
||||||
const content = readFileSync(devAutomationPath, "utf8");
|
version: SHARED_DEFAULTS.versions.automation,
|
||||||
|
source: "config/defaults.json (versions.automation)",
|
||||||
const match = content.match(
|
};
|
||||||
/const DEFAULT_AUTOMATION_VERSION = "([^"]+)"/,
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(
|
|
||||||
"Could not find DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { version: match[1], source: "dev-with-automation.mjs" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,7 +310,7 @@ async function main() {
|
|||||||
console.log("");
|
console.log("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get expected version from env var or dev-safe.mjs
|
// Get expected version from env var or config/defaults.json
|
||||||
const { version: expectedVersion, source: versionSource } = getExpectedVersion();
|
const { version: expectedVersion, source: versionSource } = getExpectedVersion();
|
||||||
console.log(
|
console.log(
|
||||||
`Expected automation SDK version: ${colors.green}${expectedVersion}${colors.reset} (from ${versionSource})`,
|
`Expected automation SDK version: ${colors.green}${expectedVersion}${colors.reset} (from ${versionSource})`,
|
||||||
@@ -337,7 +323,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get automation version from env var or dev-with-automation.mjs
|
// Get automation version from env var or config/defaults.json
|
||||||
const { version: automationVersion, source: automationSource } = getAutomationVersion();
|
const { version: automationVersion, source: automationSource } = getAutomationVersion();
|
||||||
console.log(
|
console.log(
|
||||||
`Automation package: ${colors.cyan}${AUTOMATION_PACKAGE_NAME}==${automationVersion}${colors.reset} (from ${automationSource})`,
|
`Automation package: ${colors.cyan}${AUTOMATION_PACKAGE_NAME}==${automationVersion}${colors.reset} (from ${automationSource})`,
|
||||||
@@ -427,13 +413,13 @@ async function main() {
|
|||||||
console.log("");
|
console.log("");
|
||||||
console.log("To fix, update one of the following:");
|
console.log("To fix, update one of the following:");
|
||||||
console.log(
|
console.log(
|
||||||
` 1. Update DEFAULT_AUTOMATION_SDK_VERSION in scripts/dev-with-automation.mjs to match the automation release`,
|
` 1. Update versions.automationSdk in config/defaults.json to match the automation release`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` 2. Release a new version of ${AUTOMATION_PACKAGE_NAME} with SDK dependencies pinned to ${expectedVersion}`,
|
` 2. Release a new version of ${AUTOMATION_PACKAGE_NAME} with SDK dependencies pinned to ${expectedVersion}`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` 3. Update DEFAULT_AUTOMATION_VERSION in scripts/dev-with-automation.mjs to a newer release`,
|
` 3. Update versions.automation in config/defaults.json to a newer release`,
|
||||||
);
|
);
|
||||||
console.log("");
|
console.log("");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
+10
-8
@@ -22,10 +22,16 @@ import {
|
|||||||
signalProcessTree,
|
signalProcessTree,
|
||||||
} from "./dev-process-utils.mjs";
|
} from "./dev-process-utils.mjs";
|
||||||
|
|
||||||
const DEFAULT_BACKEND_PORT = 18000;
|
// ── Centralized config (single source of truth for versions, ports, etc.) ───
|
||||||
|
const __dev_safe_dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const SHARED_DEFAULTS = JSON.parse(
|
||||||
|
readFileSync(path.join(__dev_safe_dirname, "..", "config", "defaults.json"), "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const DEFAULT_BACKEND_PORT = SHARED_DEFAULTS.ports.agentServer;
|
||||||
const DEFAULT_VITE_PORT = 3001;
|
const DEFAULT_VITE_PORT = 3001;
|
||||||
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
||||||
const DEFAULT_AGENT_SERVER_PACKAGE = "openhands-agent-server";
|
const DEFAULT_AGENT_SERVER_PACKAGE = SHARED_DEFAULTS.packages.agentServer;
|
||||||
const AGENT_SERVER_GIT_REPO = "https://github.com/OpenHands/software-agent-sdk";
|
const AGENT_SERVER_GIT_REPO = "https://github.com/OpenHands/software-agent-sdk";
|
||||||
const LOCAL_AGENT_SERVER_SUBDIRS = [
|
const LOCAL_AGENT_SERVER_SUBDIRS = [
|
||||||
"openhands-agent-server",
|
"openhands-agent-server",
|
||||||
@@ -33,12 +39,8 @@ const LOCAL_AGENT_SERVER_SUBDIRS = [
|
|||||||
"openhands-tools",
|
"openhands-tools",
|
||||||
"openhands-workspace",
|
"openhands-workspace",
|
||||||
];
|
];
|
||||||
// Default secret key for local development (DO NOT use in production)
|
const DEFAULT_SECRET_KEY = SHARED_DEFAULTS.defaults.secretKey;
|
||||||
// This is kept static because it's used for encrypting/decrypting persisted settings
|
const DEFAULT_AGENT_SERVER_VERSION = SHARED_DEFAULTS.versions.agentServer;
|
||||||
const DEFAULT_SECRET_KEY = "openhands-dev-secret-key-change-in-prod";
|
|
||||||
// Default agent-server version (released PyPI version)
|
|
||||||
// Set OH_AGENT_SERVER_GIT_REF to use a git branch/SHA instead
|
|
||||||
const DEFAULT_AGENT_SERVER_VERSION = "1.22.1";
|
|
||||||
const FRONTEND_REQUIRED_BINS = ["cross-env", "react-router"];
|
const FRONTEND_REQUIRED_BINS = ["cross-env", "react-router"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, spawnSync } from "node:child_process";
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
import { mkdirSync, existsSync } from "node:fs";
|
import { mkdirSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { join, resolve, dirname } from "node:path";
|
import { join, resolve, dirname } from "node:path";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
@@ -67,16 +67,19 @@ import {
|
|||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const projectRoot = resolve(__dirname, "..");
|
const projectRoot = resolve(__dirname, "..");
|
||||||
|
|
||||||
|
// ── Centralized config (single source of truth for versions, ports, etc.) ───
|
||||||
|
const SHARED_DEFAULTS = JSON.parse(
|
||||||
|
readFileSync(join(projectRoot, "config", "defaults.json"), "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
const DEFAULT_AUTOMATION_REPO = "https://github.com/OpenHands/automation";
|
const DEFAULT_AUTOMATION_REPO = "https://github.com/OpenHands/automation";
|
||||||
const DEFAULT_AUTOMATION_PACKAGE = "openhands-automation";
|
const DEFAULT_AUTOMATION_PACKAGE = SHARED_DEFAULTS.packages.automation;
|
||||||
// Default automation version (released PyPI version)
|
const DEFAULT_AUTOMATION_VERSION = SHARED_DEFAULTS.versions.automation;
|
||||||
// Set OH_AUTOMATION_GIT_REF to use a git branch/SHA instead
|
|
||||||
const DEFAULT_AUTOMATION_VERSION = "1.0.0a3";
|
|
||||||
// SDK version used by DEFAULT_AUTOMATION_VERSION. This can intentionally lag
|
// SDK version used by DEFAULT_AUTOMATION_VERSION. This can intentionally lag
|
||||||
// DEFAULT_AGENT_SERVER_VERSION while automation releases catch up.
|
// the agent-server version while automation releases catch up.
|
||||||
const DEFAULT_AUTOMATION_SDK_VERSION = "1.22.1";
|
const DEFAULT_AUTOMATION_SDK_VERSION = SHARED_DEFAULTS.versions.automationSdk;
|
||||||
const DEFAULT_BACKEND_PORT = 18000;
|
const DEFAULT_BACKEND_PORT = SHARED_DEFAULTS.ports.agentServer;
|
||||||
const DEFAULT_AUTOMATION_PORT = 18001;
|
const DEFAULT_AUTOMATION_PORT = SHARED_DEFAULTS.ports.automation;
|
||||||
// Where the auto-generated default automation API key is persisted. Static
|
// Where the auto-generated default automation API key is persisted. Static
|
||||||
// frontend builds bake VITE_AUTOMATION_API_KEY at build time, so the default
|
// frontend builds bake VITE_AUTOMATION_API_KEY at build time, so the default
|
||||||
// must remain stable across restarts and --skip-build reuse.
|
// must remain stable across restarts and --skip-build reuse.
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Local Docker build helper.
|
||||||
|
*
|
||||||
|
* Reads version pins from config/defaults.json and invokes `docker build`
|
||||||
|
* with the correct --build-arg values so developers never need to remember
|
||||||
|
* (or hardcode) version strings.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/docker-build.mjs # defaults
|
||||||
|
* node scripts/docker-build.mjs --tag my-tag # custom tag
|
||||||
|
* node scripts/docker-build.mjs -- --no-cache # extra docker args
|
||||||
|
*/
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = join(__dirname, "..");
|
||||||
|
|
||||||
|
const config = JSON.parse(
|
||||||
|
readFileSync(join(projectRoot, "config", "defaults.json"), "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const agentServerImage = `${config.images.agentServer}:${config.versions.agentServer}-python`;
|
||||||
|
const automationVersion = config.versions.automation;
|
||||||
|
|
||||||
|
// Parse CLI: --tag <name> and everything after -- is passed to docker build
|
||||||
|
let tag = "agent-canvas:local";
|
||||||
|
const extraArgs = [];
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === "--tag" && i + 1 < args.length) {
|
||||||
|
tag = args[++i];
|
||||||
|
} else if (args[i] === "--") {
|
||||||
|
extraArgs.push(...args.slice(i + 1));
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
extraArgs.push(args[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = [
|
||||||
|
"docker",
|
||||||
|
"build",
|
||||||
|
"-f",
|
||||||
|
"docker/Dockerfile",
|
||||||
|
"--build-arg",
|
||||||
|
`AGENT_SERVER_IMAGE=${agentServerImage}`,
|
||||||
|
"--build-arg",
|
||||||
|
`AUTOMATION_VERSION=${automationVersion}`,
|
||||||
|
"-t",
|
||||||
|
tag,
|
||||||
|
...extraArgs,
|
||||||
|
".",
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`Agent Server image : ${agentServerImage}`);
|
||||||
|
console.log(`Automation version : ${automationVersion}`);
|
||||||
|
console.log(`Tag : ${tag}`);
|
||||||
|
console.log(`\n$ ${cmd.join(" ")}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execFileSync(cmd[0], cmd.slice(1), {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
process.exit(err.status || 1);
|
||||||
|
}
|
||||||
@@ -78,10 +78,8 @@ const conversations: MockConversation[] = [
|
|||||||
{
|
{
|
||||||
id: PAGINATION_LOCAL_CONVERSATION_ID,
|
id: PAGINATION_LOCAL_CONVERSATION_ID,
|
||||||
title: "Local pagination fixture",
|
title: "Local pagination fixture",
|
||||||
created_at: new Date(PAGINATION_BASE_TIME).toISOString(),
|
created_at: new Date(now - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
updated_at: new Date(
|
updated_at: new Date(now - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
PAGINATION_BASE_TIME + PAGINATION_EVENT_COUNT * 60_000,
|
|
||||||
).toISOString(),
|
|
||||||
execution_status: "idle",
|
execution_status: "idle",
|
||||||
workspace: { working_dir: "/workspace/project" },
|
workspace: { working_dir: "/workspace/project" },
|
||||||
},
|
},
|
||||||
@@ -160,10 +158,8 @@ async function maybeReturnPaginationEvents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createCloudPaginationConversation(): AppConversation {
|
function createCloudPaginationConversation(): AppConversation {
|
||||||
const createdAt = new Date(PAGINATION_BASE_TIME).toISOString();
|
const createdAt = new Date(now - 6 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
const updatedAt = new Date(
|
const updatedAt = createdAt;
|
||||||
PAGINATION_BASE_TIME + PAGINATION_EVENT_COUNT * 60_000,
|
|
||||||
).toISOString();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: PAGINATION_CLOUD_CONVERSATION_ID,
|
id: PAGINATION_CLOUD_CONVERSATION_ID,
|
||||||
|
|||||||
Reference in New Issue
Block a user