diff --git a/.github/workflows/ghcr-build.yml b/.github/workflows/ghcr-build.yml index b1d46a897d23..5624a8dd7d56 100644 --- a/.github/workflows/ghcr-build.yml +++ b/.github/workflows/ghcr-build.yml @@ -89,6 +89,66 @@ jobs: echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT echo "Hash from app image: $hash_from_app_image" + # Builds the replay.io runner image + ghcr_build_runner: + name: Build Runner Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + hash_from_runner_image: ${{ steps.get_hash_in_runner_image.outputs.hash_from_runner_image }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: true + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.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: Build and push runner image + if: "!github.event.pull_request.head.repo.fork" + run: | + ./containers/build.sh -i runner -o ${{ github.repository_owner }} --push + - name: Build runner image + if: "github.event.pull_request.head.repo.fork" + run: | + ./containers/build.sh -i runner -o ${{ github.repository_owner }} --load + - name: Get hash in Runner Image + id: get_hash_in_runner_image + run: | + # Lowercase the repository owner + export REPO_OWNER=${{ github.repository_owner }} + REPO_OWNER=$(echo $REPO_OWNER | tr '[:upper:]' '[:lower:]') + # Run the build script in the runner image + docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${REPO_OWNER}/runner:${{ 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_runner_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1) + echo "hash_from_runner_image=$hash_from_runner_image" >> $GITHUB_OUTPUT + echo "Hash from runner image: $hash_from_runner_image" + # Builds the runtime Docker images ghcr_build_runtime: name: Build Image @@ -169,10 +229,10 @@ jobs: 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 + verify_hash_equivalence_in_runtime_and_runner_and_app: + name: Verify Hash Equivalence in App, Runner, and Runtime images runs-on: ubuntu-latest - needs: [ghcr_build_runtime, ghcr_build_app] + needs: [ghcr_build_runtime, ghcr_build_runner, ghcr_build_app] strategy: fail-fast: false matrix: @@ -200,6 +260,10 @@ jobs: 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 in Runner Image + run: | + echo "Hash from runner image: ${{ needs.ghcr_build_runner.outputs.hash_from_runner_image }}" + echo "hash_from_runner_image=${{ needs.ghcr_build_runner.outputs.hash_from_runner_image }}" >> $GITHUB_ENV - name: Get hash using code (development mode) run: | @@ -211,8 +275,9 @@ jobs: - name: Compare hashes run: | echo "Hash from App Image: ${{ env.hash_from_app_image }}" + echo "Hash from Runner Image: ${{ env.hash_from_runner_image }}" echo "Hash from Code: ${{ env.hash_from_code }}" - if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then + if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" -a "${{ env.hash_from_runner_image }}" = "${{ env.hash_from_code }}"]; then echo "Hashes match!" else echo "Hashes do not match!" diff --git a/containers/runner/Dockerfile b/containers/runner/Dockerfile new file mode 100644 index 000000000000..988d7e21557e --- /dev/null +++ b/containers/runner/Dockerfile @@ -0,0 +1,79 @@ +ARG OPENHANDS_BUILD_VERSION=dev + +FROM python:3.12.3-slim AS backend-builder + +WORKDIR /runner +ENV PYTHONPATH='/runner' + +ENV POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_IN_PROJECT=1 \ + POETRY_VIRTUALENVS_CREATE=1 \ + POETRY_CACHE_DIR=/tmp/poetry_cache + +RUN apt-get update -y \ + && apt-get install -y curl make git build-essential \ + && python3 -m pip install poetry==1.8.2 --break-system-packages + +COPY ./pyproject.toml ./poetry.lock ./ +RUN touch README.md +RUN export POETRY_CACHE_DIR && poetry install --without evaluation,llama-index --no-root && rm -rf $POETRY_CACHE_DIR + +FROM python:3.12.3-slim AS openhands-runner + +WORKDIR /runner + +ARG OPENHANDS_BUILD_VERSION #re-declare for this section + +ENV RUN_AS_OPENHANDS=true +# A random number--we need this to be different from the user's UID on the host machine +ENV OPENHANDS_USER_ID=42420 +ENV SANDBOX_LOCAL_RUNTIME_URL=http://host.docker.internal +ENV USE_HOST_NETWORK=false +ENV WORKSPACE_BASE=/opt/workspace_base +ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION +ENV SANDBOX_USER_ID=0 +RUN mkdir -p $WORKSPACE_BASE + +RUN apt-get update -y \ + && apt-get install -y curl ssh sudo + +# Default is 1000, but OSX is often 501 +RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs +# Default is 60000, but we've seen up to 200000 +RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs + +RUN groupadd runner +RUN useradd -l -m -u $OPENHANDS_USER_ID -s /bin/bash openhands && \ + usermod -aG runner openhands && \ + usermod -aG sudo openhands && \ + echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN chown -R openhands:runner /runner && chmod -R 770 /runner +RUN sudo chown -R openhands:runner $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE +USER openhands + +ENV VIRTUAL_ENV=/runner/.venv \ + PATH="/runner/.venv/bin:$PATH" \ + PYTHONPATH='/runner' + +COPY --chown=openhands:runner --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} + +COPY --chown=openhands:runner --chmod=770 ./openhands ./openhands +COPY --chown=openhands:runner --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins +COPY --chown=openhands:runner --chmod=770 ./openhands/agenthub ./openhands/agenthub +COPY --chown=openhands:runner ./pyproject.toml ./pyproject.toml +COPY --chown=openhands:runner ./poetry.lock ./poetry.lock +COPY --chown=openhands:runner ./README.md ./README.md +COPY --chown=openhands:runner ./MANIFEST.in ./MANIFEST.in +COPY --chown=openhands:runner ./LICENSE ./LICENSE + +# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership +RUN python openhands/core/download.py # No-op to download assets +# Add this line to set group ownership of all files/directories not already in "runner" group +# openhands:openhands -> openhands:runner +RUN find /runner \! -group runner -exec chgrp runner {} + + +USER root + +WORKDIR /runner + +CMD [ "sleep", "infinity" ] diff --git a/containers/runner/config.sh b/containers/runner/config.sh new file mode 100644 index 000000000000..d5c3356a551f --- /dev/null +++ b/containers/runner/config.sh @@ -0,0 +1,4 @@ +DOCKER_REGISTRY=ghcr.io +DOCKER_ORG=all-hands-ai +DOCKER_IMAGE=runner +DOCKER_BASE_DIR="."