ghcr-build.yml 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. # Workflow that builds, tests and then pushes the OpenHands and runtime docker images to the ghcr.io repository
  2. name: Docker
  3. # Always run on "main"
  4. # Always run on tags
  5. # Always run on PRs
  6. # Can also be triggered manually
  7. on:
  8. push:
  9. branches:
  10. - main
  11. tags:
  12. - '*'
  13. pull_request:
  14. workflow_dispatch:
  15. inputs:
  16. reason:
  17. description: 'Reason for manual trigger'
  18. required: true
  19. default: ''
  20. # If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
  21. concurrency:
  22. group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
  23. cancel-in-progress: true
  24. env:
  25. BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22
  26. RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
  27. jobs:
  28. # Builds the OpenHands Docker images
  29. ghcr_build_app:
  30. name: Build App Image
  31. runs-on: ubuntu-latest
  32. permissions:
  33. contents: read
  34. packages: write
  35. outputs:
  36. hash_from_app_image: ${{ steps.get_hash_in_app_image.outputs.hash_from_app_image }}
  37. steps:
  38. - name: Checkout
  39. uses: actions/checkout@v4
  40. - name: Free Disk Space (Ubuntu)
  41. uses: jlumbroso/free-disk-space@main
  42. with:
  43. # this might remove tools that are actually needed,
  44. # if set to "true" but frees about 6 GB
  45. tool-cache: true
  46. # all of these default to true, but feel free to set to
  47. # "false" if necessary for your workflow
  48. android: true
  49. dotnet: true
  50. haskell: true
  51. large-packages: true
  52. docker-images: false
  53. swap-storage: true
  54. - name: Set up QEMU
  55. uses: docker/setup-qemu-action@v3.0.0
  56. with:
  57. image: tonistiigi/binfmt:latest
  58. - name: Login to GHCR
  59. uses: docker/login-action@v3
  60. with:
  61. registry: ghcr.io
  62. username: ${{ github.repository_owner }}
  63. password: ${{ secrets.GITHUB_TOKEN }}
  64. - name: Set up Docker Buildx
  65. id: buildx
  66. uses: docker/setup-buildx-action@v3
  67. - name: "Set up docker layer caching"
  68. uses: satackey/action-docker-layer-caching@v0.0.11
  69. continue-on-error: true
  70. - name: Build and push app image
  71. if: "!github.event.pull_request.head.repo.fork"
  72. run: |
  73. ./containers/build.sh -i openhands -o ${{ github.repository_owner }} --push
  74. - name: Build app image
  75. if: "github.event.pull_request.head.repo.fork"
  76. run: |
  77. ./containers/build.sh -i openhands -o ${{ github.repository_owner }} --load
  78. - name: Get hash in App Image
  79. id: get_hash_in_app_image
  80. run: |
  81. # Lowercase the repository owner
  82. export REPO_OWNER=${{ github.repository_owner }}
  83. REPO_OWNER=$(echo $REPO_OWNER | tr '[:upper:]' '[:lower:]')
  84. # Run the build script in the app image
  85. docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${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
  86. # Get the hash from the build script
  87. hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
  88. echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT
  89. echo "Hash from app image: $hash_from_app_image"
  90. # Builds the runtime Docker images
  91. ghcr_build_runtime:
  92. name: Build Image
  93. runs-on: ubuntu-latest
  94. permissions:
  95. contents: read
  96. packages: write
  97. strategy:
  98. matrix:
  99. base_image:
  100. - image: 'nikolaik/python-nodejs:python3.12-nodejs22'
  101. tag: nikolaik
  102. steps:
  103. - name: Checkout
  104. uses: actions/checkout@v4
  105. - name: Free Disk Space (Ubuntu)
  106. uses: jlumbroso/free-disk-space@main
  107. with:
  108. # this might remove tools that are actually needed,
  109. # if set to "true" but frees about 6 GB
  110. tool-cache: true
  111. # all of these default to true, but feel free to set to
  112. # "false" if necessary for your workflow
  113. android: true
  114. dotnet: true
  115. haskell: true
  116. large-packages: true
  117. docker-images: false
  118. swap-storage: true
  119. - name: Set up QEMU
  120. uses: docker/setup-qemu-action@v3.0.0
  121. with:
  122. image: tonistiigi/binfmt:latest
  123. - name: Login to GHCR
  124. uses: docker/login-action@v3
  125. with:
  126. registry: ghcr.io
  127. username: ${{ github.repository_owner }}
  128. password: ${{ secrets.GITHUB_TOKEN }}
  129. - name: Set up Docker Buildx
  130. id: buildx
  131. uses: docker/setup-buildx-action@v3
  132. - name: Set up Python
  133. uses: actions/setup-python@v5
  134. with:
  135. python-version: '3.12'
  136. - name: Cache Poetry dependencies
  137. uses: actions/cache@v4
  138. with:
  139. path: |
  140. ~/.cache/pypoetry
  141. ~/.virtualenvs
  142. key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
  143. restore-keys: |
  144. ${{ runner.os }}-poetry-
  145. - name: Install poetry via pipx
  146. run: pipx install poetry
  147. - name: Install Python dependencies using Poetry
  148. run: make install-python-dependencies
  149. - name: Create source distribution and Dockerfile
  150. run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
  151. - name: Build and push runtime image ${{ matrix.base_image.image }}
  152. if: github.event.pull_request.head.repo.fork != true
  153. run: |
  154. ./containers/build.sh -i runtime -o ${{ github.repository_owner }} --push -t ${{ matrix.base_image.tag }}
  155. # Forked repos can't push to GHCR, so we need to upload the image as an artifact
  156. - name: Build runtime image ${{ matrix.base_image.image }} for fork
  157. if: github.event.pull_request.head.repo.fork
  158. uses: docker/build-push-action@v6
  159. with:
  160. tags: ghcr.io/all-hands-ai/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
  161. outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
  162. context: containers/runtime
  163. - name: Upload runtime image for fork
  164. if: github.event.pull_request.head.repo.fork
  165. uses: actions/upload-artifact@v4
  166. with:
  167. name: runtime-${{ matrix.base_image.tag }}
  168. path: /tmp/runtime-${{ matrix.base_image.tag }}.tar
  169. verify_hash_equivalence_in_runtime_and_app:
  170. name: Verify Hash Equivalence in Runtime and Docker images
  171. runs-on: ubuntu-latest
  172. needs: [ghcr_build_runtime, ghcr_build_app]
  173. strategy:
  174. fail-fast: false
  175. matrix:
  176. base_image: ['nikolaik']
  177. steps:
  178. - uses: actions/checkout@v4
  179. - name: Cache Poetry dependencies
  180. uses: actions/cache@v4
  181. with:
  182. path: |
  183. ~/.cache/pypoetry
  184. ~/.virtualenvs
  185. key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
  186. restore-keys: |
  187. ${{ runner.os }}-poetry-
  188. - name: Set up Python
  189. uses: actions/setup-python@v5
  190. with:
  191. python-version: '3.12'
  192. - name: Install poetry via pipx
  193. run: pipx install poetry
  194. - name: Install Python dependencies using Poetry
  195. run: make install-python-dependencies
  196. - name: Get hash in App Image
  197. run: |
  198. echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
  199. echo "hash_from_app_image=${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" >> $GITHUB_ENV
  200. - name: Get hash using code (development mode)
  201. run: |
  202. mkdir -p containers/runtime
  203. 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
  204. hash_from_code=$(cat output.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
  205. echo "hash_from_code=$hash_from_code" >> $GITHUB_ENV
  206. - name: Compare hashes
  207. run: |
  208. echo "Hash from App Image: ${{ env.hash_from_app_image }}"
  209. echo "Hash from Code: ${{ env.hash_from_code }}"
  210. if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then
  211. echo "Hashes match!"
  212. else
  213. echo "Hashes do not match!"
  214. exit 1
  215. fi
  216. # Run unit tests with the EventStream runtime Docker images as root
  217. test_runtime_root:
  218. name: RT Unit Tests (Root)
  219. needs: [ghcr_build_runtime]
  220. runs-on: ubuntu-latest
  221. strategy:
  222. fail-fast: false
  223. matrix:
  224. base_image: ['nikolaik']
  225. steps:
  226. - uses: actions/checkout@v4
  227. - name: Free Disk Space (Ubuntu)
  228. uses: jlumbroso/free-disk-space@main
  229. with:
  230. # this might remove tools that are actually needed,
  231. # if set to "true" but frees about 6 GB
  232. tool-cache: true
  233. # all of these default to true, but feel free to set to
  234. # "false" if necessary for your workflow
  235. android: true
  236. dotnet: true
  237. haskell: true
  238. large-packages: true
  239. docker-images: false
  240. swap-storage: true
  241. - name: Set up Docker Buildx
  242. id: buildx
  243. uses: docker/setup-buildx-action@v3
  244. # Forked repos can't push to GHCR, so we need to download the image as an artifact
  245. - name: Download runtime image for fork
  246. if: github.event.pull_request.head.repo.fork
  247. uses: actions/download-artifact@v4
  248. with:
  249. name: runtime-${{ matrix.base_image }}
  250. path: /tmp
  251. - name: Load runtime image for fork
  252. if: github.event.pull_request.head.repo.fork
  253. run: |
  254. docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
  255. - name: Cache Poetry dependencies
  256. uses: actions/cache@v4
  257. with:
  258. path: |
  259. ~/.cache/pypoetry
  260. ~/.virtualenvs
  261. key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
  262. restore-keys: |
  263. ${{ runner.os }}-poetry-
  264. - name: Set up Python
  265. uses: actions/setup-python@v5
  266. with:
  267. python-version: '3.12'
  268. - name: Install poetry via pipx
  269. run: pipx install poetry
  270. - name: Install Python dependencies using Poetry
  271. run: make install-python-dependencies
  272. - name: Run runtime tests
  273. run: |
  274. # We install pytest-xdist in order to run tests across CPUs
  275. poetry run pip install pytest-xdist
  276. # Install to be able to retry on failures for flaky tests
  277. poetry run pip install pytest-rerunfailures
  278. image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
  279. image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
  280. TEST_RUNTIME=eventstream \
  281. SANDBOX_USER_ID=$(id -u) \
  282. SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
  283. TEST_IN_CI=true \
  284. RUN_AS_OPENHANDS=false \
  285. 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
  286. - name: Upload coverage to Codecov
  287. uses: codecov/codecov-action@v4
  288. env:
  289. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  290. # Run unit tests with the EventStream runtime Docker images as openhands user
  291. test_runtime_oh:
  292. name: RT Unit Tests (openhands)
  293. runs-on: ubuntu-latest
  294. needs: [ghcr_build_runtime]
  295. strategy:
  296. matrix:
  297. base_image: ['nikolaik']
  298. steps:
  299. - uses: actions/checkout@v4
  300. - name: Free Disk Space (Ubuntu)
  301. uses: jlumbroso/free-disk-space@main
  302. with:
  303. # this might remove tools that are actually needed,
  304. # if set to "true" but frees about 6 GB
  305. tool-cache: true
  306. # all of these default to true, but feel free to set to
  307. # "false" if necessary for your workflow
  308. android: true
  309. dotnet: true
  310. haskell: true
  311. large-packages: true
  312. docker-images: false
  313. swap-storage: true
  314. - name: Set up Docker Buildx
  315. id: buildx
  316. uses: docker/setup-buildx-action@v3
  317. # Forked repos can't push to GHCR, so we need to download the image as an artifact
  318. - name: Download runtime image for fork
  319. if: github.event.pull_request.head.repo.fork
  320. uses: actions/download-artifact@v4
  321. with:
  322. name: runtime-${{ matrix.base_image }}
  323. path: /tmp
  324. - name: Load runtime image for fork
  325. if: github.event.pull_request.head.repo.fork
  326. run: |
  327. docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
  328. - name: Cache Poetry dependencies
  329. uses: actions/cache@v4
  330. with:
  331. path: |
  332. ~/.cache/pypoetry
  333. ~/.virtualenvs
  334. key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
  335. restore-keys: |
  336. ${{ runner.os }}-poetry-
  337. - name: Set up Python
  338. uses: actions/setup-python@v5
  339. with:
  340. python-version: '3.12'
  341. - name: Install poetry via pipx
  342. run: pipx install poetry
  343. - name: Install Python dependencies using Poetry
  344. run: make install-python-dependencies
  345. - name: Run runtime tests
  346. run: |
  347. # We install pytest-xdist in order to run tests across CPUs
  348. poetry run pip install pytest-xdist
  349. # Install to be able to retry on failures for flaky tests
  350. poetry run pip install pytest-rerunfailures
  351. image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
  352. image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
  353. TEST_RUNTIME=eventstream \
  354. SANDBOX_USER_ID=$(id -u) \
  355. SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
  356. TEST_IN_CI=true \
  357. RUN_AS_OPENHANDS=true \
  358. 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
  359. - name: Upload coverage to Codecov
  360. uses: codecov/codecov-action@v4
  361. env:
  362. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  363. # The two following jobs (named identically) are to check whether all the runtime tests have passed as the
  364. # "All Runtime Tests Passed" is a required job for PRs to merge
  365. # Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
  366. # prerequisites have been cancelled or failed so merging is disallowed, otherwise Github considers "skipped" as "success"
  367. runtime_tests_check_success:
  368. name: All Runtime Tests Passed
  369. if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
  370. runs-on: ubuntu-latest
  371. needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
  372. steps:
  373. - name: All tests passed
  374. run: echo "All runtime tests have passed successfully!"
  375. runtime_tests_check_fail:
  376. name: All Runtime Tests Passed
  377. if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
  378. runs-on: ubuntu-latest
  379. needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
  380. steps:
  381. - name: Some tests failed
  382. run: |
  383. echo "Some runtime tests failed or were cancelled"
  384. exit 1
  385. update_pr_description:
  386. name: Update PR Description
  387. if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
  388. needs: [ghcr_build_runtime]
  389. runs-on: ubuntu-latest
  390. steps:
  391. - name: Checkout
  392. uses: actions/checkout@v4
  393. - name: Get short SHA
  394. id: short_sha
  395. run: echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
  396. - name: Update PR Description
  397. env:
  398. GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  399. PR_NUMBER: ${{ github.event.pull_request.number }}
  400. REPO: ${{ github.repository }}
  401. SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
  402. run: |
  403. echo "updating PR description"
  404. DOCKER_RUN_COMMAND="docker run -it --rm \
  405. -p 3000:3000 \
  406. -v /var/run/docker.sock:/var/run/docker.sock \
  407. --add-host host.docker.internal:host-gateway \
  408. -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
  409. --name openhands-app-$SHORT_SHA \
  410. docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA"
  411. PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body)
  412. if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then
  413. UPDATED_PR_BODY=$(echo "${PR_BODY}" | sed -E "s|docker run -it --rm.*|$DOCKER_RUN_COMMAND|")
  414. else
  415. UPDATED_PR_BODY="${PR_BODY}
  416. ---
  417. To run this PR locally, use the following command:
  418. \`\`\`
  419. $DOCKER_RUN_COMMAND
  420. \`\`\`"
  421. fi
  422. echo "updated body: $UPDATED_PR_BODY"
  423. gh pr edit $PR_NUMBER --body "$UPDATED_PR_BODY"