integration-cloud.yaml 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. name: Cloud integration tests
  2. # Runs the per-cloud integration matrix. This workflow handles untrusted PR
  3. # code from forks, so it relies on layered protections:
  4. #
  5. # 1. Trigger is pull_request_target with types=[labeled] — fires only when a
  6. # label is *added*. A force-push to the PR head does not re-fire this.
  7. # 2. The `safe-to-test` label is the only one that matters (see `if:` below).
  8. # Pushing new commits to the PR causes pr-label-strip.yaml to remove the
  9. # label, so re-testing requires a maintainer to re-label after re-review.
  10. # 3. The PR head SHA is taken from the event payload, which is captured at
  11. # label-add time. Even if the attacker force-pushes between label-add and
  12. # the runner starting, the checkout pins to the SHA the maintainer saw.
  13. # 4. The `cloud-integration` GitHub Environment requires reviewer approval
  14. # per run. This is the last-line defense and is enforced by GitHub before
  15. # the OIDC token / cloud secrets are exposed.
  16. # 5. The AWS trust policy only accepts the `environment:cloud-integration`
  17. # sub claim for PR runs — so even if 1–4 were bypassed, a fork PR run
  18. # without our environment cannot assume the role.
  19. # 6. Each cloud's secrets are gated by matrix.cloud-provider so a single
  20. # compromised matrix cell cannot exfiltrate other clouds' credentials.
  21. #
  22. # Push to main and workflow_dispatch run without the label gate (the
  23. # Environment is conditionally skipped — they are trusted contexts).
  24. on:
  25. pull_request_target:
  26. types: [labeled]
  27. push:
  28. branches: [main]
  29. workflow_dispatch: {}
  30. permissions:
  31. contents: read
  32. jobs:
  33. cloud:
  34. name: Per-cloud integration tests
  35. # Gate fork PR runs by the `safe-to-test` label. Push and workflow_dispatch
  36. # don't carry a `github.event.label` so the right-hand branch fires.
  37. if: >-
  38. github.event_name != 'pull_request_target'
  39. || github.event.label.name == 'safe-to-test'
  40. runs-on: ubuntu-latest
  41. # The environment is only set for pull_request_target — push and
  42. # workflow_dispatch are trusted contexts and don't need a per-run approval.
  43. environment: ${{ github.event_name == 'pull_request_target' && 'cloud-integration' || '' }}
  44. permissions:
  45. id-token: write # required for AWS OIDC
  46. contents: read
  47. strategy:
  48. fail-fast: false
  49. matrix:
  50. python-version: ['3.10']
  51. cloud-provider: ['aws', 'azure', 'gcp', 'openstack']
  52. steps:
  53. - name: Checkout code
  54. uses: actions/checkout@v6
  55. with:
  56. # github.event.pull_request.head.sha is captured at trigger time and
  57. # immutable in the event payload — pins to the SHA the maintainer
  58. # reviewed when applying the label.
  59. ref: ${{ github.event.pull_request.head.sha }}
  60. persist-credentials: false
  61. - name: Setup Python
  62. uses: actions/setup-python@v6
  63. with:
  64. python-version: ${{ matrix.python-version }}
  65. - name: Cache pip dir
  66. uses: actions/cache@v5
  67. with:
  68. path: ~/.cache/pip
  69. key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('**/setup.py', '**/requirements.txt') }}
  70. - name: Install required packages
  71. run: pip install tox
  72. - name: Configure AWS credentials via OIDC
  73. if: matrix.cloud-provider == 'aws'
  74. uses: aws-actions/configure-aws-credentials@v6
  75. with:
  76. role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
  77. aws-region: us-east-1
  78. - name: Run tox
  79. id: tox
  80. run: tox -e py${{ matrix.python-version }}-${{ matrix.cloud-provider }}
  81. env:
  82. PYTHONUNBUFFERED: "True"
  83. # Per-cloud secret scoping: each variable is only set in the matrix
  84. # cell that needs it. Limits blast radius if a single cell is
  85. # compromised.
  86. # aws — credentials supplied via the OIDC step above
  87. CB_VM_TYPE_AWS: ${{ matrix.cloud-provider == 'aws' && secrets.CB_VM_TYPE_AWS || '' }}
  88. # azure
  89. AZURE_CLIENT_ID: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_CLIENT_ID || '' }}
  90. AZURE_SUBSCRIPTION_ID: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_SUBSCRIPTION_ID || '' }}
  91. AZURE_SECRET: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_SECRET || '' }}
  92. AZURE_TENANT: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_TENANT || '' }}
  93. AZURE_RESOURCE_GROUP: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_RESOURCE_GROUP || '' }}
  94. AZURE_STORAGE_ACCOUNT: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_STORAGE_ACCOUNT || '' }}
  95. CB_IMAGE_AZURE: ${{ matrix.cloud-provider == 'azure' && secrets.CB_IMAGE_AZURE || '' }}
  96. CB_VM_TYPE_AZURE: ${{ matrix.cloud-provider == 'azure' && secrets.CB_VM_TYPE_AZURE || '' }}
  97. # gcp
  98. GCP_SERVICE_CREDS_DICT: ${{ matrix.cloud-provider == 'gcp' && secrets.GCP_SERVICE_CREDS_DICT || '' }}
  99. CB_IMAGE_GCP: ${{ matrix.cloud-provider == 'gcp' && secrets.CB_IMAGE_GCP || '' }}
  100. CB_VM_TYPE_GCP: ${{ matrix.cloud-provider == 'gcp' && secrets.CB_VM_TYPE_GCP || '' }}
  101. # openstack
  102. OS_AUTH_URL: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_AUTH_URL || '' }}
  103. OS_PASSWORD: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_PASSWORD || '' }}
  104. OS_PROJECT_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_PROJECT_NAME || '' }}
  105. OS_PROJECT_DOMAIN_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_PROJECT_DOMAIN_NAME || '' }}
  106. OS_TENANT_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_TENANT_NAME || '' }}
  107. OS_USERNAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_USERNAME || '' }}
  108. OS_REGION_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_REGION_NAME || '' }}
  109. OS_USER_DOMAIN_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_USER_DOMAIN_NAME || '' }}
  110. OS_APPLICATION_CREDENTIAL_ID: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_APPLICATION_CREDENTIAL_ID || '' }}
  111. OS_APPLICATION_CREDENTIAL_SECRET: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_APPLICATION_CREDENTIAL_SECRET || '' }}
  112. CB_IMAGE_OS: ${{ matrix.cloud-provider == 'openstack' && secrets.CB_IMAGE_OS || '' }}
  113. CB_VM_TYPE_OS: ${{ matrix.cloud-provider == 'openstack' && secrets.CB_VM_TYPE_OS || '' }}
  114. CB_PLACEMENT_OS: ${{ matrix.cloud-provider == 'openstack' && secrets.CB_PLACEMENT_OS || '' }}
  115. - name: Create Build Status Badge
  116. if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  117. uses: schneegans/dynamic-badges-action@0e50b8bad39e7e1afd3e4e9c2b7dd145fad07501 # v1.8.0
  118. with:
  119. auth: ${{ secrets.BUILD_STATUS_GIST_SECRET }}
  120. gistID: ${{ secrets.BUILD_STATUS_GIST_ID }}
  121. filename: cloudbridge_py${{ matrix.python-version }}_${{ matrix.cloud-provider }}.json
  122. label: ${{ matrix.cloud-provider }}
  123. message: ${{ fromJSON('["passing", "failing"]')[steps.tox.outcome != 'success'] }}
  124. color: ${{ fromJSON('["green", "red"]')[steps.tox.outcome != 'success'] }}
  125. - name: Coveralls
  126. if: ${{ steps.tox.outcome == 'success' }}
  127. uses: AndreMiras/coveralls-python-action@ac868b9540fad490f7ca82b8ca00480fd751ed19 # develop @ 2024-09-26
  128. with:
  129. github-token: ${{ secrets.GITHUB_TOKEN }}
  130. flag-name: run-${{ matrix.python-version }}-${{ matrix.cloud-provider }}
  131. parallel: true
  132. finish:
  133. needs: cloud
  134. if: ${{ always() && needs.cloud.result != 'skipped' }}
  135. runs-on: ubuntu-latest
  136. steps:
  137. - name: Coveralls Finished
  138. uses: AndreMiras/coveralls-python-action@ac868b9540fad490f7ca82b8ca00480fd751ed19 # develop @ 2024-09-26
  139. with:
  140. github-token: ${{ secrets.GITHUB_TOKEN }}
  141. parallel-finished: true