mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
425 lines
17 KiB
YAML
425 lines
17 KiB
YAML
# Workflow that builds, tests and then pushes the OpenHands and runtime docker images to the ghcr.io repository
|
|
name: Docker
|
|
|
|
# Always run on "main"
|
|
# Always run on tags
|
|
# Always run on PRs
|
|
# Can also be triggered manually
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
tags:
|
|
- "*"
|
|
pull_request:
|
|
workflow_dispatch:
|
|
inputs:
|
|
reason:
|
|
description: "Reason for manual trigger"
|
|
required: true
|
|
default: ""
|
|
|
|
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
|
|
jobs:
|
|
define-matrix:
|
|
runs-on: blacksmith
|
|
outputs:
|
|
base_image: ${{ steps.define-base-images.outputs.base_image }}
|
|
steps:
|
|
- name: Define base images
|
|
shell: bash
|
|
id: define-base-images
|
|
run: |
|
|
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
|
|
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
|
json=$(jq -n -c '[
|
|
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
|
|
{ image: "ubuntu:24.04", tag: "ubuntu" }
|
|
]')
|
|
else
|
|
json=$(jq -n -c '[
|
|
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
|
|
{ image: "ghcr.io/all-hands-ai/python-nodejs:python3.13-nodejs22-trixie", tag: "trixie" },
|
|
{ image: "ubuntu:24.04", tag: "ubuntu" }
|
|
]')
|
|
fi
|
|
echo "base_image=$json" >> "$GITHUB_OUTPUT"
|
|
|
|
# Builds the OpenHands Docker images
|
|
ghcr_build_app:
|
|
name: Build App Image
|
|
runs-on: blacksmith-4vcpu-ubuntu-2204
|
|
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@v3.6.0
|
|
with:
|
|
image: tonistiigi/binfmt:latest
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
- name: Set up Docker Buildx
|
|
id: buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Lowercase Repository Owner
|
|
run: |
|
|
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
|
- name: Build and push app image
|
|
if: "!github.event.pull_request.head.repo.fork"
|
|
run: |
|
|
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --push
|
|
|
|
# Builds the runtime Docker images
|
|
ghcr_build_runtime:
|
|
name: Build Image
|
|
runs-on: blacksmith-8vcpu-ubuntu-2204
|
|
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
needs: define-matrix
|
|
strategy:
|
|
matrix:
|
|
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@v3.6.0
|
|
with:
|
|
image: tonistiigi/binfmt:latest
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
- name: Set up Docker Buildx
|
|
id: buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Install poetry via pipx
|
|
run: pipx install poetry
|
|
- name: Set up Python
|
|
uses: useblacksmith/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
cache: poetry
|
|
- name: Install Python dependencies using Poetry
|
|
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
|
|
- name: Create source distribution and Dockerfile
|
|
run: poetry run python3 -m openhands.runtime.utils.runtime_build --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
|
|
- name: Lowercase Repository Owner
|
|
run: |
|
|
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
|
- name: Short SHA
|
|
run: |
|
|
echo SHORT_SHA=$(git rev-parse --short "$RELEVANT_SHA") >> $GITHUB_ENV
|
|
- name: Determine docker build params
|
|
if: github.event.pull_request.head.repo.fork != true
|
|
shell: bash
|
|
run: |
|
|
|
|
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} -t ${{ matrix.base_image.tag }} --dry
|
|
|
|
DOCKER_BUILD_JSON=$(jq -c . < docker-build-dry.json)
|
|
echo "DOCKER_TAGS=$(echo "$DOCKER_BUILD_JSON" | jq -r '.tags | join(",")')" >> $GITHUB_ENV
|
|
echo "DOCKER_PLATFORM=$(echo "$DOCKER_BUILD_JSON" | jq -r '.platform')" >> $GITHUB_ENV
|
|
echo "DOCKER_BUILD_ARGS=$(echo "$DOCKER_BUILD_JSON" | jq -r '.build_args | join(",")')" >> $GITHUB_ENV
|
|
- name: Build and push runtime image ${{ matrix.base_image.image }}
|
|
if: github.event.pull_request.head.repo.fork != true
|
|
uses: useblacksmith/build-push-action@v1
|
|
with:
|
|
push: true
|
|
tags: ${{ env.DOCKER_TAGS }}
|
|
platforms: ${{ env.DOCKER_PLATFORM }}
|
|
build-args: ${{ env.DOCKER_BUILD_ARGS }}
|
|
context: containers/runtime
|
|
provenance: false
|
|
# Forked repos can't push to GHCR, so we just build in order to populate the cache for rebuilding
|
|
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
|
if: github.event.pull_request.head.repo.fork
|
|
uses: useblacksmith/build-push-action@v1
|
|
with:
|
|
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
|
context: containers/runtime
|
|
- name: Upload runtime source for fork
|
|
if: github.event.pull_request.head.repo.fork
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: runtime-src-${{ matrix.base_image.tag }}
|
|
path: containers/runtime
|
|
|
|
ghcr_build_enterprise:
|
|
name: Push Enterprise Image
|
|
runs-on: blacksmith-8vcpu-ubuntu-2204
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
needs: [define-matrix, ghcr_build_app]
|
|
# Do not build enterprise in forks
|
|
if: github.event.pull_request.head.repo.fork != true
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
|
|
# Set up Docker Buildx for better performance
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
with:
|
|
driver-opts: network=host
|
|
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract metadata (tags, labels) for Docker
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ghcr.io/all-hands-ai/enterprise-server
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=ref,event=pr
|
|
type=sha
|
|
type=sha,format=long
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=semver,pattern={{major}}
|
|
flavor: |
|
|
latest=auto
|
|
prefix=
|
|
suffix=
|
|
env:
|
|
DOCKER_METADATA_PR_HEAD_SHA: true
|
|
- name: Determine app image tag
|
|
shell: bash
|
|
run: |
|
|
# Duplicated with build.sh
|
|
sanitized_ref_name=$(echo "$GITHUB_REF_NAME" | sed 's/[^a-zA-Z0-9.-]\+/-/g')
|
|
OPENHANDS_BUILD_VERSION=$sanitized_ref_name
|
|
sanitized_ref_name=$(echo "$sanitized_ref_name" | tr '[:upper:]' '[:lower:]') # lower case is required in tagging
|
|
echo "OPENHANDS_DOCKER_TAG=${sanitized_ref_name}" >> $GITHUB_ENV
|
|
- name: Build and push Docker image
|
|
uses: useblacksmith/build-push-action@v1
|
|
with:
|
|
context: .
|
|
file: enterprise/Dockerfile
|
|
push: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
build-args: |
|
|
OPENHANDS_VERSION=${{ env.OPENHANDS_DOCKER_TAG }}
|
|
platforms: linux/amd64
|
|
# Add build provenance
|
|
provenance: true
|
|
# Add build attestations for better security
|
|
sbom: true
|
|
|
|
enterprise-preview:
|
|
name: Enterprise preview
|
|
if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy')
|
|
runs-on: blacksmith-4vcpu-ubuntu-2204
|
|
needs: [ghcr_build_enterprise]
|
|
steps:
|
|
# This should match the version in enterprise-preview.yml
|
|
- name: Trigger remote job
|
|
run: |
|
|
curl --fail-with-body -sS -X POST \
|
|
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-d "{\"ref\": \"main\", \"inputs\": {\"openhandsPrNumber\": \"${{ github.event.pull_request.number }}\", \"deployEnvironment\": \"feature\", \"enterpriseImageTag\": \"pr-${{ github.event.pull_request.number }}\" }}" \
|
|
https://api.github.com/repos/All-Hands-AI/deploy/actions/workflows/deploy.yaml/dispatches
|
|
|
|
# Run unit tests with the Docker runtime Docker images as root
|
|
test_runtime_root:
|
|
name: RT Unit Tests (Root)
|
|
needs: [ghcr_build_runtime, define-matrix]
|
|
runs-on: blacksmith-8vcpu-ubuntu-2204
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Set up Docker Buildx
|
|
id: buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Download runtime source for fork
|
|
if: github.event.pull_request.head.repo.fork
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: runtime-src-${{ matrix.base_image.tag }}
|
|
path: containers/runtime
|
|
- name: Lowercase Repository Owner
|
|
run: |
|
|
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
|
# Forked repos can't push to GHCR, so we need to rebuild using cache
|
|
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
|
if: github.event.pull_request.head.repo.fork
|
|
uses: useblacksmith/build-push-action@v1
|
|
with:
|
|
load: true
|
|
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
|
context: containers/runtime
|
|
- name: Install poetry via pipx
|
|
run: pipx install poetry
|
|
- name: Set up Python
|
|
uses: useblacksmith/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
cache: poetry
|
|
- name: Install Python dependencies using Poetry
|
|
run: make install-python-dependencies INSTALL_PLAYWRIGHT=0
|
|
- name: Run docker runtime tests
|
|
shell: bash
|
|
run: |
|
|
# We install pytest-xdist in order to run tests across CPUs
|
|
poetry run pip install pytest-xdist
|
|
|
|
# Install to be able to retry on failures for flaky tests
|
|
poetry run pip install pytest-rerunfailures
|
|
|
|
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
|
|
|
# Setting RUN_AS_OPENHANDS to false means use root.
|
|
# That should mean SANDBOX_USER_ID is ignored but some tests do not check for RUN_AS_OPENHANDS.
|
|
|
|
TEST_RUNTIME=docker \
|
|
SANDBOX_USER_ID=$(id -u) \
|
|
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
|
TEST_IN_CI=true \
|
|
RUN_AS_OPENHANDS=false \
|
|
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
|
env:
|
|
DEBUG: "1"
|
|
|
|
# Run unit tests with the Docker runtime Docker images as openhands user
|
|
test_runtime_oh:
|
|
name: RT Unit Tests (openhands)
|
|
runs-on: blacksmith-8vcpu-ubuntu-2204
|
|
needs: [ghcr_build_runtime, define-matrix]
|
|
strategy:
|
|
matrix:
|
|
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Set up Docker Buildx
|
|
id: buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Download runtime source for fork
|
|
if: github.event.pull_request.head.repo.fork
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: runtime-src-${{ matrix.base_image.tag }}
|
|
path: containers/runtime
|
|
- name: Lowercase Repository Owner
|
|
run: |
|
|
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
|
# Forked repos can't push to GHCR, so we need to rebuild using cache
|
|
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
|
if: github.event.pull_request.head.repo.fork
|
|
uses: useblacksmith/build-push-action@v1
|
|
with:
|
|
load: true
|
|
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
|
context: containers/runtime
|
|
- name: Install poetry via pipx
|
|
run: pipx install poetry
|
|
- name: Set up Python
|
|
uses: useblacksmith/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
cache: poetry
|
|
- name: Install Python dependencies using Poetry
|
|
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
|
|
- name: Run runtime tests
|
|
shell: bash
|
|
run: |
|
|
# We install pytest-xdist in order to run tests across CPUs
|
|
poetry run pip install pytest-xdist
|
|
|
|
# Install to be able to retry on failures for flaky tests
|
|
poetry run pip install pytest-rerunfailures
|
|
|
|
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
|
|
|
TEST_RUNTIME=docker \
|
|
SANDBOX_USER_ID=$(id -u) \
|
|
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
|
TEST_IN_CI=true \
|
|
RUN_AS_OPENHANDS=true \
|
|
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
|
env:
|
|
DEBUG: "1"
|
|
|
|
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
|
|
# "All Runtime Tests Passed" is a required job for PRs to merge
|
|
# Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
|
|
# prerequisites have been cancelled or failed so merging is disallowed, otherwise Github considers "skipped" as "success"
|
|
runtime_tests_check_success:
|
|
name: All Runtime Tests Passed
|
|
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
|
|
runs-on: blacksmith-4vcpu-ubuntu-2204
|
|
needs: [test_runtime_root, test_runtime_oh]
|
|
steps:
|
|
- name: All tests passed
|
|
run: echo "All runtime tests have passed successfully!"
|
|
|
|
runtime_tests_check_fail:
|
|
name: All Runtime Tests Passed
|
|
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
|
runs-on: blacksmith-4vcpu-ubuntu-2204
|
|
needs: [test_runtime_root, test_runtime_oh]
|
|
steps:
|
|
- name: Some tests failed
|
|
run: |
|
|
echo "Some runtime tests failed or were cancelled"
|
|
exit 1
|
|
update_pr_description:
|
|
name: Update PR Description
|
|
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
|
|
needs: [ghcr_build_runtime]
|
|
runs-on: blacksmith-4vcpu-ubuntu-2204
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Get short SHA
|
|
id: short_sha
|
|
run: echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
|
|
|
- name: Update PR Description
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
REPO: ${{ github.repository }}
|
|
SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
|
|
shell: bash
|
|
run: |
|
|
echo "Updating PR description with Docker and uvx commands"
|
|
bash ${GITHUB_WORKSPACE}/.github/scripts/update_pr_description.sh
|