# 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