# 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-4vcpu-ubuntu-2204 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" } ]') else json=$(jq -n -c '[ { image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }, { 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 permissions: contents: read packages: write outputs: # Since this job uses outputs it cannot use matrix hash_from_app_image: ${{ steps.get_hash_in_app_image.outputs.hash_from_app_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: 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 - name: Build app image if: "github.event.pull_request.head.repo.fork" run: | ./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --load - name: Get hash in App Image id: get_hash_in_app_image run: | # Run the build script in the app image docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${{ env.REPO_OWNER }}/openhands:${{ env.RELEVANT_SHA }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt # Get the hash from the build script hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1) echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT echo "Hash from app image: $hash_from_app_image" # Builds the runtime Docker images ghcr_build_runtime: name: Build Image runs-on: blacksmith-4vcpu-ubuntu-2204 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: Set up Python uses: useblacksmith/setup-python@v6 with: python-version: '3.12' - name: Cache Poetry dependencies uses: useblacksmith/cache@v5 with: path: | ~/.cache/pypoetry ~/.cache/ms-playwright ~/.virtualenvs key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-poetry- - name: Install poetry via pipx run: pipx install poetry - name: Install Python dependencies using Poetry run: make install-python-dependencies - name: Create source distribution and Dockerfile run: poetry run python3 openhands/runtime/utils/runtime_build.py --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: Build and push runtime image ${{ matrix.base_image.image }} if: github.event.pull_request.head.repo.fork != true run: | ./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} --push -t ${{ matrix.base_image.tag }} # Forked repos can't push to GHCR, so we need to upload the image as an artifact - name: Build runtime image ${{ matrix.base_image.image }} for fork if: github.event.pull_request.head.repo.fork uses: docker/build-push-action@v6 with: tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }} outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar context: containers/runtime - name: Upload runtime image for fork if: github.event.pull_request.head.repo.fork uses: actions/upload-artifact@v4 with: name: runtime-${{ matrix.base_image.tag }} path: /tmp/runtime-${{ matrix.base_image.tag }}.tar verify_hash_equivalence_in_runtime_and_app: name: Verify Hash Equivalence in Runtime and Docker images runs-on: blacksmith-4vcpu-ubuntu-2204 needs: [ghcr_build_runtime, ghcr_build_app] strategy: fail-fast: false matrix: base_image: ['nikolaik'] env: BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22 steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Cache Poetry dependencies uses: useblacksmith/cache@v5 with: path: | ~/.cache/pypoetry ~/.cache/ms-playwright ~/.virtualenvs key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-poetry- - name: Set up Python uses: useblacksmith/setup-python@v6 with: python-version: '3.12' - name: Install poetry via pipx run: pipx install poetry - name: Install Python dependencies using Poetry run: make install-python-dependencies - name: Get hash in App Image run: | echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" echo "hash_from_app_image=${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" >> $GITHUB_ENV - name: Get hash using code (development mode) run: | mkdir -p containers/runtime poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild > output.txt 2>&1 hash_from_code=$(cat output.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1) echo "hash_from_code=$hash_from_code" >> $GITHUB_ENV - name: Compare hashes run: | echo "Hash from App Image: ${{ env.hash_from_app_image }}" echo "Hash from Code: ${{ env.hash_from_code }}" if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then echo "Hashes match!" else echo "Hashes do not match!" exit 1 fi # 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-4vcpu-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 # Forked repos can't push to GHCR, so we need to download the image as an artifact - name: Download runtime image for fork if: github.event.pull_request.head.repo.fork uses: actions/download-artifact@v4 with: name: runtime-${{ matrix.base_image.tag }} path: /tmp - name: Load runtime image for fork if: github.event.pull_request.head.repo.fork run: | docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar - name: Cache Poetry dependencies uses: useblacksmith/cache@v5 with: path: | ~/.cache/pypoetry ~/.cache/ms-playwright ~/.virtualenvs key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-poetry- - name: Set up Python uses: useblacksmith/setup-python@v6 with: python-version: '3.12' - name: Install poetry via pipx run: pipx install poetry - name: Install Python dependencies using Poetry run: make install-python-dependencies - name: Lowercase Repository Owner run: | echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - name: Run docker runtime tests 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 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # Run unit tests with the Docker runtime Docker images as openhands user test_runtime_oh: name: RT Unit Tests (openhands) runs-on: blacksmith-4vcpu-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 # Forked repos can't push to GHCR, so we need to download the image as an artifact - name: Download runtime image for fork if: github.event.pull_request.head.repo.fork uses: actions/download-artifact@v4 with: name: runtime-${{ matrix.base_image.tag }} path: /tmp - name: Load runtime image for fork if: github.event.pull_request.head.repo.fork run: | docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar - name: Cache Poetry dependencies uses: useblacksmith/cache@v5 with: path: | ~/.cache/pypoetry ~/.virtualenvs key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-poetry- - name: Set up Python uses: useblacksmith/setup-python@v6 with: python-version: '3.12' - name: Install poetry via pipx run: pipx install poetry - name: Install Python dependencies using Poetry run: make install-python-dependencies - name: Lowercase Repository Owner run: | echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - name: Run runtime tests 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 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # 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, verify_hash_equivalence_in_runtime_and_app] 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, verify_hash_equivalence_in_runtime_and_app] 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 }} run: | echo "updating PR description" DOCKER_RUN_COMMAND="docker run -it --rm \ -p 3000:3000 \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \ --name openhands-app-$SHORT_SHA \ docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA" PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body) if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then UPDATED_PR_BODY=$(echo "${PR_BODY}" | sed -E "s|docker run -it --rm.*|$DOCKER_RUN_COMMAND|") else UPDATED_PR_BODY="${PR_BODY} --- To run this PR locally, use the following command: \`\`\` $DOCKER_RUN_COMMAND \`\`\`" fi echo "updated body: $UPDATED_PR_BODY" gh pr edit $PR_NUMBER --body "$UPDATED_PR_BODY"