GoogleCloudPlatform / GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK

Release 0.2.0: configure PyPI/TestPyPI trusted publishers

Open
#353 17 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.