ghcr-build.yml 17 KB

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