integration-testing.yaml 16 KB

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