ghcr_runtime.yml 14 KB

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