diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0a5ff3f..7897649 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,3 +1,4 @@ +# Live workflow. Legacy token-keeper snapshot: docs/ci/legacy/build-and-test.token-keeper.yml name: Build and Test on: @@ -13,7 +14,7 @@ jobs: build-and-test: name: Build and Test runs-on: ubuntu-latest - environment: chris-1xrn.wbx.ai + environment: webex-ci env: POETRY_VIRTUALENVS_CREATE: false steps: @@ -33,10 +34,31 @@ jobs: poetry self add "poetry-dynamic-versioning[plugin]" poetry install --no-root --only=dev - - name: Get Webex Token + - name: Get Webex access token id: webex_token + env: + WEBEX_ACCESS_TOKEN_STATIC: ${{ secrets.WEBEX_ACCESS_TOKEN }} + WEBEX_CLIENT_ID: ${{ secrets.WEBEX_CLIENT_ID }} + WEBEX_CLIENT_SECRET: ${{ secrets.WEBEX_CLIENT_SECRET }} + WEBEX_REFRESH_TOKEN: ${{ secrets.WEBEX_REFRESH_TOKEN }} run: | - WEBEX_ACCESS_TOKEN=$(curl -s ${{ secrets.WEBEX_TOKEN_KEEPER_URL }} | jq -r .access_token) + set -euo pipefail + if [ -n "${WEBEX_ACCESS_TOKEN_STATIC:-}" ]; then + echo "WEBEX_ACCESS_TOKEN=$WEBEX_ACCESS_TOKEN_STATIC" >> "$GITHUB_OUTPUT" + echo "::add-mask::$WEBEX_ACCESS_TOKEN_STATIC" + exit 0 + fi + RESPONSE=$(curl -sS -X POST https://webexapis.com/v1/access_token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=refresh_token" \ + --data-urlencode "client_id=${WEBEX_CLIENT_ID}" \ + --data-urlencode "client_secret=${WEBEX_CLIENT_SECRET}" \ + --data-urlencode "refresh_token=${WEBEX_REFRESH_TOKEN}") + WEBEX_ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token) + if [ "$WEBEX_ACCESS_TOKEN" = "null" ] || [ -z "$WEBEX_ACCESS_TOKEN" ]; then + echo "::error::Token refresh failed (no access_token in response)." + exit 1 + fi echo "WEBEX_ACCESS_TOKEN=$WEBEX_ACCESS_TOKEN" >> "$GITHUB_OUTPUT" echo "::add-mask::$WEBEX_ACCESS_TOKEN" diff --git a/docs/ci/MAINTAINERS.md b/docs/ci/MAINTAINERS.md new file mode 100644 index 0000000..9fc5d38 --- /dev/null +++ b/docs/ci/MAINTAINERS.md @@ -0,0 +1,61 @@ +# CI integration tests (Webex) — maintainer notes + +This document describes how the GitHub Actions **Build and Test** workflow authenticates to Webex and which settings must be aligned so the suite can run against a shared org. + +## Webex integration and org (stakeholder alignment) + +Before running CI against a Webex org, maintainers should agree on: + +- **Integration (Developer portal):** A single Webex integration used for **CI only** (bot or service user), with OAuth scopes sufficient for the API tests in `tests/api/`. +- **Control Hub org:** The org that hosts test users, licenses (e.g. **Advanced Messaging** where required by tests), and admin actions for people/rooms. Document who can grant licenses and manage the integration. +- **Rotation:** Multiple repository admins should have GitHub permission to manage **Environment secrets** for the `webex-ci` environment so updates are not blocked by a single person. + +## GitHub Environment: `webex-ci` + +Create a GitHub [Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) named **`webex-ci`** (or rename the workflow to match if you choose a different name). + +1. In the repo or organization: **Settings → Environments → New environment**. +2. Add **several** trusted maintainers as admins who can edit secrets and variables. +3. Configure **Environment secrets** and **Environment variables** as below (or move secrets from the old `chris-1xrn.wbx.ai` environment after migrating). + +### Variables (GitHub Environment or repository) + +| Variable | Purpose | +|----------|---------| +| `WEBEX_TEST_DOMAIN` | Email domain used for synthetic test addresses (see `tests/environment.py`). | +| `WEBEX_TEST_ID_START` | Starting index for generated test email addresses. | +| `WEBEX_TEST_FILE_URL` | URL of a downloadable file used by fixtures. | + +### Secrets (GitHub Environment) + +| Secret | Required | Purpose | +|--------|----------|---------| +| `WEBEX_GUEST_ISSUER_ID` | Yes | Guest issuer for tests that need it. | +| `WEBEX_GUEST_ISSUER_SECRET` | Yes | Guest issuer secret. | +| `WEBEX_CLIENT_ID` | Yes for OAuth path | Webex integration OAuth client ID. | +| `WEBEX_CLIENT_SECRET` | Yes for OAuth path | Webex integration OAuth client secret. | +| `WEBEX_REFRESH_TOKEN` | Yes for OAuth path | OAuth refresh token for the CI user/bot (obtained via authorization code flow). | +| `WEBEX_ACCESS_TOKEN` | Optional | **Alternative:** If set, the workflow uses this token directly and skips the OAuth refresh step (useful for short-term testing or when refresh setup is not ready). Prefer OAuth refresh for production CI. | + +**Deprecated:** `WEBEX_TOKEN_KEEPER_URL` is no longer used by CI. The legacy keeper-based workflow is archived under [`docs/ci/legacy/`](legacy/README.md). + +## Obtaining and rotating OAuth refresh tokens + +1. In the [Webex Developer Portal](https://developer.webex.com/), open your CI integration and note **Client ID** and **Client Secret**. +2. Complete the OAuth **authorization code** flow for the CI identity to obtain an initial `access_token` and `refresh_token`. +3. Store `WEBEX_CLIENT_ID`, `WEBEX_CLIENT_SECRET`, and `WEBEX_REFRESH_TOKEN` in the `webex-ci` environment secrets. +4. The workflow exchanges the refresh token at **`POST https://webexapis.com/v1/access_token`** (`grant_type=refresh_token`). If the response includes a new `refresh_token`, update the secret in GitHub. + +If refresh fails (invalid grant, revoked token), repeat the authorization code flow and update `WEBEX_REFRESH_TOKEN`. + +## Fork and pull request limitations + +- **Secrets are not available to workflows triggered from forks** by default. Contributors pushing from a fork will not run integration tests with real Webex credentials unless you use a different, carefully reviewed pattern (e.g. `pull_request_target` with strict guards — not recommended without security review). +- **Practical approach:** Run full integration tests on branches in the **main repository** (e.g. maintainer branches), or rely on scheduled runs / `push` to `main` after merge. + +## Legacy reference + +The previous token-keeper workflow (`WEBEX_TOKEN_KEEPER_URL`, environment `chris-1xrn.wbx.ai`) is preserved for reference only: + +- [`docs/ci/legacy/build-and-test.token-keeper.yml`](legacy/build-and-test.token-keeper.yml) +- [`docs/ci/legacy/README.md`](legacy/README.md) diff --git a/docs/ci/legacy/README.md b/docs/ci/legacy/README.md new file mode 100644 index 0000000..45046ba --- /dev/null +++ b/docs/ci/legacy/README.md @@ -0,0 +1,7 @@ +# Legacy CI workflow (reference only) + +This directory preserves a snapshot of the **pre-migration** GitHub Actions workflow that obtained a Webex access token by calling an external HTTP endpoint (`WEBEX_TOKEN_KEEPER_URL`) and used the GitHub Environment `chris-1xrn.wbx.ai`. + +- **Retired:** 2026-03-24 (archived for archaeology; not executed by GitHub Actions). +- **Not executed:** Files here are outside `.github/workflows/` on purpose so they do not register as workflows. +- **Replaced by:** [`.github/workflows/build-and-test.yml`](../../../.github/workflows/build-and-test.yml) using OAuth `refresh_token` grant (or optional static `WEBEX_ACCESS_TOKEN` secret) against the `webex-ci` GitHub Environment. See [`../MAINTAINERS.md`](../MAINTAINERS.md). diff --git a/docs/ci/legacy/build-and-test.token-keeper.yml b/docs/ci/legacy/build-and-test.token-keeper.yml new file mode 100644 index 0000000..0a5ff3f --- /dev/null +++ b/docs/ci/legacy/build-and-test.token-keeper.yml @@ -0,0 +1,63 @@ +name: Build and Test + +on: + push: + branches: + - main + schedule: + - cron: "0 13 * * 1" + workflow_dispatch: + workflow_call: + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + environment: chris-1xrn.wbx.ai + env: + POETRY_VIRTUALENVS_CREATE: false + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - uses: Gr1N/setup-poetry@v9 + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Install Dependencies + run: | + poetry self add "poetry-dynamic-versioning[plugin]" + poetry install --no-root --only=dev + + - name: Get Webex Token + id: webex_token + run: | + WEBEX_ACCESS_TOKEN=$(curl -s ${{ secrets.WEBEX_TOKEN_KEEPER_URL }} | jq -r .access_token) + echo "WEBEX_ACCESS_TOKEN=$WEBEX_ACCESS_TOKEN" >> "$GITHUB_OUTPUT" + echo "::add-mask::$WEBEX_ACCESS_TOKEN" + + - name: Build + run: poetry build + + - name: Install + run: pip install dist/*.whl + + - name: Test + run: pytest -s -m "not slow and not manual" + env: + WEBEX_ACCESS_TOKEN: ${{ steps.webex_token.outputs.WEBEX_ACCESS_TOKEN }} + WEBEX_TEST_DOMAIN: ${{ vars.WEBEX_TEST_DOMAIN }} + WEBEX_TEST_ID_START: ${{ vars.WEBEX_TEST_ID_START }} + WEBEX_TEST_FILE_URL: ${{ vars.WEBEX_TEST_FILE_URL }} + WEBEX_GUEST_ISSUER_ID: ${{ secrets.WEBEX_GUEST_ISSUER_ID }} + WEBEX_GUEST_ISSUER_SECRET: ${{ secrets.WEBEX_GUEST_ISSUER_SECRET }} + + - name: Upload Distribution Files + uses: actions/upload-artifact@v4 + with: + name: distribution-files + path: dist/