Skip to content

Commit 08d1dd8

Browse files
authored
docs(release): org-owned private App procedure with key handling (#353) (#366)
* docs(release): org-owned private App procedure with key handling (#353) Mirror the issue-353 tracker into the runbook: the policy-read App requires GoogleCloudPlatform organization-owner authority to install (repository Administration permission), must be registered as installable only on this account (private App), webhooks disabled and no organization/account permissions, with explicit private-key generation, installation scoped to this repository only, and PEM handling guidance (delete the workstation copy after storing the secret; record key-rotation ownership). * docs(release): first-release setup parity + corrected recovery semantics (#353 review) - Immutable-release refusal recovery: the GitHub release stays a draft, but PyPI/TestPyPI already contain the exact files when finalize reaches the policy precondition — enable the setting and re-run finalize from the ORIGINAL attempt, never a full rerun. - Pre-flight: the first release requires PENDING Trusted Publishers on both indexes (no project exists yet; the pending publisher creates it on first trusted upload). - One-time GitHub setup enumerates the tracing-v* tag restriction on all three environments and scopes required reviewers to pypi and release-promote only. - Version-burn rule distinguishes true burns (defective bytes after acceptance, index deviations, reconciler burn states) from retryable failures where the index holds the exact anchor bytes (original-attempt rerun; the pre-checks pass without re-upload). * docs(release): reject-vs-burn distinction, missing-release state, Owner requirement, publisher failure paths (#353 review) - A full rerun is REJECTED (return to the recoverable original run), not itself a burn: a burn is asserted only for deviation from the original accepted anchor. - The reconciler burn-state list includes missing-release. - Post-first-publish: a second Google-controlled Owner (not Maintainer) on each index, timed to each index's first trusted upload, with 2FA/recovery for both owners. - Publisher misconfiguration is documented per DAG path: missing TestPyPI publisher stops promote/publish-pypi; missing production publisher fails only publish-pypi after earlier stages pass. * docs(release): 'original workflow build anchor' wording; first-attempt conflicts burn (#353 review round 2)
1 parent 7cd8401 commit 08d1dd8

1 file changed

Lines changed: 89 additions & 24 deletions

File tree

producers/RELEASING.md

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ The tag namespace (`tracing-vX.Y.Z`) is distinct from the root SDK's
2020
the curated customer-first template via
2121
`scripts/render_release_notes.py`; generic generated notes are
2222
disabled — the repo-wide tag stream would pull unrelated SDK PRs).
23-
4. PyPI side: project + Trusted Publisher are already configured.
24-
Setup is one-time per project — see "PyPI Trusted Publishing
25-
setup" below.
23+
4. PyPI side: a **pending Trusted Publisher** is registered on BOTH
24+
indexes (for the first release neither project exists yet — the
25+
pending publisher creates it on the first trusted upload and does
26+
not reserve the name before then; for later releases the existing
27+
project publishers suffice). Setup is one-time per project — see
28+
"PyPI Trusted Publishing setup" below.
2629

2730
## Cut the release
2831

@@ -164,10 +167,30 @@ Post evidence on the release issue.
164167
## Version-burn rule
165168

166169
TestPyPI and PyPI must carry **byte-identical artifacts at the same
167-
version**. If a candidate fails the gate, that version is burned
168-
everywhere: bump the version, re-tag, rebuild from scratch. Never
169-
re-upload, never re-tag an image (staging and public tags are
170-
immutable — enforced at the repository level).
170+
version**. Not every failure burns the version — distinguish:
171+
172+
- **Burned**: a lifecycle-gate failure caused by defective candidate
173+
bytes after TestPyPI accepted them, an index deviation from the
174+
**original workflow build anchor** (subset/extra/yanked/digest
175+
mismatch — including pre-existing files found on the FIRST attempt,
176+
where the current distributions are that anchor and the conflicting
177+
filenames can never be replaced), or any reconciler burn state
178+
(`empty-release`, `testpypi-partial`, `partial`,
179+
`missing-release`). Bump the version, rebuild, re-tag. Never
180+
re-upload, never re-tag an image (staging and public tags are
181+
immutable — enforced at the repository level).
182+
- **NOT burned**: a transient job failure, a lost upload response, or
183+
a `finalize` failure while the index carries the EXACT anchor bytes
184+
— re-run the failed jobs from the **original** workflow attempt
185+
(the rerun-safe pre-checks recognize the byte-identical publication
186+
and pass without re-uploading).
187+
- **Rejected, but not a burn**: an accidental **full rerun**. Its
188+
rebuilt bytes cannot match what an index accepted, so the guard
189+
refuses the rebuilt attempt — abandon it and return to the
190+
**original** workflow run, which remains recoverable (its draft and
191+
artifact are preserved). A burn is asserted only against the
192+
ORIGINAL workflow build anchor, never merely against a newly
193+
rebuilt one.
171194

172195
## Verifying the release
173196

@@ -217,9 +240,11 @@ tar -tzf /tmp/plugin.tar.gz | head
217240
original attempt: the pre-check verifies the index already carries
218241
the exact byte-identical files and skips the re-upload.
219242
- **`finalize` refuses to publish because immutable releases are
220-
disabled** — nothing was published (the release is still a draft):
221-
enable the setting (Settings → General → Releases → immutable
222-
releases) and re-run `finalize` from the original attempt.
243+
disabled** — the GitHub release remains a draft, but PyPI and
244+
TestPyPI already contain the exact files (the index stages run
245+
before `finalize`). Enable immutable releases (Settings → General →
246+
Releases) and re-run `finalize` from the **original** workflow
247+
attempt; never trigger a full rerun.
223248
- **a release was somehow published while the setting was off**
224249
immutability is NOT retroactive: enabling the setting cannot protect
225250
it, so treat the version as burned (delete the release, yank any
@@ -248,10 +273,20 @@ These names must match exactly — the `environment:` blocks in the
248273
workflow are the binding contract.
249274

250275
GitHub-side one-time setup: create environments `testpypi`, `pypi`,
251-
and `release-promote` in the repo settings, with required reviewers
252-
on `pypi` and `release-promote` (approving `release-promote` asserts
253-
the TestPyPI full-lifecycle gate passed), and **enable immutable
254-
releases BEFORE the first release** (Settings → General → Releases):
276+
and `release-promote` in the repo settings —
277+
278+
- restrict **all three** environments to tag `tracing-v*`
279+
(Settings → Environments → Deployment branches and tags → Selected →
280+
Tag rule `tracing-v*`); the `testpypi` rule matters even without a
281+
reviewer, because it stops a branch-modified copy of the workflow
282+
from using the TestPyPI OIDC identity and burning a version;
283+
- required reviewers on `pypi` and `release-promote` ONLY (approving
284+
`release-promote` asserts the TestPyPI full-lifecycle gate passed);
285+
`testpypi` gets no reviewer — the manual lifecycle verdict is the
286+
later `release-promote` approval —
287+
288+
and **enable immutable releases BEFORE the first release**
289+
(Settings → General → Releases):
255290
`finalize` verifies the setting and refuses to publish while it is
256291
off, because GitHub applies immutability only at publish time — a
257292
release published while the setting is off keeps mutable assets and
@@ -261,19 +296,49 @@ release published while the setting is off keeps mutable assets and
261296
Policy-read credential (one-time): the immutable-releases check calls
262297
`GET /repos/{repo}/immutable-releases`, which requires repository
263298
**Administration: read** — a permission the workflow `GITHUB_TOKEN`
264-
can never be granted. Create a GitHub App whose ONLY repository
265-
permission is Administration: read, install it on this repository
266-
only, and set the repository variable `BQAA_RELEASE_POLICY_APP_ID`
267-
plus the secret `BQAA_RELEASE_POLICY_APP_PRIVATE_KEY`. The workflow
268-
mints a short-lived installation token from these only when a DRAFT
269-
publication is about to happen — idempotent reruns of an
299+
can never be granted. Because the App requests repository
300+
Administration permission, installation needs a **GoogleCloudPlatform
301+
organization owner** — a repository admin alone cannot complete this
302+
([installation requirements](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party)):
303+
304+
1. An organization owner creates an **organization-owned** GitHub
305+
App.
306+
2. Registration setting: **"Where can this GitHub App be
307+
installed?" → Only on this account** — keeps the App private so no
308+
other account can install it
309+
([docs](https://docs.github.com/en/enterprise-cloud@latest/apps/creating-github-apps/registering-a-github-app/making-a-github-app-public-or-private)).
310+
3. Grant only repository **Administration: read**; disable webhooks;
311+
request no organization or account permissions.
312+
4. Generate and download a private key for the App.
313+
5. The organization owner installs it on **only**
314+
`BigQuery-Agent-Analytics-SDK`.
315+
6. Store the App ID as the repository variable
316+
`BQAA_RELEASE_POLICY_APP_ID` and the PEM as the repository secret
317+
`BQAA_RELEASE_POLICY_APP_PRIVATE_KEY`. Then delete the downloaded
318+
workstation copy of the PEM unless it is retained in an approved
319+
secret manager, and record who owns future key rotation — an App
320+
private key can authenticate against every installation of that
321+
App ([best practices](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/best-practices-for-creating-a-github-app)).
322+
323+
The workflow mints a short-lived installation token from these only
324+
when a DRAFT publication is about to happen — idempotent reruns of an
270325
already-published release perform no policy read and never touch the
271326
App credentials, so a rotated key cannot break them or mask the
272327
mutable-release burn guidance; all other API calls keep the standard
273328
job token. Until they are configured, `finalize` fails at the token
274329
mint with a clear error and the release stays a draft.
275330

276-
Until both publishers are configured, the `publish-testpypi` and
277-
`publish-pypi` jobs will fail with a clear error. The `build` and
278-
`github-release` jobs are independent and will still complete, so
279-
the tag stays valid.
331+
Publisher misconfiguration fails at two different points in the DAG:
332+
a missing **TestPyPI** publisher fails `publish-testpypi`, so
333+
`promote` and `publish-pypi` never run; a missing **production**
334+
publisher lets every earlier stage pass and then fails
335+
`publish-pypi`. In both cases the error is a clear OIDC/trusted-
336+
publisher failure, the `build` and `github-release` jobs still
337+
complete, and the tag stays valid.
338+
339+
Immediately after each index's FIRST trusted upload creates that
340+
index project, add a second Google-controlled **Owner** (not
341+
Maintainer — a Maintainer can upload but cannot manage the project or
342+
collaborators, so it provides no administrative recovery;
343+
https://docs.pypi.org/organization-accounts/roles-entities/) and
344+
confirm 2FA/account recovery for both owners.

0 commit comments

Comments
 (0)