2
0

integration-testing.yaml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. name: Run OpenCost Integration Tests
  2. on:
  3. schedule:
  4. - cron: '0 14 * * *'
  5. push:
  6. branches:
  7. - develop
  8. pull_request_target:
  9. branches:
  10. - develop
  11. merge_group:
  12. types: [checks_requested]
  13. concurrency:
  14. group: ${{ github.event.merge_group.head.sha || github.event.pull_request.head.sha || github.ref }}-intg-tests
  15. cancel-in-progress: false
  16. permissions: {}
  17. jobs:
  18. check_actor_permissions:
  19. runs-on: ubuntu-latest
  20. if: ${{ github.event_name == 'pull_request_target' }}
  21. outputs:
  22. ismaintainer: ${{ steps.determine-maintainer.outputs.ismaintainer }}
  23. steps:
  24. - name: Check team membership
  25. uses: tspascoal/get-user-teams-membership@v3
  26. if: ${{ github.actor != 'dependabot[bot]' }}
  27. id: teamAffiliation
  28. with:
  29. GITHUB_TOKEN: ${{ secrets.ORG_READER_PAT }}
  30. username: ${{ github.actor }}
  31. organization: opencost
  32. - name: determine if actor is a maintainer
  33. id: determine-maintainer
  34. env:
  35. TEAMS: ${{ join(steps.teamAffiliation.outputs.teams, ',') }}
  36. ACTOR: ${{ github.actor }}
  37. IS_MAINTAINER: ${{ contains(join(steps.teamAffiliation.outputs.teams, ','), 'OpenCost Maintainers') || github.actor == 'dependabot[bot]' }}
  38. run: |
  39. echo "Actor: $ACTOR"
  40. echo "Is maintainer: $IS_MAINTAINER"
  41. echo "ismaintainer=$IS_MAINTAINER" >> $GITHUB_OUTPUT
  42. noop-tests:
  43. needs: check_actor_permissions
  44. permissions: {}
  45. runs-on: ubuntu-latest
  46. if: ${{ (always() && !cancelled()) && github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'false' }}
  47. outputs:
  48. is_noop: ${{ steps.noop-tests.outputs.is_noop }}
  49. steps:
  50. - name: Tests Not Needed
  51. id: noop-tests
  52. run: |
  53. echo "integration tests not running because you are not a maintainer. they will run automatically when a PR is merged."
  54. echo "is_noop=true" >> $GITHUB_OUTPUT
  55. wait_for_image_ready:
  56. runs-on: ubuntu-latest
  57. permissions: {}
  58. needs: check_actor_permissions
  59. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  60. outputs:
  61. IMAGE_TAG: ${{ steps.set_image_tags.outputs.IMAGE_TAG }}
  62. NAMESPACE: ${{ steps.set_image_tags.outputs.NAMESPACE }}
  63. MAINBRANCH: ${{ steps.set_image_tags.outputs.mainbranch }}
  64. passed: ${{ steps.wait_for_image_ready.outputs.passed }}
  65. steps:
  66. - uses: actions/checkout@v4
  67. with:
  68. ref: ${{ github.event.merge_group.head.sha || github.event.pull_request.head.sha || github.ref }}
  69. - name: Set OC SHA
  70. id: sha
  71. run: |
  72. echo "OC_SHORTHASH=$(git rev-parse --short HEAD)"
  73. echo "OC_SHORTHASH=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
  74. - name: Set image tags
  75. id: set_image_tags
  76. env:
  77. OC_SHORTHASH: ${{ steps.sha.outputs.OC_SHORTHASH }}
  78. REPO_OWNER: ${{ github.repository_owner }}
  79. EVENT_NAME: ${{ github.event_name }}
  80. PR_NUMBER: ${{ github.event.pull_request.number }}
  81. run: |
  82. echo "github.event_name: $EVENT_NAME"
  83. if [[ "$EVENT_NAME" == "merge_group" ]]; then
  84. echo "IMAGE_TAG=ghcr.io/$REPO_OWNER/opencost:test-$OC_SHORTHASH" >> $GITHUB_OUTPUT
  85. echo "NAMESPACE=merge-queue-oc-$OC_SHORTHASH" >> $GITHUB_OUTPUT
  86. echo "mainbranch=false" >> $GITHUB_OUTPUT
  87. elif [[ "$EVENT_NAME" == "pull_request_target" ]]; then
  88. echo "building on maintainer pull request branch"
  89. echo "IMAGE_TAG=ghcr.io/$REPO_OWNER/opencost:test-$OC_SHORTHASH" >> $GITHUB_OUTPUT
  90. echo "NAMESPACE=pr-$PR_NUMBER-oc-$OC_SHORTHASH" >> $GITHUB_OUTPUT
  91. echo "mainbranch=false" >> $GITHUB_OUTPUT
  92. else
  93. echo "building on develop branch"
  94. echo "IMAGE_TAG=ghcr.io/$REPO_OWNER/opencost:develop-$OC_SHORTHASH" >> $GITHUB_OUTPUT
  95. echo "NAMESPACE=develop-oc-$OC_SHORTHASH" >> $GITHUB_OUTPUT
  96. echo "mainbranch=true" >> $GITHUB_OUTPUT
  97. fi
  98. - name: Log into ghcr.io
  99. uses: docker/login-action@v3
  100. with:
  101. registry: ghcr.io
  102. username: ${{ github.actor }}
  103. password: ${{ secrets.GITHUB_TOKEN }}
  104. - name: wait for docker image to be ready
  105. id: wait_for_image_ready
  106. env:
  107. IMAGE_TAG: ${{ steps.set_image_tags.outputs.IMAGE_TAG }}
  108. run: |
  109. max_attempts=100
  110. # Loop until the Docker image can be pulled
  111. until docker manifest inspect $IMAGE_TAG; do
  112. echo "Waiting for Docker image $IMAGE_TAG to be available, $max_attempts tries remain..."
  113. sleep 6
  114. max_attempts=$((max_attempts - 1))
  115. if [[ $max_attempts -eq 0 ]]; then
  116. echo "Docker image $IMAGE_TAG is not available after 10 minutes. Exiting..."
  117. exit 1
  118. fi
  119. done
  120. echo "Docker image $IMAGE_TAG is ready!"
  121. echo "passed=true" >> $GITHUB_OUTPUT
  122. build-test-stack:
  123. needs: wait_for_image_ready
  124. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  125. uses: opencost/opencost-infra/.github/workflows/build-stack.yaml@main
  126. secrets: inherit
  127. with:
  128. oc-container-version: "${{ needs.wait_for_image_ready.outputs.IMAGE_TAG }}"
  129. namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}"
  130. build-test-stack-promless:
  131. needs: wait_for_image_ready
  132. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  133. uses: opencost/opencost-infra/.github/workflows/build-stack.yaml@main
  134. secrets: inherit
  135. with:
  136. oc-container-version: "${{ needs.wait_for_image_ready.outputs.IMAGE_TAG }}"
  137. namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}-promless"
  138. prometheus: false
  139. wait-for-dns:
  140. needs: [wait_for_image_ready, build-test-stack]
  141. runs-on: ubuntu-latest
  142. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  143. permissions: {}
  144. steps:
  145. - name: Wait for DNS to resolve
  146. id: wait-for-dns
  147. env:
  148. NAMESPACE: ${{ needs.wait_for_image_ready.outputs.NAMESPACE }}
  149. run: |
  150. echo "Waiting for $NAMESPACE.infra.opencost.io to resolve in DNS..."
  151. max_attempts=60
  152. until host $NAMESPACE.infra.opencost.io; do
  153. echo "DNS not yet resolved for $NAMESPACE.infra.opencost.io, $max_attempts tries remain..."
  154. sleep 10
  155. max_attempts=$((max_attempts - 1))
  156. if [[ $max_attempts -eq 0 ]]; then
  157. echo "DNS resolution failed for $NAMESPACE.infra.opencost.io after 10 minutes. Exiting..."
  158. exit 1
  159. fi
  160. done
  161. echo "DNS resolved successfully for $NAMESPACE.infra.opencost.io!"
  162. run-tests:
  163. needs: [wait_for_image_ready, build-test-stack, wait-for-dns]
  164. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  165. permissions: {}
  166. uses: opencost/opencost-infra/.github/workflows/test-stack.yaml@main
  167. secrets: inherit
  168. with:
  169. namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}"
  170. target_branch: "${{ github.event.pull_request.head.ref || 'main' }}"
  171. wait-for-data-collection:
  172. needs: [wait_for_image_ready, build-test-stack, build-test-stack-promless]
  173. runs-on: ubuntu-latest
  174. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  175. permissions: {}
  176. steps:
  177. - name: Wait 22 minutes for promless data collection
  178. run: |
  179. sleep 1320 # 22 minutes
  180. run-comparison-tests:
  181. needs: [wait_for_image_ready, build-test-stack, build-test-stack-promless, wait-for-data-collection]
  182. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  183. permissions: {}
  184. uses: opencost/opencost-infra/.github/workflows/test-stack.yaml@main
  185. secrets: inherit
  186. with:
  187. namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}"
  188. comparison_namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}-promless"
  189. target_branch: "${{ github.event.pull_request.head.ref || 'main' }}"
  190. comparison: true
  191. print-outputs:
  192. needs: [run-comparison-tests, run-tests]
  193. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  194. runs-on: ubuntu-latest
  195. permissions: {}
  196. steps:
  197. - name: Print outputs
  198. run: |
  199. echo "NAMESPACE: ${{ needs.wait_for_image_ready.outputs.NAMESPACE }}"
  200. echo "COMPARISON_NAMESPACE: ${{ needs.wait_for_image_ready.outputs.NAMESPACE }}-promless"
  201. echo "TARGET_BRANCH: ${{ github.event.pull_request.head.ref || 'main' }}"
  202. echo "PASSED COMPARISON: ${{ needs.run-comparison-tests.outputs.passed }}"
  203. echo "PASSED INTEGRATION: ${{ needs.run-tests.outputs.passed }}"
  204. hold-on-failure:
  205. needs: [wait_for_image_ready, run-tests, run-comparison-tests]
  206. if: ${{ always() && (needs.run-tests.outputs.passed == 'false' || needs.run-comparison-tests.outputs.passed == 'false') && github.event_name != 'merge_group'}}
  207. runs-on: ubuntu-latest
  208. permissions: {}
  209. steps:
  210. - name: Hold stack for investigation
  211. env:
  212. NAMESPACE: ${{ needs.wait_for_image_ready.outputs.NAMESPACE }}
  213. run: |
  214. echo "Tests failed. Holding stacks up for 1 hour for investigation..."
  215. echo "Stack namespace: $NAMESPACE"
  216. echo "Stack will be automatically torn down after 1 hour"
  217. sleep 3600
  218. teardown-test-stack:
  219. needs: [wait_for_image_ready, run-tests, run-comparison-tests, hold-on-failure]
  220. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  221. uses: opencost/opencost-infra/.github/workflows/destroy-stack.yaml@main
  222. secrets: inherit
  223. permissions: {}
  224. with:
  225. namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}"
  226. teardown-test-stack-comparison:
  227. needs: [wait_for_image_ready, run-comparison-tests, hold-on-failure]
  228. if: ${{ (always() && !cancelled()) && ( github.event.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true')) }}
  229. uses: opencost/opencost-infra/.github/workflows/destroy-stack.yaml@main
  230. secrets: inherit
  231. permissions: {}
  232. with:
  233. namespace: "${{ needs.wait_for_image_ready.outputs.NAMESPACE }}-promless"
  234. check-success:
  235. needs: [noop-tests, run-tests, run-comparison-tests]
  236. permissions: {}
  237. runs-on: ubuntu-latest
  238. if: ${{ always() }}
  239. steps:
  240. - name: Check success
  241. id: check-success
  242. env:
  243. IS_NOOP: ${{ needs.noop-tests.outputs.is_noop }}
  244. PASSED: ${{ needs.run-tests.outputs.passed }}
  245. run: |
  246. echo "IS_NOOP: $IS_NOOP"
  247. echo "PASSED: $PASSED"
  248. if [[ "$IS_NOOP" == "true" ]]; then
  249. echo "No-op tests, skipping success check"
  250. exit 0
  251. fi
  252. if [[ "$PASSED" != "true" ]]; then
  253. echo "One or more integration tests failed"
  254. exit 1
  255. fi
  256. echo "All integration tests passed"
  257. exit 0
  258. set-labels:
  259. needs: [noop-tests, run-tests, run-comparison-tests]
  260. if: ${{ (always() && !cancelled()) && ( github.event_name == 'pull_request_target' && needs.check_actor_permissions.outputs.ismaintainer == 'true') }}
  261. runs-on: ubuntu-latest
  262. permissions: {}
  263. steps:
  264. - name: label integration tests failing
  265. if: ${{ always() && contains(needs.*.result, 'failure') && !cancelled()}}
  266. uses: andymckay/labeler@1.0.4
  267. with:
  268. add-labels: "integration tests failed"
  269. - uses: mondeja/remove-labels-gh-action@v2
  270. if: ${{ always() && contains(needs.*.result, 'failure') && !cancelled()}}
  271. with:
  272. token: ${{ secrets.GITHUB_TOKEN }}
  273. labels: |
  274. integration tests passed
  275. - name: Label integration tests passing
  276. if: ${{ always() && !contains(needs.*.result, 'failure') && !cancelled()}}
  277. uses: andymckay/labeler@1.0.4
  278. with:
  279. add-labels: "integration tests passed"
  280. - uses: mondeja/remove-labels-gh-action@v2
  281. if: ${{ always() && !contains(needs.*.result, 'failure') && !cancelled()}}
  282. with:
  283. token: ${{ secrets.GITHUB_TOKEN }}
  284. labels: |
  285. integration tests failed