GoogleCloudPlatform / GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK
Release 0.2.0: configure PyPI/TestPyPI trusted publishers
- Dominant language
- Python
- Stars
- 47
- Forks
- 21
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 33
Description
## Context
Issue #349 moved `bigquery-agent-analytics-tracing` to a no-repo-checkout 0.2.0 release flow (merged as PR #356, `d7b41e3`):
1. publish wheel/sdist to TestPyPI,
2. install from TestPyPI in a clean environment,
3. run `bqaa-otel bootstrap --preflight`,
4. run `bqaa-otel bootstrap --execute`,
5. run `bqaa-otel verify --smoke`,
6. run `bqaa-otel teardown --confirm`,
7. approve image promotion (`release-promote`),
8. smoke the promoted default image without `--image`,
9. approve PyPI, then `finalize` reconciles and publishes the GitHub release.
Both indexes must trust `.github/workflows/release-tracing.yml` via GitHub Actions Trusted Publishing (OIDC — no long-lived tokens), and two GitHub-side prerequisites added during the #356 review must be configured **before** the first tag.
**This body is the canonical checklist.** The status comments below (2026-07-09 and 2026-07-15) are historical and superseded by it. The operational runbook is [`producers/RELEASING.md` on `main`](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/blob/main/producers/RELEASING.md).
## Required before tagging (canonical — verified against live APIs 2026-07-16)
- [x] Merge PR #356 (merged 2026-07-15, `d7b41e3`).
- [x] Merge lock-refresh PR #367 (upstream yanked `build==1.5.1`) — merged as `96a079f`.
- [x] Merge guard-message PR #369 — merged as `7cd8401` (the deviating guard message states both branches: a first-attempt index conflict burns against the **original workflow build anchor**; a full rerun is rejected, not burned).
- [x] Merge docs PR #366 (the full operational runbook contract) — merged as `08d1dd8`; the `main` runbook now carries the complete first-release operational contract.
- [x] GitHub environment `release-promote` exists, has a required reviewer, and is restricted to tag `tracing-v*`.
- [x] **Merge environment-split PR #370** — merged 2026-07-16 as `5e6bcca`. The tracing workflow now uses dedicated `tracing-testpypi`/`tracing-pypi` environments, leaving the root SDK's shared `testpypi`/`pypi` environments for its `v*` release flow; the runbook and pending-publisher fields are updated.
- [ ] **Apply the environment split in repo settings** (Tier 3 — now actionable after #370): create `tracing-testpypi` (no reviewer) and `tracing-pypi` (required reviewer), restrict BOTH plus `release-promote` to tag `tracing-v*`; then restore the root SDK's `pypi` environment (remove the `tracing-v*` rule; apply its own `v*`-appropriate rule) and leave `testpypi` for the root flow.
- [ ] **Add the TestPyPI pending Trusted Publisher** at https://test.pypi.org/manage/account/publishing/ (separate account database — confirm the TestPyPI account separately).
- [ ] **Add the PyPI pending Trusted Publisher** at https://pypi.org/manage/account/publishing/.
Fields for both (environment differs):
| Field | PyPI | TestPyPI |
|---|---|---|
| Project name | `bigquery-agent-analytics-tracing` | `bigquery-agent-analytics-tracing` |
| Owner | `GoogleCloudPlatform` | `GoogleCloudPlatform` |
| Repository | `BigQuery-Agent-Analytics-SDK` | `BigQuery-Agent-Analytics-SDK` |
| Workflow filename | `release-tracing.yml` | `release-tracing.yml` |
| Environment | `tracing-pypi` | `tracing-testpypi` |
Pending publishers do **not** reserve the project name before first publish — configure both shortly before the tag.
- [ ] **Immutable releases — controlled temporary toggle** (repo admin; the setting is repo-wide and the root SDK flow publishes the GitHub release FIRST and has recovered `v0.3.4` and the legacy bare tag `0.2.1` by re-running a tag from a new SHA (the current root workflow intentionally accepts only `vX.Y.Z`), so leaving it permanently on would break root-release recovery). Protocol per tracing release, AFTER the App + publishers are configured: enable immediately before pushing the tag (`PUT /repos/{repo}/immutable-releases`, [API](https://docs.github.com/en/enterprise-cloud@latest/rest/repos/repos#enable-immutable-releases)) → publish → verify the tracing release reports `immutable: true` → disable again ([disabling is not retroactive](https://github.blog/changelog/2025-10-28-immutable-releases-are-now-generally-available/)). `finalize` verifies the setting **before publishing a draft** and fails closed while it is off. Longer term: harden the root SDK flow so verification precedes publication, then leave the setting permanently enabled. Live state 2026-07-16: `{"enabled": false}`.
- [ ] **Configure the policy-read GitHub App** (requires a `GoogleCloudPlatform` **organization owner** — the App requests repository Administration permission, so a repo admin alone cannot install it; [installation requirements](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party)):
1. An organization owner creates an **organization-owned** GitHub App.
2. Registration setting: **"Where can this GitHub App be installed?" → Only on this account** (keeps the App private; [docs](https://docs.github.com/en/enterprise-cloud@latest/apps/creating-github-apps/registering-a-github-app/making-a-github-app-public-or-private)).
3. Grant only repository **Administration: read**; disable webhooks; request no organization/account permissions.
4. Generate and download a private key for the App.
5. The organization owner installs it on **only** `BigQuery-Agent-Analytics-SDK`.
6. Store the App ID as repository variable `BQAA_RELEASE_POLICY_APP_ID` and the PEM as repository secret `BQAA_RELEASE_POLICY_APP_PRIVATE_KEY`. Then delete the downloaded workstation copy of the PEM unless it is retained in an approved secret manager, and record who owns future key rotation — an App private key can authenticate against every installation of that App ([best practices](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/best-practices-for-creating-a-github-app)).
Live state 2026-07-16: the repository has no Actions variables or secrets. `finalize` mints this token only when a **draft** publication is about to happen; idempotent reruns never touch the App credentials.
- [ ] Confirm 2FA/account recovery for both index accounts.
## Required immediately after each index's first trusted upload
- [ ] Add a second Google-controlled **Owner** to each newly created index project, immediately after that index's first trusted upload creates it (impossible earlier). An Owner, not a Maintainer: a Maintainer can upload releases but cannot manage the project or collaborators, so it provides no administrative recovery if the first owner's account becomes unavailable ([PyPI roles](https://docs.pypi.org/organization-accounts/roles-entities/)).
- [ ] Confirm 2FA/account recovery for **both** owners on both indexes.
## Step-by-step operator runbook
Expand: TestPyPI/PyPI accounts, Trusted Publishers, GitHub App, tag, approvals, and post-release cleanup
### 1. Apply the GitHub environment split
In the repository, open **Settings → Environments**.
1. Create `tracing-testpypi`.
- Deployment branches and tags: **Selected branches and tags**.
- Add tag rule `tracing-v*`.
- Do not add a required reviewer.
2. Create `tracing-pypi`.
- Add the designated release reviewer (initially the same reviewer as the existing `pypi` environment).
- Deployment branches and tags: **Selected branches and tags**.
- Add tag rule `tracing-v*`.
3. Leave `release-promote` unchanged: required reviewer + tag rule `tracing-v*`.
4. Restore the root SDK environments.
- In `pypi`, preserve its reviewer and replace `tracing-v*` with tag rule `v*`.
- Keep `testpypi` assigned to the root flow and restrict it to tag `v*`.
- This is intentionally forward-only: the current root workflow accepts only `vX.Y.Z`. Legacy bare tags `0.1.0` through `0.3.2` cannot deploy through the current `release.yml` without a separate workflow change; adding an `[0-9]*` environment rule alone would not restore them because the build job itself skips non-`v` tags.
5. Do not put PyPI tokens in GitHub; both tracing jobs use OIDC Trusted Publishing.
### 2. Prepare the TestPyPI account
TestPyPI has a separate account database from production PyPI.
1. Register at if needed.
2. Verify the email address.
3. In **Account settings**, configure 2FA (preferably a security key plus an authenticator).
4. Generate recovery codes and store them in an approved password manager or offline secure location.
5. Record which Google-controlled account owns the setup.
### 3. Add the TestPyPI pending Trusted Publisher
Open , choose **Add a new pending publisher → GitHub Actions**, and enter:
| Field | Value |
|---|---|
| PyPI project name | `bigquery-agent-analytics-tracing` |
| Owner | `GoogleCloudPlatform` |
| Repository | `BigQuery-Agent-Analytics-SDK` |
| Workflow filename | `release-tracing.yml` |
| Environment | `tracing-testpypi` |
`Owner` is the GitHub organization, not the TestPyPI username. Enter only the workflow basename, not `.github/workflows/release-tracing.yml`. Confirm the publisher appears as pending. Do not manually create the project, upload files, generate an API token, or add a `.pypirc`.
### 4. Prepare production PyPI and add its pending publisher
1. Register or log in at .
2. Verify the email address.
3. Confirm 2FA; add a second method if possible.
4. Generate and securely store recovery codes.
5. Open .
6. Choose **Add a new pending publisher → GitHub Actions** and enter:
| Field | Value |
|---|---|
| PyPI project name | `bigquery-agent-analytics-tracing` |
| Owner | `GoogleCloudPlatform` |
| Repository | `BigQuery-Agent-Analytics-SDK` |
| Workflow filename | `release-tracing.yml` |
| Environment | `tracing-pypi` |
Confirm it appears as pending. Pending publishers create the project on first trusted upload but do not reserve the name beforehand. Do not manually upload a setup release.
### 5. Configure the policy-read GitHub App
A `GoogleCloudPlatform` organization owner must:
1. Create an organization-owned GitHub App.
2. Set **Where can this GitHub App be installed? → Only on this account**.
3. Disable webhooks.
4. Grant only repository **Administration: read**; request no organization/account permissions.
5. Generate a private key.
6. Install the App on only `BigQuery-Agent-Analytics-SDK`.
7. Store the App ID as repository variable `BQAA_RELEASE_POLICY_APP_ID`.
8. Store the complete PEM as repository secret `BQAA_RELEASE_POLICY_APP_PRIVATE_KEY`.
9. Delete the workstation PEM unless retained in an approved secret manager, and record key-rotation ownership.
### 6. Final pre-tag check
Confirm all of the following before enabling immutability:
- The three tracing environments have the exact reviewer/tag policies above.
- Both pending publishers exist with the exact field values above.
- Both index accounts have verified email, 2FA, and recovery codes.
- The App variable and secret exist.
- Protected `main` CI is green.
- `producers/pyproject.toml` is version `0.2.0`.
- Version/project `bigquery-agent-analytics-tracing` is still absent from both indexes.
- No manual distribution files have been uploaded.
### 7. Enable immutable releases immediately before tagging
```bash
gh api --method PUT \
repos/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/immutable-releases
gh api \
repos/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/immutable-releases
```
The second command must report `"enabled": true`.
### 8. Tag the exact current protected-main head
Do not tag whichever local feature branch happens to be checked out. The tagger must have repository-admin access: the active `tracing release tags` ruleset blocks creation, update, and deletion of `tracing-v*` tags for everyone else.
```bash
git fetch origin main
MAIN_SHA=$(git rev-parse origin/main)
VERSION=$(
git show "$MAIN_SHA:producers/pyproject.toml" |
python -c 'import sys,tomllib; print(tomllib.load(sys.stdin.buffer)["project"]["version"])'
)
test "$VERSION" = "0.2.0"
git tag -a tracing-v0.2.0 "$MAIN_SHA" -m "tracing 0.2.0"
test "$(git ls-remote origin refs/heads/main | awk '{print $1}')" = "$MAIN_SHA"
git push origin refs/tags/tracing-v0.2.0
```
If the remote-main comparison fails, delete only the local unpushed tag, fetch the new `main`, and repeat. The workflow independently verifies the tag still equals the current protected-main head.
### 9. Operate the release workflow
1. Watch `.github/workflows/release-tracing.yml`.
2. Let the TestPyPI trusted upload complete.
3. Follow the checksum-gated clean-environment lifecycle in [`producers/RELEASING.md`](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/blob/main/producers/RELEASING.md): download only from TestPyPI, verify against the draft's `SHA256SUMS`, install the verified local wheel with dependencies from production PyPI, then run preflight → execute → smoke → confirmed teardown.
4. Only after the lifecycle passes, approve `release-promote`.
5. Run the promoted-image smoke without `--image`.
6. Only after that passes, approve `tracing-pypi`.
7. Let `finalize` reconcile all surfaces and publish the immutable GitHub release.
8. Never use **Re-run all jobs**. For a recoverable failure, re-run failed jobs from the original workflow attempt.
### 10. Immediately after each index's first trusted upload
As soon as each project is created:
1. Open its **Manage → Collaborators** page.
2. Add a second Google-controlled account as **Owner**, not Maintainer.
3. Confirm both owners have 2FA and recovery methods.
Do this independently after the first TestPyPI upload and after the first production PyPI upload.
### 11. Verify immutability, then restore the repo-wide setting
After `finalize` succeeds, verify the published tracing release reports `"immutable": true`. Only then disable repository-wide immutability so the current root SDK recovery flow is unaffected:
```bash
gh api --method DELETE \
repos/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/immutable-releases
```
Do not disable it while the tracing release is still a draft or while `finalize` may need an original-attempt retry. Disabling the repository setting does not make a release published while it was enabled mutable again.
## Release rules this protects
- No long-lived PyPI API tokens; no manual wheel/sdist uploads — **do not manually upload `0.2.0` as a setup test**. The release contract requires byte-identical workflow-built files on both indexes; any pre-existing or deviating file burns the version.
- No approving `release-promote` until the TestPyPI checksum-gated lifecycle passes; no approving `tracing-pypi` until the post-promotion no-`--image` smoke passes.
- Not every failure burns the version. **Burned**: defective candidate bytes failing the lifecycle gate after TestPyPI accepted them, an index deviation from the **original workflow build anchor** (including pre-existing files found on the FIRST attempt — there the current distributions ARE that anchor, and the conflicting filenames can never be replaced), or a reconciler burn state — bump, rebuild, re-tag. **Not burned**: a transient job failure, lost upload response, or `finalize` failure while the index holds the EXACT anchor bytes — re-run the failed jobs from the **original** workflow attempt (the rerun-safe pre-checks recognize the byte-identical publication and pass without re-uploading). **Rejected, but not a burn**: an accidental full rerun — its rebuilt bytes cannot match, so the guard refuses that attempt; abandon it and return to the recoverable original run. A burn is asserted only against the original workflow build anchor, never merely against a rebuilt one.
- The **first validation is the real `tracing-v0.2.0` workflow** after every **pre-tag** item above is complete — there is no separate dry run.
## Why the two GitHub-side items must precede the tag
Nothing burns if they are missed — the pipeline fails closed — but the failure lands *after* both approvals and after the indexes accepted the uploads: the version would sit published on PyPI/TestPyPI while the GitHub release stays a draft, until the items are configured and `finalize` is re-run from the **original** workflow attempt (never a full rerun).
## References
- #349 (release contract), PR #356 (merged, `d7b41e3`)
- Runbook: [`producers/RELEASING.md` on `main`](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/blob/main/producers/RELEASING.md)
- PyPI: [adding a publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) · [pending publishers](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) · [using a publisher](https://docs.pypi.org/trusted-publishers/using-a-publisher/) · [TestPyPI accounts are separate](https://packaging.python.org/en/latest/guides/using-testpypi/)
Contributor guide
Research direction
Start with producers/RELEASING.md and .github/workflows/release-tracing.yml, then inspect the repository Environments settings and the PyPI/TestPyPI pending-publisher pages. Done means the tracing environments, publishers, policy-read App credentials, account protections, and controlled immutable-release procedure match the checklist before tagging version 0.2.0.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github, github-actions, python
- Domain
- ci-cd, devops, release, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100