| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- name: Cloud integration tests
- # Runs the per-cloud integration matrix. This workflow handles untrusted PR
- # code from forks, so it relies on layered protections:
- #
- # 1. Trigger is pull_request_target with types=[labeled] — fires only when a
- # label is *added*. A force-push to the PR head does not re-fire this.
- # 2. The `safe-to-test` label is the only one that matters (see `if:` below).
- # Pushing new commits to the PR causes pr-label-strip.yaml to remove the
- # label, so re-testing requires a maintainer to re-label after re-review.
- # 3. The PR head SHA is taken from the event payload, which is captured at
- # label-add time. Even if the attacker force-pushes between label-add and
- # the runner starting, the checkout pins to the SHA the maintainer saw.
- # 4. The `cloud-integration` GitHub Environment requires reviewer approval
- # per run. This is the last-line defense and is enforced by GitHub before
- # the OIDC token / cloud secrets are exposed.
- # 5. The AWS trust policy only accepts the `environment:cloud-integration`
- # sub claim for PR runs — so even if 1–4 were bypassed, a fork PR run
- # without our environment cannot assume the role.
- # 6. Each cloud's secrets are gated by matrix.cloud-provider so a single
- # compromised matrix cell cannot exfiltrate other clouds' credentials.
- #
- # Push to main and workflow_dispatch run without the label gate (the
- # Environment is conditionally skipped — they are trusted contexts).
- on:
- pull_request_target:
- types: [labeled]
- push:
- branches: [main]
- workflow_dispatch: {}
- permissions:
- contents: read
- jobs:
- cloud:
- name: Per-cloud integration tests
- # Gate fork PR runs by the `safe-to-test` label. Push and workflow_dispatch
- # don't carry a `github.event.label` so the right-hand branch fires.
- if: >-
- github.event_name != 'pull_request_target'
- || github.event.label.name == 'safe-to-test'
- runs-on: ubuntu-latest
- # The environment is only set for pull_request_target — push and
- # workflow_dispatch are trusted contexts and don't need a per-run approval.
- environment: ${{ github.event_name == 'pull_request_target' && 'cloud-integration' || '' }}
- permissions:
- id-token: write # required for AWS OIDC
- contents: read
- strategy:
- fail-fast: false
- matrix:
- python-version: ['3.10']
- cloud-provider: ['aws', 'azure', 'gcp', 'openstack']
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
- with:
- # github.event.pull_request.head.sha is captured at trigger time and
- # immutable in the event payload — pins to the SHA the maintainer
- # reviewed when applying the label.
- ref: ${{ github.event.pull_request.head.sha }}
- persist-credentials: false
- - name: Setup Python
- uses: actions/setup-python@v6
- with:
- python-version: ${{ matrix.python-version }}
- - name: Cache pip dir
- uses: actions/cache@v5
- with:
- path: ~/.cache/pip
- key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('**/setup.py', '**/requirements.txt') }}
- - name: Install required packages
- run: pip install tox
- - name: Configure AWS credentials via OIDC
- if: matrix.cloud-provider == 'aws'
- uses: aws-actions/configure-aws-credentials@v6
- with:
- role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
- aws-region: us-east-1
- - name: Run tox
- id: tox
- run: tox -e py${{ matrix.python-version }}-${{ matrix.cloud-provider }}
- env:
- PYTHONUNBUFFERED: "True"
- # Per-cloud secret scoping: each variable is only set in the matrix
- # cell that needs it. Limits blast radius if a single cell is
- # compromised.
- # aws — credentials supplied via the OIDC step above
- CB_VM_TYPE_AWS: ${{ matrix.cloud-provider == 'aws' && secrets.CB_VM_TYPE_AWS || '' }}
- # azure
- AZURE_CLIENT_ID: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_CLIENT_ID || '' }}
- AZURE_SUBSCRIPTION_ID: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_SUBSCRIPTION_ID || '' }}
- AZURE_SECRET: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_SECRET || '' }}
- AZURE_TENANT: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_TENANT || '' }}
- AZURE_RESOURCE_GROUP: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_RESOURCE_GROUP || '' }}
- AZURE_STORAGE_ACCOUNT: ${{ matrix.cloud-provider == 'azure' && secrets.AZURE_STORAGE_ACCOUNT || '' }}
- CB_IMAGE_AZURE: ${{ matrix.cloud-provider == 'azure' && secrets.CB_IMAGE_AZURE || '' }}
- CB_VM_TYPE_AZURE: ${{ matrix.cloud-provider == 'azure' && secrets.CB_VM_TYPE_AZURE || '' }}
- # gcp
- GCP_SERVICE_CREDS_DICT: ${{ matrix.cloud-provider == 'gcp' && secrets.GCP_SERVICE_CREDS_DICT || '' }}
- CB_IMAGE_GCP: ${{ matrix.cloud-provider == 'gcp' && secrets.CB_IMAGE_GCP || '' }}
- CB_VM_TYPE_GCP: ${{ matrix.cloud-provider == 'gcp' && secrets.CB_VM_TYPE_GCP || '' }}
- # openstack
- OS_AUTH_URL: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_AUTH_URL || '' }}
- OS_PASSWORD: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_PASSWORD || '' }}
- OS_PROJECT_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_PROJECT_NAME || '' }}
- OS_PROJECT_DOMAIN_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_PROJECT_DOMAIN_NAME || '' }}
- OS_TENANT_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_TENANT_NAME || '' }}
- OS_USERNAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_USERNAME || '' }}
- OS_REGION_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_REGION_NAME || '' }}
- OS_USER_DOMAIN_NAME: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_USER_DOMAIN_NAME || '' }}
- OS_APPLICATION_CREDENTIAL_ID: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_APPLICATION_CREDENTIAL_ID || '' }}
- OS_APPLICATION_CREDENTIAL_SECRET: ${{ matrix.cloud-provider == 'openstack' && secrets.OS_APPLICATION_CREDENTIAL_SECRET || '' }}
- CB_IMAGE_OS: ${{ matrix.cloud-provider == 'openstack' && secrets.CB_IMAGE_OS || '' }}
- CB_VM_TYPE_OS: ${{ matrix.cloud-provider == 'openstack' && secrets.CB_VM_TYPE_OS || '' }}
- CB_PLACEMENT_OS: ${{ matrix.cloud-provider == 'openstack' && secrets.CB_PLACEMENT_OS || '' }}
- - name: Create Build Status Badge
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- uses: schneegans/dynamic-badges-action@0e50b8bad39e7e1afd3e4e9c2b7dd145fad07501 # v1.8.0
- with:
- auth: ${{ secrets.BUILD_STATUS_GIST_SECRET }}
- gistID: ${{ secrets.BUILD_STATUS_GIST_ID }}
- filename: cloudbridge_py${{ matrix.python-version }}_${{ matrix.cloud-provider }}.json
- label: ${{ matrix.cloud-provider }}
- message: ${{ fromJSON('["passing", "failing"]')[steps.tox.outcome != 'success'] }}
- color: ${{ fromJSON('["green", "red"]')[steps.tox.outcome != 'success'] }}
- - name: Coveralls
- if: ${{ steps.tox.outcome == 'success' }}
- uses: AndreMiras/coveralls-python-action@ac868b9540fad490f7ca82b8ca00480fd751ed19 # develop @ 2024-09-26
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- flag-name: run-${{ matrix.python-version }}-${{ matrix.cloud-provider }}
- parallel: true
- finish:
- needs: cloud
- if: ${{ always() && needs.cloud.result != 'skipped' }}
- runs-on: ubuntu-latest
- steps:
- - name: Coveralls Finished
- uses: AndreMiras/coveralls-python-action@ac868b9540fad490f7ca82b8ca00480fd751ed19 # develop @ 2024-09-26
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- parallel-finished: true
|