Serve bundled agent-canvas frontend from agent-server #13961
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Agent Server | |
| on: | |
| push: | |
| branches: [main] | |
| tags: | |
| - '*' # Trigger on any tag (e.g., 1.0.0, 1.0.0a5, build-docker) | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| base_image: | |
| description: Base runtime image | |
| type: string | |
| default: nikolaik/python-nodejs:python3.13-nodejs22-slim | |
| image: | |
| description: GHCR image name | |
| type: string | |
| default: ghcr.io/openhands/agent-server | |
| platforms: | |
| description: Target platforms | |
| type: string | |
| default: linux/amd64,linux/arm64 | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| build-binary-and-test: | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| version: latest | |
| python-version: '3.13' | |
| - name: Install dependencies | |
| run: uv sync --dev | |
| - name: Build binary (Unix) | |
| if: runner.os != 'Windows' | |
| run: make build-server | |
| # Windows runners have no `make`; invoke PyInstaller directly. | |
| - name: Build binary (Windows) | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| run: uv run pyinstaller openhands-agent-server/openhands/agent_server/agent-server.spec | |
| - name: Test binary | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${RUNNER_OS:-}" == "Windows" ]]; then | |
| BIN=./dist/openhands-agent-server.exe | |
| else | |
| BIN=./dist/openhands-agent-server | |
| fi | |
| "$BIN" --help | |
| echo "Testing server startup and template loading..." | |
| "$BIN" --port 8002 > server_test.log 2>&1 & | |
| SERVER_PID=$! | |
| sleep 5 | |
| if grep -q "system_prompt.j2.*not found" server_test.log; then | |
| echo "ERROR: Template files not found in binary!" | |
| cat server_test.log | |
| kill "$SERVER_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| if ! kill -0 "$SERVER_PID" 2>/dev/null; then | |
| echo "ERROR: Server failed to start!" | |
| cat server_test.log | |
| exit 1 | |
| fi | |
| if command -v curl >/dev/null 2>&1; then | |
| echo "Testing basic API endpoint..." | |
| if curl -f -s http://localhost:8002/health >/dev/null 2>&1; then | |
| echo "✓ Health endpoint accessible" | |
| else | |
| echo "⚠ Health endpoint not accessible (may be expected)" | |
| fi | |
| fi | |
| kill "$SERVER_PID" 2>/dev/null || true | |
| wait "$SERVER_PID" 2>/dev/null || true | |
| rm -f server_test.log | |
| echo "✓ Binary test completed successfully" | |
| - name: Test --extra-python-path custom tool import | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${RUNNER_OS:-}" == "Windows" ]]; then | |
| BIN=./dist/openhands-agent-server.exe | |
| else | |
| BIN=./dist/openhands-agent-server | |
| fi | |
| wait_for_log() { | |
| local log_file=$1 | |
| local pattern=$2 | |
| local timeout_seconds=${3:-45} | |
| for _ in $(seq 1 "$timeout_seconds"); do | |
| if grep -q "$pattern" "$log_file"; then | |
| return 0 | |
| fi | |
| sleep 1 | |
| done | |
| return 1 | |
| } | |
| stop_process() { | |
| local pid=$1 | |
| kill "$pid" 2>/dev/null || true | |
| wait "$pid" 2>/dev/null || true | |
| } | |
| # Create a temporary directory with an external tool module | |
| TOOL_DIR=$(mktemp -d) | |
| EXTRA_TOOL_DIR=$TOOL_DIR | |
| if [[ "${RUNNER_OS:-}" == "Windows" ]]; then | |
| EXTRA_TOOL_DIR=$(cygpath -w "$TOOL_DIR") | |
| fi | |
| cat > "$TOOL_DIR/ci_test_tool.py" << 'TOOL_EOF' | |
| """CI smoke-test tool: NOT bundled in the binary. | |
| Importing this module proves that --extra-python-path / | |
| OH_EXTRA_PYTHON_PATH correctly extends sys.path at runtime | |
| so external .py files are reachable from a frozen build. | |
| """ | |
| CI_TOOL_LOADED = True | |
| TOOL_EOF | |
| echo "=== Negative test: import WITHOUT extra path (should fail) ===" | |
| "$BIN" --import-modules ci_test_tool --port 8003 \ | |
| > neg_test.log 2>&1 & | |
| NEG_PID=$! | |
| if wait_for_log neg_test.log "No module named 'ci_test_tool'"; then | |
| echo "✓ Negative test passed: import correctly failed without --extra-python-path" | |
| else | |
| echo "ERROR: Expected ModuleNotFoundError but got:" | |
| cat neg_test.log | |
| stop_process "$NEG_PID" | |
| rm -rf "$TOOL_DIR" neg_test.log | |
| exit 1 | |
| fi | |
| stop_process "$NEG_PID" | |
| rm -f neg_test.log | |
| echo "=== Positive test: import WITH OH_EXTRA_PYTHON_PATH ===" | |
| OH_EXTRA_PYTHON_PATH="$EXTRA_TOOL_DIR" \ | |
| "$BIN" --import-modules ci_test_tool --port 8004 \ | |
| > pos_test.log 2>&1 & | |
| POS_PID=$! | |
| if wait_for_log pos_test.log "Imported module: ci_test_tool"; then | |
| echo "✓ Positive test passed: external module imported via OH_EXTRA_PYTHON_PATH" | |
| else | |
| echo "ERROR: Module was not imported. Server log:" | |
| cat pos_test.log | |
| stop_process "$POS_PID" | |
| rm -rf "$TOOL_DIR" pos_test.log | |
| exit 1 | |
| fi | |
| if grep -q "Added to sys.path:" pos_test.log; then | |
| echo "✓ sys.path was extended with the tool directory" | |
| else | |
| echo "ERROR: sys.path was not extended. Server log:" | |
| cat pos_test.log | |
| stop_process "$POS_PID" | |
| rm -rf "$TOOL_DIR" pos_test.log | |
| exit 1 | |
| fi | |
| stop_process "$POS_PID" | |
| echo "=== Positive test: import WITH --extra-python-path CLI flag ===" | |
| "$BIN" --extra-python-path "$EXTRA_TOOL_DIR" \ | |
| --import-modules ci_test_tool --port 8005 \ | |
| > cli_test.log 2>&1 & | |
| CLI_PID=$! | |
| if wait_for_log cli_test.log "Imported module: ci_test_tool"; then | |
| echo "✓ CLI flag test passed: external module imported via --extra-python-path" | |
| else | |
| echo "ERROR: Module was not imported via CLI flag. Server log:" | |
| cat cli_test.log | |
| stop_process "$CLI_PID" | |
| rm -rf "$TOOL_DIR" cli_test.log pos_test.log | |
| exit 1 | |
| fi | |
| stop_process "$CLI_PID" | |
| # Cleanup | |
| rm -rf "$TOOL_DIR" pos_test.log neg_test.log cli_test.log | |
| echo "✓ All --extra-python-path tests passed" | |
| - name: Upload binary artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: openhands-server-${{ matrix.os }} | |
| path: | | |
| dist/openhands-agent-server* | |
| retention-days: 7 | |
| check-openapi-schema: | |
| name: Check OpenAPI Schema | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout PR branch | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| version: latest | |
| python-version: '3.13' | |
| - name: Install Node.js (for npx) | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Install dependencies | |
| run: | | |
| uv sync --frozen --dev | |
| - name: Check OpenAPI JSON and build client | |
| env: | |
| PYTHONPATH: . | |
| run: | | |
| make test-server-schema | |
| build-and-push-image: | |
| name: Build & Push (${{ matrix.variant }}-${{ matrix.arch }}) | |
| # Run on push events, pull requests from the same repository (not forks), and manual workflow_dispatch | |
| # Fork PRs cannot push to GHCR and would fail authentication | |
| 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: | |
| # Explicit matrix: 3 variants × 2 architectures = 6 jobs | |
| # Each job specifies exactly what it builds and where it runs | |
| include: | |
| # Python variant | |
| - variant: python | |
| arch: amd64 | |
| base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim | |
| runner: ubuntu-24.04 | |
| platform: linux/amd64 | |
| - variant: python | |
| arch: arm64 | |
| base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim | |
| runner: ubuntu-24.04-arm | |
| platform: linux/arm64 | |
| # Java variant | |
| - variant: java | |
| arch: amd64 | |
| base_image: eclipse-temurin:17-jdk | |
| runner: ubuntu-24.04 | |
| platform: linux/amd64 | |
| - variant: java | |
| arch: arm64 | |
| base_image: eclipse-temurin:17-jdk | |
| runner: ubuntu-24.04-arm | |
| platform: linux/arm64 | |
| # Golang variant | |
| - variant: golang | |
| arch: amd64 | |
| base_image: golang:1.21-bookworm | |
| runner: ubuntu-24.04 | |
| platform: linux/amd64 | |
| - variant: golang | |
| arch: arm64 | |
| base_image: golang:1.21-bookworm | |
| runner: ubuntu-24.04-arm | |
| platform: linux/arm64 | |
| runs-on: ${{ matrix.runner }} | |
| env: | |
| IMAGE: ${{ inputs.image != '' && inputs.image || 'ghcr.io/openhands/agent-server' }} | |
| BASE_IMAGE: ${{ inputs.base_image != '' && inputs.base_image || matrix.base_image }} | |
| CUSTOM_TAGS: ${{ matrix.variant }} | |
| VARIANT: ${{ matrix.variant }} | |
| ARCH: ${{ matrix.arch }} | |
| TARGET: binary | |
| PLATFORM: ${{ matrix.platform }} | |
| # Use SDK_SHA/SDK_REF so build.py tags PR images with the head commit and branch. | |
| # GITHUB_SHA/GITHUB_REF point at the synthetic merge ref on pull_request events. | |
| SDK_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| SDK_REF: ${{ github.head_ref != '' && format('refs/heads/{0}', github.head_ref) || github.ref }} | |
| GITHUB_REF: ${{ github.ref }} | |
| CI: 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| version: latest | |
| python-version: '3.13' | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Prepare build context and metadata | |
| id: prep | |
| env: | |
| GITHUB_REF: ${{ github.ref }} | |
| run: | | |
| uv sync --frozen | |
| # Generate build context and tags with arch suffix | |
| # build.py now handles architecture tagging internally via --arch flag | |
| # Add --versioned-tag when triggered by a git tag (e.g., v1.0.0) | |
| BUILD_CMD="uv run ./openhands-agent-server/openhands/agent_server/docker/build.py --build-ctx-only --arch ${{ matrix.arch }}" | |
| if [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| BUILD_CMD="$BUILD_CMD --versioned-tag" | |
| fi | |
| eval "$BUILD_CMD" | |
| # Alias tags_csv output to tags for the build action | |
| TAGS=$(grep '^tags_csv=' $GITHUB_OUTPUT | cut -d= -f2-) | |
| echo "tags=$TAGS" >> $GITHUB_OUTPUT | |
| # Extract short SHA for consolidation | |
| # Use SDK_SHA env var (set above to PR head SHA for PRs) | |
| SHORT_SHA=$(echo $SDK_SHA | cut -c1-7) | |
| echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT | |
| # Extract versioned tags CSV for consolidation | |
| VERSIONED_TAGS_CSV=$(grep '^versioned_tags_csv=' $GITHUB_OUTPUT | cut -d= -f2- || echo "") | |
| echo "versioned_tags_csv=$VERSIONED_TAGS_CSV" >> $GITHUB_OUTPUT | |
| # Verify outputs | |
| echo "=== Build outputs ===" | |
| echo "Build context: $(grep '^build_context=' $GITHUB_OUTPUT | cut -d= -f2-)" | |
| echo "Tags: $TAGS" | |
| echo "Short SHA: $SHORT_SHA" | |
| echo "Versioned tags: $VERSIONED_TAGS_CSV" | |
| echo "====================" | |
| - name: Build & Push (${{ matrix.variant }}-${{ matrix.arch }}) | |
| id: build | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 | |
| with: | |
| context: ${{ steps.prep.outputs.build_context }} | |
| file: ${{ steps.prep.outputs.dockerfile }} | |
| target: ${{ env.TARGET }} | |
| platforms: ${{ env.PLATFORM }} | |
| push: true | |
| tags: ${{ steps.prep.outputs.tags }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| BASE_IMAGE=${{ env.BASE_IMAGE }} | |
| OPENHANDS_BUILD_GIT_SHA=${{ env.SDK_SHA }} | |
| OPENHANDS_BUILD_GIT_REF=${{ env.SDK_REF }} | |
| - name: Cleanup build context | |
| if: always() | |
| env: | |
| BUILD_CONTEXT: ${{ steps.prep.outputs.build_context }} | |
| run: | | |
| if [ -n "$BUILD_CONTEXT" ] && [ -d "$BUILD_CONTEXT" ]; then | |
| echo "Cleaning up build context: $BUILD_CONTEXT" | |
| rm -rf "$BUILD_CONTEXT" | |
| fi | |
| - name: Summary (${{ matrix.variant }}-${{ matrix.arch }}) - outputs | |
| env: | |
| SHORT_SHA: ${{ steps.prep.outputs.short_sha }} | |
| TAGS: ${{ steps.prep.outputs.tags }} | |
| BUILD_DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| echo "Image: $IMAGE" | |
| echo "Variant: $VARIANT" | |
| echo "Architecture: $ARCH" | |
| echo "Platform: $PLATFORM" | |
| echo "Short SHA: $SHORT_SHA" | |
| echo "Tags: $TAGS" | |
| echo "Build digest: $BUILD_DIGEST" | |
| - name: Save build info for consolidation | |
| env: | |
| SHORT_SHA: ${{ steps.prep.outputs.short_sha }} | |
| TAGS: ${{ steps.prep.outputs.tags }} | |
| VERSIONED_TAGS_CSV: ${{ steps.prep.outputs.versioned_tags_csv }} | |
| run: | | |
| mkdir -p build-info | |
| jq -n \ | |
| --arg variant "$VARIANT" \ | |
| --arg arch "$ARCH" \ | |
| --arg base_image "$BASE_IMAGE" \ | |
| --arg image "$IMAGE" \ | |
| --arg short_sha "$SHORT_SHA" \ | |
| --arg tags "$TAGS" \ | |
| --arg versioned_tags_csv "$VERSIONED_TAGS_CSV" \ | |
| --arg platform "$PLATFORM" \ | |
| '{ | |
| variant: $variant, | |
| arch: $arch, | |
| base_image: $base_image, | |
| image: $image, | |
| short_sha: $short_sha, | |
| tags: $tags, | |
| versioned_tags_csv: $versioned_tags_csv, | |
| platform: $platform | |
| }' > "build-info/${{ matrix.variant }}-${{ matrix.arch }}.json" | |
| - name: Upload build info artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: build-info-${{ matrix.variant }}-${{ matrix.arch }} | |
| path: build-info/${{ matrix.variant }}-${{ matrix.arch }}.json | |
| retention-days: 1 | |
| 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 | |
| strategy: | |
| matrix: | |
| variant: [python, java, golang] | |
| env: | |
| IMAGE: ${{ inputs.image != '' && inputs.image || 'ghcr.io/openhands/agent-server' }} | |
| steps: | |
| - name: Download build info to extract SHORT_SHA | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: build-info-${{ matrix.variant }}-* | |
| merge-multiple: true | |
| path: build-info | |
| - name: Extract SHORT_SHA from build info | |
| id: get_sha | |
| run: | | |
| # Get SHORT_SHA from any build info artifact for this variant | |
| SHORT_SHA=$(jq -r '.short_sha' build-info/${{ matrix.variant }}-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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create and push multi-arch manifest for ${{ matrix.variant }} | |
| id: create_manifest | |
| env: | |
| GIT_REF: ${{ github.ref }} | |
| SHORT_SHA: ${{ steps.get_sha.outputs.short_sha }} | |
| VARIANT: ${{ matrix.variant }} | |
| run: | | |
| AMD64_TAGS_CSV=$(jq -r '.tags' build-info/${VARIANT}-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 the latest-<variant> alias used by the workspace defaults. | |
| if [ "$GIT_REF" == "refs/heads/main" ]; then | |
| LATEST_TAG="latest-${VARIANT}" | |
| create_manifest "$LATEST_TAG" "main-${VARIANT}" | |
| MANIFEST_TAGS+=("$LATEST_TAG") | |
| fi | |
| MANIFEST_TAG_CSV=$(IFS=,; echo "${MANIFEST_TAGS[*]}") | |
| # Save manifest info for consolidation | |
| mkdir -p manifest-info | |
| jq -n \ | |
| --arg variant "$VARIANT" \ | |
| --arg image "$IMAGE" \ | |
| --arg short_sha "$SHORT_SHA" \ | |
| --arg manifest_tag "$MANIFEST_TAG_CSV" \ | |
| '{ | |
| variant: $variant, | |
| image: $image, | |
| short_sha: $short_sha, | |
| manifest_tag: $manifest_tag | |
| }' > "manifest-info/${VARIANT}.json" | |
| - name: Upload manifest info artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: manifest-info-${{ matrix.variant }} | |
| path: manifest-info/${{ matrix.variant }}.json | |
| retention-days: 1 | |
| consolidate-build-info: | |
| name: Consolidate Build Information | |
| needs: [build-and-push-image, merge-manifests] | |
| # Run if it's a PR and the matrix job completed (even if some variants failed) | |
| 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: | |
| pattern: manifest-info-* | |
| merge-multiple: true | |
| path: manifest-info | |
| - name: Consolidate build information from artifacts | |
| id: consolidate | |
| run: | | |
| echo "Processing build info artifacts..." | |
| ls -la build-info/ | |
| echo "Found $(ls build-info/*.json 2>/dev/null | wc -l) JSON files" | |
| # Initialize variables | |
| IMAGE="" | |
| SHORT_SHA="" | |
| ALL_TAGS="" | |
| # Use associative arrays to track variants (bash 4+) | |
| declare -A VARIANT_BASE_IMAGE | |
| declare -A VARIANT_ARCHS | |
| # Process each build info | |
| for info_file in build-info/*.json; do | |
| if [[ ! -f "$info_file" ]]; then | |
| echo "Skipping $info_file - not a file" | |
| continue | |
| fi | |
| echo "=== Processing $info_file ===" | |
| cat "$info_file" | |
| echo "=== End of $info_file ===" | |
| # Extract information from JSON | |
| VARIANT=$(jq -r '.variant' "$info_file") | |
| ARCH=$(jq -r '.arch' "$info_file") | |
| BASE_IMAGE=$(jq -r '.base_image' "$info_file") | |
| VARIANT_IMAGE=$(jq -r '.image' "$info_file") | |
| VARIANT_SHA=$(jq -r '.short_sha' "$info_file") | |
| VARIANT_TAGS=$(jq -r '.tags' "$info_file") | |
| # Set common values (same across all builds) | |
| if [[ -z "$IMAGE" ]]; then | |
| IMAGE="$VARIANT_IMAGE" | |
| SHORT_SHA="$VARIANT_SHA" | |
| fi | |
| # Store variant information | |
| VARIANT_BASE_IMAGE[$VARIANT]=$BASE_IMAGE | |
| if [[ -z "${VARIANT_ARCHS[$VARIANT]}" ]]; then | |
| VARIANT_ARCHS[$VARIANT]=$ARCH | |
| else | |
| VARIANT_ARCHS[$VARIANT]="${VARIANT_ARCHS[$VARIANT]}, $ARCH" | |
| fi | |
| # Collect tags (comma-separated to newline-separated) | |
| if [[ -n "$VARIANT_TAGS" ]]; then | |
| VARIANT_TAG_LIST=$(echo "$VARIANT_TAGS" | tr ',' '\n') | |
| if [[ -n "$ALL_TAGS" ]]; then | |
| ALL_TAGS="${ALL_TAGS}"$'\n'"${VARIANT_TAG_LIST}" | |
| else | |
| ALL_TAGS="$VARIANT_TAG_LIST" | |
| fi | |
| fi | |
| done | |
| # Build variants JSON array from collected data | |
| VARIANTS_JSON="[]" | |
| for VARIANT in "${!VARIANT_BASE_IMAGE[@]}"; do | |
| BASE_IMG="${VARIANT_BASE_IMAGE[$VARIANT]}" | |
| ARCHS="${VARIANT_ARCHS[$VARIANT]}" | |
| VARIANTS_JSON=$(echo "$VARIANTS_JSON" | jq \ | |
| --arg variant "$VARIANT" \ | |
| --arg base_image "$BASE_IMG" \ | |
| --arg archs "$ARCHS" \ | |
| '. += [{custom_tags: $variant, base_image: $base_image, architectures: $archs}]') | |
| echo "Added variant $VARIANT ($ARCHS), current variants JSON:" | |
| echo "$VARIANTS_JSON" | jq . | |
| done | |
| # Process manifest info artifacts | |
| echo "Processing manifest info artifacts..." | |
| if [[ -d "manifest-info" ]]; then | |
| ls -la manifest-info/ | |
| MANIFEST_TAGS="" | |
| for manifest_file in manifest-info/*.json; do | |
| if [[ -f "$manifest_file" ]]; then | |
| echo "=== Processing $manifest_file ===" | |
| cat "$manifest_file" | |
| MANIFEST_TAG_CSV=$(jq -r '.manifest_tag' "$manifest_file") | |
| # Convert comma-separated tags to newline-separated | |
| MANIFEST_TAG_LIST=$(echo "$MANIFEST_TAG_CSV" | tr ',' '\n' | sed "s|^|${IMAGE}:|") | |
| if [[ -n "$MANIFEST_TAGS" ]]; then | |
| MANIFEST_TAGS="${MANIFEST_TAGS}"$'\n'"${MANIFEST_TAG_LIST}" | |
| else | |
| MANIFEST_TAGS="$MANIFEST_TAG_LIST" | |
| fi | |
| fi | |
| done | |
| # Add manifest tags to ALL_TAGS | |
| if [[ -n "$MANIFEST_TAGS" ]]; then | |
| echo "Adding manifest tags to output" | |
| if [[ -n "$ALL_TAGS" ]]; then | |
| ALL_TAGS="${ALL_TAGS}"$'\n'"${MANIFEST_TAGS}" | |
| else | |
| ALL_TAGS="$MANIFEST_TAGS" | |
| fi | |
| fi | |
| else | |
| echo "No manifest-info directory found (merge-manifests may not have run)" | |
| fi | |
| # Create consolidated build summary | |
| BUILD_SUMMARY=$(jq -n \ | |
| --arg image "$IMAGE" \ | |
| --arg short_sha "$SHORT_SHA" \ | |
| --arg ghcr_url "https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server" \ | |
| --arg all_tags "$ALL_TAGS" \ | |
| --argjson variants "$VARIANTS_JSON" \ | |
| '{ | |
| image: $image, | |
| short_sha: $short_sha, | |
| ghcr_package_url: $ghcr_url, | |
| all_tags: $all_tags, | |
| variants: $variants | |
| }') | |
| echo "Consolidated build summary:" | |
| echo "$BUILD_SUMMARY" | jq . | |
| echo "DEBUG: Final variants count: $(echo "$VARIANTS_JSON" | jq 'length')" | |
| echo "DEBUG: Final variants: $(echo "$VARIANTS_JSON" | jq -c '.')" | |
| # Set output | |
| { | |
| echo 'build_summary<<EOF' | |
| echo "$BUILD_SUMMARY" | |
| echo 'EOF' | |
| } >> $GITHUB_OUTPUT | |
| update-pr-description: | |
| name: Update PR description with agent server image | |
| needs: consolidate-build-info | |
| # Only on PRs, and only if the consolidation succeeded | |
| 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 | |
| env: | |
| BUILD_SUMMARY_JSON: ${{ needs.consolidate-build-info.outputs.build_summary }} | |
| run: | | |
| echo "Event: ${{ github.event_name }}" | |
| echo "PR number: ${{ github.event.number }}" | |
| echo "Run attempt: ${{ github.run_attempt }}" | |
| # Parse the build summary JSON | |
| BUILD_SUMMARY="$BUILD_SUMMARY_JSON" | |
| echo "Build summary received:" | |
| echo "$BUILD_SUMMARY" | jq . | |
| # Extract basic information | |
| 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') | |
| # Build the variants table dynamically | |
| VARIANTS_TABLE="" | |
| # Process each build | |
| VARIANTS=$(echo "$BUILD_SUMMARY" | jq -r '.variants[] | @base64') | |
| echo "DEBUG: Found builds (base64 encoded):" | |
| echo "$VARIANTS" | |
| echo "DEBUG: Number of builds: $(echo "$VARIANTS" | wc -l)" | |
| for variant_data in $VARIANTS; do | |
| # Decode base64 and extract build info | |
| VARIANT_JSON=$(echo "$variant_data" | base64 --decode) | |
| echo "DEBUG: Processing build JSON: $VARIANT_JSON" | |
| CUSTOM_TAGS=$(echo "$VARIANT_JSON" | jq -r '.custom_tags') | |
| BASE_IMAGE=$(echo "$VARIANT_JSON" | jq -r '.base_image') | |
| ARCHS=$(echo "$VARIANT_JSON" | jq -r '.architectures // "amd64, arm64"') | |
| echo "DEBUG: Adding variant $CUSTOM_TAGS with base image $BASE_IMAGE (archs: $ARCHS)" | |
| # Add to variants table with architecture info | |
| VARIANTS_TABLE="${VARIANTS_TABLE}| ${CUSTOM_TAGS} | ${ARCHS} | \`${BASE_IMAGE}\` | [Link](https://hub.docker.com/_/${BASE_IMAGE}) |"$'\n' | |
| done | |
| echo "DEBUG: Final variants table:" | |
| echo "$VARIANTS_TABLE" | |
| # Create the complete PR description with the requested format | |
| PR_CONTENT=$(cat << EOF | |
| <!-- AGENT_SERVER_IMAGES_START --> | |
| --- | |
| **Agent Server images for this PR** | |
| • **GHCR package:** ${GHCR_URL} | |
| **Variants & Base Images** | |
| | Variant | Architectures | Base Image | Docs / Tags | | |
| |---|---|---|---| | |
| ${VARIANTS_TABLE} | |
| **Pull (multi-arch manifest)** | |
| \`\`\`bash | |
| # Each variant is a multi-arch manifest supporting both amd64 and arm64 | |
| docker pull ${IMAGE}:${SHORT_SHA}-python | |
| \`\`\` | |
| **Run** | |
| \`\`\`bash | |
| docker run -it --rm \\ | |
| -p 8000:8000 \\ | |
| --name agent-server-${SHORT_SHA}-python \\ | |
| ${IMAGE}:${SHORT_SHA}-python | |
| \`\`\` | |
| **All tags pushed for this build** | |
| \`\`\` | |
| ${ALL_TAGS} | |
| \`\`\` | |
| **About Multi-Architecture Support** | |
| - Each variant tag (e.g., \`${SHORT_SHA}-python\`) is a **multi-arch manifest** supporting both **amd64** and **arm64** | |
| - Docker automatically pulls the correct architecture for your platform | |
| - Individual architecture tags (e.g., \`${SHORT_SHA}-python-amd64\`) are also available if needed | |
| <!-- AGENT_SERVER_IMAGES_END --> | |
| EOF | |
| ) | |
| # Set output for the next step | |
| { | |
| echo 'pr_content<<EOF' | |
| echo "$PR_CONTENT" | |
| echo 'EOF' | |
| } >> $GITHUB_OUTPUT | |
| - name: Update PR description with docker image details | |
| uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 # v1.2.0 | |
| with: | |
| content: ${{ steps.generate_description.outputs.pr_content }} | |
| regex: <!-- AGENT_SERVER_IMAGES_START -->.*?<!-- AGENT_SERVER_IMAGES_END --> | |
| regexFlags: s | |
| token: ${{ secrets.GITHUB_TOKEN }} |