integration-testing.yaml 14 KB

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