ghcr-build.yml 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. # Workflow that builds, tests and then pushes the runtime docker images to the ghcr.io repository
  2. name: Build, Test and Publish RT Image
  3. # Only run one workflow of the same group at a time.
  4. # There can be at most one running and one pending job in a concurrency group at any time.
  5. concurrency:
  6. group: ${{ github.workflow }}-${{ github.ref }}
  7. cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
  8. # Always run on "main"
  9. # Always run on tags
  10. # Always run on PRs
  11. # Can also be triggered manually
  12. on:
  13. push:
  14. branches:
  15. - main
  16. tags:
  17. - '*'
  18. pull_request:
  19. workflow_dispatch:
  20. inputs:
  21. reason:
  22. description: 'Reason for manual trigger'
  23. required: true
  24. default: ''
  25. env:
  26. BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22
  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:${{ github.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:${{ github.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:${{ github.sha }}-${{ matrix.base_image }}
  276. image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
  277. SKIP_CONTAINER_LOGS=true \
  278. TEST_RUNTIME=eventstream \
  279. SANDBOX_USER_ID=$(id -u) \
  280. SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
  281. TEST_IN_CI=true \
  282. RUN_AS_OPENHANDS=false \
  283. poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
  284. - name: Upload coverage to Codecov
  285. uses: codecov/codecov-action@v4
  286. env:
  287. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  288. # Run unit tests with the EventStream runtime Docker images as openhands user
  289. test_runtime_oh:
  290. name: RT Unit Tests (openhands)
  291. runs-on: ubuntu-latest
  292. needs: [ghcr_build_runtime]
  293. strategy:
  294. matrix:
  295. base_image: ['nikolaik']
  296. steps:
  297. - uses: actions/checkout@v4
  298. - name: Free Disk Space (Ubuntu)
  299. uses: jlumbroso/free-disk-space@main
  300. with:
  301. # this might remove tools that are actually needed,
  302. # if set to "true" but frees about 6 GB
  303. tool-cache: true
  304. # all of these default to true, but feel free to set to
  305. # "false" if necessary for your workflow
  306. android: true
  307. dotnet: true
  308. haskell: true
  309. large-packages: true
  310. docker-images: false
  311. swap-storage: true
  312. - name: Set up Docker Buildx
  313. id: buildx
  314. uses: docker/setup-buildx-action@v3
  315. # Forked repos can't push to GHCR, so we need to download the image as an artifact
  316. - name: Download runtime image for fork
  317. if: github.event.pull_request.head.repo.fork
  318. uses: actions/download-artifact@v4
  319. with:
  320. name: runtime-${{ matrix.base_image }}
  321. path: /tmp
  322. - name: Load runtime image for fork
  323. if: github.event.pull_request.head.repo.fork
  324. run: |
  325. docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
  326. - name: Cache Poetry dependencies
  327. uses: actions/cache@v4
  328. with:
  329. path: |
  330. ~/.cache/pypoetry
  331. ~/.virtualenvs
  332. key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
  333. restore-keys: |
  334. ${{ runner.os }}-poetry-
  335. - name: Set up Python
  336. uses: actions/setup-python@v5
  337. with:
  338. python-version: '3.12'
  339. - name: Install poetry via pipx
  340. run: pipx install poetry
  341. - name: Install Python dependencies using Poetry
  342. run: make install-python-dependencies
  343. - name: Run runtime tests
  344. run: |
  345. # We install pytest-xdist in order to run tests across CPUs
  346. poetry run pip install pytest-xdist
  347. # Install to be able to retry on failures for flaky tests
  348. poetry run pip install pytest-rerunfailures
  349. image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ github.sha }}-${{ matrix.base_image }}
  350. image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
  351. SKIP_CONTAINER_LOGS=true \
  352. TEST_RUNTIME=eventstream \
  353. SANDBOX_USER_ID=$(id -u) \
  354. SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
  355. TEST_IN_CI=true \
  356. RUN_AS_OPENHANDS=true \
  357. poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
  358. - name: Upload coverage to Codecov
  359. uses: codecov/codecov-action@v4
  360. env:
  361. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  362. # Run integration tests with the eventstream runtime Docker image
  363. runtime_integration_tests_on_linux:
  364. name: RT Integration Tests (Linux)
  365. runs-on: ubuntu-latest
  366. needs: [ghcr_build_runtime]
  367. strategy:
  368. fail-fast: false
  369. matrix:
  370. base_image: ['nikolaik']
  371. steps:
  372. - uses: actions/checkout@v4
  373. - name: Free Disk Space (Ubuntu)
  374. uses: jlumbroso/free-disk-space@main
  375. with:
  376. # this might remove tools that are actually needed,
  377. # if set to "true" but frees about 6 GB
  378. tool-cache: true
  379. # all of these default to true, but feel free to set to
  380. # "false" if necessary for your workflow
  381. android: true
  382. dotnet: true
  383. haskell: true
  384. large-packages: true
  385. docker-images: false
  386. swap-storage: true
  387. - name: Set up Docker Buildx
  388. id: buildx
  389. uses: docker/setup-buildx-action@v3
  390. # Forked repos can't push to GHCR, so we need to download the image as an artifact
  391. - name: Download runtime image for fork
  392. if: github.event.pull_request.head.repo.fork
  393. uses: actions/download-artifact@v4
  394. with:
  395. name: runtime-${{ matrix.base_image }}
  396. path: /tmp
  397. - name: Load runtime image for fork
  398. if: github.event.pull_request.head.repo.fork
  399. run: |
  400. docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
  401. - name: Cache Poetry dependencies
  402. uses: actions/cache@v4
  403. with:
  404. path: |
  405. ~/.cache/pypoetry
  406. ~/.virtualenvs
  407. key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
  408. restore-keys: |
  409. ${{ runner.os }}-poetry-
  410. - name: Set up Python
  411. uses: actions/setup-python@v5
  412. with:
  413. python-version: '3.12'
  414. - name: Install poetry via pipx
  415. run: pipx install poetry
  416. - name: Install Python dependencies using Poetry
  417. run: make install-python-dependencies
  418. - name: Run integration tests
  419. run: |
  420. image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ github.sha }}-${{ matrix.base_image }}
  421. image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
  422. TEST_RUNTIME=eventstream \
  423. SANDBOX_USER_ID=$(id -u) \
  424. SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
  425. TEST_IN_CI=true \
  426. TEST_ONLY=true \
  427. ./tests/integration/regenerate.sh
  428. - name: Upload coverage to Codecov
  429. uses: codecov/codecov-action@v4
  430. env:
  431. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  432. # The two following jobs (named identically) are to check whether all the runtime tests have passed as the
  433. # "All Runtime Tests Passed" is a required job for PRs to merge
  434. # Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
  435. # prerequisites have been cancelled or failed so merging is disallowed, otherwise Github considers "skipped" as "success"
  436. runtime_tests_check_success:
  437. name: All Runtime Tests Passed
  438. if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
  439. runs-on: ubuntu-latest
  440. needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app]
  441. steps:
  442. - name: All tests passed
  443. run: echo "All runtime tests have passed successfully!"
  444. runtime_tests_check_fail:
  445. name: All Runtime Tests Passed
  446. if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
  447. runs-on: ubuntu-latest
  448. needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app]
  449. steps:
  450. - name: Some tests failed
  451. run: |
  452. echo "Some runtime tests failed or were cancelled"
  453. exit 1