ghcr_runtime.yml 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # Workflow that builds, tests and then pushes the runtime docker images to the ghcr.io repository
  2. name: Build, Test and Publish Runtime Image
  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. jobs:
  21. # Builds the runtime Docker images
  22. ghcr_build_runtime:
  23. name: Build Image
  24. runs-on: ubuntu-latest
  25. permissions:
  26. contents: read
  27. packages: write
  28. strategy:
  29. matrix:
  30. image: ['runtime']
  31. base_image: ['nikolaik/python-nodejs:python3.11-nodejs22', 'python:3.11-bookworm', 'node:22-bookworm']
  32. platform: ['amd64', 'arm64']
  33. outputs:
  34. tags: ${{ steps.capture-tags.outputs.tags }}
  35. steps:
  36. - name: Checkout
  37. uses: actions/checkout@v4
  38. - name: Free Disk Space (Ubuntu)
  39. uses: jlumbroso/free-disk-space@main
  40. with:
  41. # this might remove tools that are actually needed,
  42. # if set to "true" but frees about 6 GB
  43. tool-cache: true
  44. # all of these default to true, but feel free to set to
  45. # "false" if necessary for your workflow
  46. android: true
  47. dotnet: true
  48. haskell: true
  49. large-packages: true
  50. docker-images: false
  51. swap-storage: true
  52. - name: Set up QEMU
  53. uses: docker/setup-qemu-action@v3
  54. - name: Set up Docker Buildx
  55. id: buildx
  56. uses: docker/setup-buildx-action@v3
  57. - name: Install poetry via pipx
  58. run: pipx install poetry
  59. - name: Set up Python
  60. uses: actions/setup-python@v5
  61. with:
  62. python-version: '3.11'
  63. cache: 'poetry'
  64. - name: Install Python dependencies using Poetry
  65. run: make install-python-dependencies
  66. - name: Create source distribution and Dockerfile
  67. run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime --force_rebuild
  68. - name: Build and export image
  69. id: build
  70. run: |
  71. if [ -f 'containers/runtime/Dockerfile' ]; then
  72. echo 'Dockerfile detected, building runtime image...'
  73. ./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} ${{ matrix.platform }}
  74. # Capture the last tag to use in the artifact name
  75. last_tag=$(cat tags.txt | awk '{print $NF}')
  76. else
  77. echo 'No Dockerfile detected which means an exact image is already built. Pulling the image and saving it to a tar file...'
  78. source containers/runtime/config.sh
  79. echo "$DOCKER_IMAGE_HASH_TAG $DOCKER_IMAGE_TAG" >> tags.txt
  80. export last_tag=$DOCKER_IMAGE_TAG
  81. echo "Pulling image $DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE:$DOCKER_IMAGE_HASH_TAG to /tmp/${{ matrix.image }}_${last_tag}_${{ matrix.platform }}.tar"
  82. docker pull $DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE:$DOCKER_IMAGE_HASH_TAG
  83. docker save $DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE:$DOCKER_IMAGE_HASH_TAG -o /tmp/${{ matrix.image }}_${last_tag}_${{ matrix.platform }}.tar
  84. fi
  85. echo "last_tag=${last_tag}" >> $GITHUB_OUTPUT
  86. - name: Capture tags
  87. id: capture-tags
  88. run: |
  89. tags=$(cat tags.txt)
  90. echo "tags=$tags"
  91. echo "tags=$tags" >> $GITHUB_OUTPUT
  92. - name: Upload Docker image as artifact
  93. uses: actions/upload-artifact@v4
  94. with:
  95. name: ${{ matrix.image }}_${{ steps.build.outputs.last_tag }}_${{ matrix.platform }}
  96. path: /tmp/${{ matrix.image }}_${{ steps.build.outputs.last_tag }}_${{ matrix.platform }}.tar
  97. retention-days: 14
  98. - name: Capture last tag
  99. id: capture-last-tag
  100. run: |
  101. last_tag=$(cat tags.txt | awk '{print $NF}')
  102. echo "$last_tag" > /tmp/last-tag-${{ matrix.image }}-${{ matrix.platform }}-${{ steps.build.outputs.last_tag }}.txt
  103. echo "Saved last tag to /tmp/last-tag-${{ matrix.image }}-${{ matrix.platform }}-${{ steps.build.outputs.last_tag }}.txt"
  104. - name: Upload last tag as artifact
  105. uses: actions/upload-artifact@v4
  106. with:
  107. name: last-tag-${{ matrix.image }}-${{ matrix.platform }}-${{ steps.build.outputs.last_tag }}
  108. path: /tmp/last-tag-${{ matrix.image }}-${{ matrix.platform }}-${{ steps.build.outputs.last_tag }}.txt
  109. retention-days: 1
  110. prepare_test_image_tags:
  111. name: Prepare Test Images Tags
  112. needs: ghcr_build_runtime
  113. runs-on: ubuntu-latest
  114. outputs:
  115. test_image_tags: ${{ steps.set-matrix.outputs.test_image_tags }}
  116. steps:
  117. - name: Download last tags
  118. uses: actions/download-artifact@v4
  119. with:
  120. pattern: last-tag-*
  121. path: /tmp/
  122. merge-multiple: true
  123. - name: Set up test matrix
  124. id: set-matrix
  125. run: |
  126. matrix=$(cat /tmp/last-tag-*.txt | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
  127. echo "test_image_tags=$matrix" >> $GITHUB_OUTPUT
  128. echo "Generated test_image_tags: $matrix"
  129. # Run unit tests with the EventStream runtime Docker images
  130. test_runtime:
  131. name: Test Runtime
  132. runs-on: ubuntu-latest
  133. needs: prepare_test_image_tags
  134. strategy:
  135. matrix:
  136. image: ['runtime']
  137. runtime_type: ['eventstream']
  138. platform: ['amd64']
  139. last_tag: ${{ fromJson(needs.prepare_test_image_tags.outputs.test_image_tags) }}
  140. steps:
  141. - uses: actions/checkout@v4
  142. - name: Free Disk Space (Ubuntu)
  143. uses: jlumbroso/free-disk-space@main
  144. with:
  145. tool-cache: true
  146. android: true
  147. dotnet: true
  148. haskell: true
  149. large-packages: true
  150. swap-storage: true
  151. - name: Install poetry via pipx
  152. run: pipx install poetry
  153. - name: Set up Python
  154. uses: actions/setup-python@v5
  155. with:
  156. python-version: '3.11'
  157. cache: 'poetry'
  158. - name: Install Python dependencies using Poetry
  159. run: make install-python-dependencies
  160. - name: Download Runtime Docker image
  161. uses: actions/download-artifact@v4
  162. with:
  163. name: ${{ matrix.image }}_${{ matrix.last_tag }}_${{ matrix.platform }}
  164. path: /tmp/
  165. - name: Load Runtime image and run runtime tests
  166. run: |
  167. image_file=$(find /tmp -name "${{ matrix.image }}_${{ matrix.last_tag }}_${{ matrix.platform }}.tar" | head -n 1)
  168. if [ -z "$image_file" ]; then
  169. echo "No matching image file found for tag: ${{ matrix.last_tag }}"
  170. exit 1
  171. fi
  172. echo "Loading image from file: $image_file"
  173. output=$(docker load -i "$image_file")
  174. # Extract the image name from the output
  175. # Print all tags
  176. echo "All tags:"
  177. all_tags=$(echo "$output" | grep -oP 'Loaded image: \K.*')
  178. echo "$all_tags"
  179. # Choose the last tag
  180. image_name=$(echo "$all_tags" | tail -n 1)
  181. # Print the full name of the image
  182. echo "Loaded Docker image: $image_name"
  183. TEST_RUNTIME=${{ matrix.runtime_type }} SANDBOX_USER_ID=$(id -u) SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true poetry run pytest --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/unit/test_runtime.py
  184. - name: Upload coverage to Codecov
  185. uses: codecov/codecov-action@v4
  186. env:
  187. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  188. # Run integration tests with the eventstream runtime Docker image
  189. runtime_integration_tests_on_linux:
  190. name: Runtime Integration Tests on Linux
  191. runs-on: ubuntu-latest
  192. needs: prepare_test_image_tags
  193. strategy:
  194. fail-fast: false
  195. matrix:
  196. image: ['runtime']
  197. runtime_type: ['eventstream']
  198. platform: ['amd64']
  199. last_tag: ${{ fromJson(needs.prepare_test_image_tags.outputs.test_image_tags) }}
  200. steps:
  201. - uses: actions/checkout@v4
  202. - name: Install poetry via pipx
  203. run: pipx install poetry
  204. - name: Set up Python
  205. uses: actions/setup-python@v5
  206. with:
  207. python-version: '3.11'
  208. cache: 'poetry'
  209. - name: Install Python dependencies using Poetry
  210. run: make install-python-dependencies
  211. - name: Download Runtime Docker image
  212. uses: actions/download-artifact@v4
  213. with:
  214. name: ${{ matrix.image }}_${{ matrix.last_tag }}_${{ matrix.platform }}
  215. path: /tmp/
  216. - name: Load runtime image and run integration tests
  217. run: |
  218. image_file=$(find /tmp -name "${{ matrix.image }}_${{ matrix.last_tag }}_${{ matrix.platform }}.tar" | head -n 1)
  219. if [ -z "$image_file" ]; then
  220. echo "No matching image file found for tag: ${{ matrix.last_tag }}"
  221. exit 1
  222. fi
  223. echo "Loading image from file: $image_file"
  224. output=$(docker load -i "$image_file")
  225. # Extract the image name from the output
  226. image_name=$(echo "$output" | grep -oP 'Loaded image: \K.*' | head -n 1)
  227. # Print the full name of the image
  228. echo "Loaded Docker image: $image_name"
  229. TEST_RUNTIME=${{ matrix.runtime_type }} SANDBOX_USER_ID=$(id -u) SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true TEST_ONLY=true ./tests/integration/regenerate.sh
  230. - name: Upload coverage to Codecov
  231. uses: codecov/codecov-action@v4
  232. env:
  233. CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  234. # Checks that all runtime tests have passed
  235. all_runtime_tests_passed:
  236. name: All Runtime Tests Passed
  237. runs-on: ubuntu-latest
  238. needs: [test_runtime, runtime_integration_tests_on_linux]
  239. steps:
  240. - name: All tests passed
  241. run: echo "All runtime tests have passed successfully!"
  242. # Push the runtime Docker images to the ghcr.io repository
  243. ghcr_push_runtime:
  244. name: Push Image
  245. runs-on: ubuntu-latest
  246. needs: [ghcr_build_runtime, prepare_test_image_tags, all_runtime_tests_passed]
  247. if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
  248. env:
  249. RUNTIME_TAGS: ${{ needs.ghcr_build_runtime.outputs.tags }}
  250. permissions:
  251. contents: read
  252. packages: write
  253. strategy:
  254. matrix:
  255. image: ['runtime']
  256. runtime_type: ['eventstream']
  257. platform: ['amd64', 'arm64']
  258. last_tag: ${{ fromJson(needs.prepare_test_image_tags.outputs.test_image_tags) }}
  259. steps:
  260. - name: Checkout code
  261. uses: actions/checkout@v4
  262. - name: Free Disk Space (Ubuntu)
  263. uses: jlumbroso/free-disk-space@main
  264. with:
  265. tool-cache: true
  266. android: true
  267. dotnet: true
  268. haskell: true
  269. large-packages: true
  270. docker-images: false
  271. swap-storage: true
  272. - name: Login to GHCR
  273. uses: docker/login-action@v3
  274. with:
  275. registry: ghcr.io
  276. username: ${{ github.repository_owner }}
  277. password: ${{ secrets.GITHUB_TOKEN }}
  278. - name: Download Docker images
  279. uses: actions/download-artifact@v4
  280. with:
  281. name: ${{ matrix.image }}_${{ matrix.last_tag }}_${{ matrix.platform }}
  282. path: /tmp/
  283. - name: Load images and push to registry
  284. run: |
  285. image_file=$(find /tmp -name "${{ matrix.image }}_${{ matrix.last_tag }}_${{ matrix.platform }}.tar" | head -n 1)
  286. if [ -z "$image_file" ]; then
  287. echo "No matching image file found for tag: ${{ matrix.last_tag }}"
  288. exit 1
  289. fi
  290. echo "Loading image from file: $image_file"
  291. if ! loaded_image=$(docker load -i "$image_file" | grep "Loaded image:" | head -n 1 | awk '{print $3}'); then
  292. echo "Failed to load Docker image"
  293. exit 1
  294. fi
  295. echo "loaded image = $loaded_image"
  296. image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
  297. echo "image name = $image_name"
  298. echo "$RUNTIME_TAGS" | tr ' ' '\n' | while read -r tag; do
  299. echo "tag = $tag"
  300. if [ -n "$image_name" ] && [ -n "$tag" ]; then
  301. docker tag $loaded_image $image_name:${tag}_${{ matrix.platform }}
  302. docker push $image_name:${tag}_${{ matrix.platform }}
  303. docker buildx imagetools create --tag $image_name:$tag $image_name:${tag}_${{ matrix.platform }}
  304. else
  305. echo "Skipping tag and push due to empty image_name or tag"
  306. fi
  307. done