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:
Rohit Malhotra
2026-05-20 04:24:45 +00:00
committed by GitHub
co-authored by openhands
parent 1a6e629fda
commit 979e64fe19
12 changed files with 1070 additions and 84 deletions
+519
View File
@@ -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 }}