ghcr-build.yml 18 KB

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