Skip to content

Commit d7b41e3

Browse files
authored
chore(release): 0.2.0 bump, pipx-primary docs, customer-first release notes (#349 PR4) (#356)
* chore(release): 0.2.0 bump, pipx-primary docs, customer-first release notes (#349 PR4) Last code increment before the tracing-v0.2.0 tag: - producers 0.1.0 -> 0.2.0. - Docs flip: deploy/otlp_receiver/README.md leads with the released- package path (pipx install -> preflight -> bootstrap -> teardown, no repo checkout, digest-pinned image, no build permissions); the source checkout Cloud Build path moves to its own secondary section. producers/README.md points enterprise admins at it. - Customer-first release notes: producers/scripts/ release_notes_template.md (install -> preflight -> bootstrap -> config artifacts -> verify -> cleanup, pinned image reference, verified product versions, hero-demo evaluation link); the github-release job renders it with version+digest via body_path, with GitHub auto-notes appended below. Template rendering verified locally. * fix(release): SA split, tag provenance, publication ordering, doc corrections (#356 review) Addresses all 13 findings (8 inline + 5 body) from the release review: Infrastructure (applied live to bqaa-releases, IAM read-back verified): - P0 approval bypass: split service accounts. The stager (bqaa-release-publisher, used by the UNPROTECTED build-image job) can now write ONLY the private staging repo; a new promoter (bqaa-release-promoter) can write ONLY the public repo, and its WIF binding trusts ONLY the release-promote environment's OIDC subject — an unapproved workflow cannot reach the public registry. - P0 tag provenance: "tracing release tags" ruleset (active) restricts tracing-v* creation/update/deletion to repository admins, and the verify job now refuses to proceed unless the tagged SHA is an ancestor of protected main — checked before any credentialed job. Workflow: - P1 publication ordering: promote no longer publishes the GitHub release; a final rerunnable finalize job publishes the draft AFTER the pypi approval + upload, always with --latest=false so the SDK's vX.Y.Z release stays the repository's Latest. - P1 generated notes: generate_release_notes disabled (a live preview for tracing-v0.2.0 pulled unrelated SDK PRs against a v0.4.0 base); the rendered template now carries a curated tracing-only changelog (#316, #324, #317, #340, #342, #343). - P2 renderer: extracted to scripts/render_release_notes.py, PR-tested (6 tests: no placeholder drift, digest pinning, canonical CODEX_MIN_VERSION from config_artifacts instead of a hand copy, absolute tag-pinned demo URL, token-fill + confirmed-teardown presence, curated changelog coverage). Docs: - P1 release notes template: token retrieval + placeholder replacement before distribution (Codex does not expand env vars in headers); teardown shown as preview THEN --confirm; absolute tag-pinned demo link (relative hrefs 404 from release pages). - P1 deploy README: verify example uses the installed bqaa-otel (the no-checkout guide no longer verifies through a checkout); teardown shows the --confirm execution alongside the preview. - P1 producers README: absolute GitHub URL for the deployment guide (relative links break in the PyPI long description). - P1 RELEASING.md: the lifecycle gate's teardown now says --confirm — gate evidence must show real deletion + existence PASSes. * fix(release): promoter staging-read, rerun safety, supply-chain pins, honest gates (#356 review round 2) Addresses all 13 findings from the second release review. Infrastructure (applied live, read-back verified): - P1-1: promoter granted artifactregistry.reader on bqaa-staging — crane copy pulls its source with promoter credentials; without this the very first promotion would have failed. - P2-9 (partial): release-promote and pypi environments now carry a deployment tag policy restricting them to tracing-v* refs. prevent_self_review is deliberately NOT set: this is a single-maintainer repository and it would deadlock every approval. Workflow: - P1-3: verify installs .[dev,receiver] and asserts the protobuf import BEFORE tests — the release suite can no longer silently skip the documented protobuf path (review reproduced 389/5, the fifth skip being protobuf). The image self-test now also drives real HTTP through the built container: bad-token OTLP POST must return 401, proving gunicorn + app factory + auth end to end. - P1-4: run_attempt is part of the candidate tag and the artifact name, so reruns never collide with immutable staging tags; public promotion is idempotent (an existing tag is accepted only with the exact expected digest, otherwise the version is declared burned). - P1-5: github-release refuses to touch an already-published release (rerun guard) and sets overwrite_files: false. - P1-6: every third-party action pinned to a reviewed full SHA (checkout, setup-python, upload/download-artifact, gh-release, gcp-auth, pypi-publish). - P1-8: finalize runs always(): reconciles the PyPI file set (want 2) and release assets (want 4); publishes the draft only on COMPLETE state; partial publication surfaces the yank/version-burn recovery instead of stranding assets silently. - P2-12: the main-ancestry check is now an exact-head check — a tag pointing at a stale or reverted ancestor of main is refused. Renderer/template/docs (TDD, renderer tests extended to 8): - P1-2: the token-fill step covers BOTH generated artifacts (claude-code.managed-settings.json AND codex.config.toml), guards against an empty token, and asserts no <token> placeholder survives. - P2-10: PROJECT/DATASET/REGION defined up front; URL recovered via gcloud run services describe (bootstrap prints it but exports nothing). - P2-11: the renderer holds no duplicate coordinate — --public-image is a required, validated argument fed from the workflow's authoritative env var. - P1-7: RELEASING.md gate procedure no longer mixes indexes: download the exact TestPyPI candidate --no-deps, checksum against the draft SHA256SUMS, install deps from real PyPI, install the verified local file. - P2-13: workflow header + RELEASING.md job walkthrough describe the actual promote/finalize split and reconciliation states. * fix(release): bootable self-test, byte-identity finalize, executed lifecycle tests (#356 review round 3) Addresses all 12 findings. The headline: the container self-test the previous round added could not even boot (make_app eagerly constructs PubSubPublisher -> DefaultCredentialsError without ADC; the review reproduced it). This round the ENTIRE self-test flow was executed locally before landing. P1: - Self-test boots against a Pub/Sub emulator (credential-free channel, no product-code change) and now proves the full advertised path: bad token -> 401 AND a real authenticated protobuf export -> 200 (decode + publish through the emulator). Executed locally: 401/200. - Artifact identity comes from the producing job (build exports dist_name); every downloader consumes it — a downstream-only rerun no longer requests an artifact that was never uploaded. - Existing-release lookup fails CLOSED: only an explicit 404 proceeds; a stale draft from an earlier attempt is deleted (assets can never mix across attempts); published -> hard fail; auth/rate-limit/ network/parse errors -> hard fail. - finalize verifies IDENTITY, not counts: exact expected filenames, sha256sum -c against the attested SHA256SUMS, PyPI digests equal to the same manifest and not yanked, and the public image digest re-checked against the release body — a failed upload is idempotent only when byte identity is proven. P2: - Producers PR CI installs .[dev,receiver] — the protobuf path runs in every PR, not only at tag time. - Dockerfile base pinned by digest + a pip-freeze audit layer (full hash-locked deps noted as follow-up). - Token-fill block is strict bash (set -euo pipefail, per-file existence + placeholder checks, python replacement so the token never appears in argv) and the tests EXECUTE it — which immediately caught a real bug: TOKEN was unexported and invisible to the child process. Tests now reproduce the real shell flow. - RELEASING.md gate: mktemp dir, exact filename, sha256sum -c against the downloaded SHA256SUMS (mechanical, not an eyeball); the old mixed-index verification recipe removed — one safe path. - deploy README defines PROJECT/DATASET/REGION and derives URL before use; stale generate_release_notes claim fixed; promote renamed and stripped of contents:write (only finalize touches the release). * fix(release): artifact-anchored reconcile, hermetic self-test, hash-locked image (#356 review round 4) Addresses all 12 findings; the trust chain now anchors on immutable things only, and the previously tag-only Docker path runs in PR CI. P1: - Reconciliation anchored to the IMMUTABLE build artifact, never the draft's own manifest (the review reproduced a present-but-unlisted file passing sha256sum -c). New scripts/reconcile_release.py with a 10-case state-matrix suite: exact filename sets everywhere (subsets AND extras rejected), byte identity against the anchor, yanked rejected, plus a distinct invalid-anchor state. finalize downloads the build artifact and the release assets, runs the tested script, and re-checks the public image digest against the constant extracted from the ANCHOR wheel. - No mutable host code between test and push: the protobuf payload is generated INSIDE the image under test, and the tested image ID is re-asserted immediately before docker push. - The Pub/Sub emulator is digest-pinned and isolated on a private bridge network; only the receiver port reaches the host, so the oracle cannot spoof receiver responses. - Bounded readiness (finite retries, curl connect/max timeouts), a cleanup trap covering every exit path, and timeout-minutes on every job. - Promotion fails CLOSED on indeterminate registry state: only an explicit MANIFEST_UNKNOWN enters the copy path; auth/rate-limit/ network errors abort. crane token now travels via --password-stdin. P2: - Self-test extracted to deploy/otlp_receiver/selftest.sh (checked in, EXECUTED locally end-to-end before landing: version assert proved itself by failing on a wrong version, then 401 + authenticated protobuf 200) and wired into producers-ci — the Docker path is no longer tag-only. Version + both-factory assertions restored. - Image dependencies hash-locked: requirements.lock generated with pip-compile --generate-hashes INSIDE the pinned base image; Dockerfile installs --require-hashes + package --no-deps. Rebuilt and re-self-tested locally with the lock. - .dockerignore: the build context drops from ~920 MB to the producers tree + lock. - Token-fill chmods 600 before writing (executed test asserts the mode); RELEASING.md candidate-tag text + dev install extra synced; producers README relative links absolutized for the PyPI long description and the plugin example bumped to 0.2.0; workflow synopsis matches the actual jobs. * fix(ci): image-selftest job runs from the repo root producers-ci sets working-directory: producers as the workflow default; the new job needs the repository root (Dockerfile build-context contract). * fix(release): locked build backend, proven publication, exhaustive dispatch (#356 review round 5) All five P1s + the P2s, each verified by execution: - P1 anchor toolchain: the wheel/sdist/plugin anchor is now built with a reviewed hash-locked toolchain (producers/build-requirements.lock: hatchling + build, generated inside the pinned base image), python -m build --no-isolation, and the metadata wheel install runs --no-deps — no mutable upstream code touches the job that produces the trusted anchor. - P1 container backend: the Dockerfile installs the same locked backend and builds the package with --no-build-isolation --no-deps; the CI log symptom the review cited ("Installing build dependencies" after the locked install) is gone. Rebuilt + re-self-tested locally. - P1 proven publication: the self-test parses the response body and requires published > 0 AND dead_lettered == 0 before emitting the image ID — HTTP 200 alone can mean published=0 or dead-lettered records (the review reproduced both). Local run: "decoded + published: 1 record(s), 0 dead-lettered". - P1 exhaustive dispatch: the state→workflow mapping moved into reconcile_release.dispatch() — unit-tested (4 new tests), exhaustive and fail-closed; invalid-anchor and unknown states can never fall through with exit 0, and each state carries its own recovery advice (rerun vs yank/burn vs investigate-CI). - P1 gate selection: producers-ci path filters (push AND pull_request) now include deploy/otlp_receiver/**, .dockerignore, and release-tracing.yml — a Dockerfile/lock/self-test-only PR can no longer skip its own gate. - P2 lookup semantics: finalize distinguishes explicit absence (release-not-found, PyPI 404) from indeterminate failures (auth/rate-limit/network/5xx), which now fail with surface-specific diagnostics instead of routing the operator to the wrong recovery. 409 tests green; image rebuilt with both locks and the full self-test executed locally end to end. * fix(release): re-anchored release body, cross-surface states, staged image (#356 review round 6) All four P1s + four P2s + metadata sync: P1: - The published release body is re-rendered AT FINALIZE from the protected checkout, with the digest extracted from the anchor wheel, and published atomically via gh release edit --notes-file: a draft body mutated during the approval pause can never reach customers while asset checks pass. - Producers PR CI now runs the EXACT trusted-anchor build path (--require-hashes toolchain, python -m build --no-isolation, wheel install --no-deps) — a lock/backend incompatibility fails in PR CI, not first at tag time. - A malformed HTTP-200 PyPI body is a new fail-closed 'invalid-response' state (INDETERMINATE, retry-the-lookup recovery), never confirmed absence; CLI-level tests cover truncated, non-object, and explicit-404-marker bodies. - A missing GitHub release no longer short-circuits: surfaces are classified independently, with a new 'missing-release' state (PyPI has files but the release is gone -> cross-surface partial, yank+burn recovery). dispatch() stays exhaustive; 19 -> 24 tests. P2: - Multi-stage Dockerfile: builder stage compiles the wheel with the locked backend; the runtime stage installs only the runtime lock + built wheel. Verified: full self-test passes and the runtime image contains zero build/hatchling packages. Header comment now documents the actual contract (digest-pinned base + two locks). - PyPI lookup wraps curl in an explicit transport-error conditional with connect/max timeouts; DNS/TLS/connect failures get their own diagnostic instead of dying under set -e. - deploy/otlp_receiver/build-toolchain.in checked in + regen-locks.sh regenerates BOTH locks inside the pinned base image (copy-pastable, reviewable). - Workflow synopsis: the draft is published by finalize, not promote. Also: Artifact Registry vulnerability scanning (containerscanning.googleapis.com) enabled on bqaa-releases per the #349 security-artifacts item. * fix(release): validated cross-surface states, pinned lock generator (#356 review round 7) Both P1s + all four P2s: - P1: the release-missing branch no longer classifies an unvalidated PyPI 200 as missing-release. Surface combination moved INTO the tested reconciler: the anchor is validated and the PyPI response is schema-checked on EVERY path (urls must be a list of objects with string filenames and valid sha256 digest objects; duplicates rejected) before any classification. {"urls": []} with a missing release is 'unpublished'; a truncated/invalid body is 'invalid-response' (indeterminate, retry-first) — never destructive yank advice. New tests cover the release-present/missing x 404/empty/files/malformed x anchor-valid/invalid matrix (19 -> 34 reconciler tests). - P1: regen-locks.sh no longer executes an unpinned resolver with write access: pip-tools (+hatchling for metadata, +setuptools via --allow-unsafe) installs from the reviewed pip-tools.lock, the repository mounts READ-ONLY, outputs land in a scratch dir and only the two expected lock files are copied back. A new producers-ci lock-drift job runs the exact generator and fails on any diff. Executed end to end locally (which also surfaced a macOS Docker gotcha: /tmp and /var/folders are not shared into the VM — scratch lives under $HOME). - P2: object-shaped invalid PyPI schemas return 'invalid-response' instead of crashing (reproduced TypeError/KeyError cases now tests). - P2: the complete publish-time re-anchor path is a composed, PR-tested helper (publish_release_body.py: anchor-wheel digest -> renderer -> atomic gh release edit --notes-file --draft=false --latest=false, tested with a fake anchor wheel and stubbed gh); finalize calls it. - P2: the image self-test asserts the multi-stage property (no build/ hatchling importable, no /build or /src tree) — verified locally against the rebuilt image. - P2: reconciler docstring lists all six states. Locks regenerated via the hardened path; image rebuilt from them and the full self-test (incl. hygiene asserts) passed locally. * fix(release): complete PyPI schema, absence-first recovery, convergent locks (#356 review round 8) Both P1s + all P2s: - P1 complete success schema: an HTTP-200 body must carry 'urls' (the explicit 404 — a new --pypi-missing flag — is the ONLY absence marker; '{}' is now invalid-response), every sha256 must fullmatch [0-9a-f]{64} (uppercase/wrong-length/non-hex rejected), and 'yanked' must exist with Boolean type. The reproduced yanked-omitted -> complete case is now a test, plus negatives for missing urls, null/string yanked, and each bad-digest shape — none can publish or emit destructive recovery. - P1 absence-first classification: confirmed PyPI absence is decided BEFORE asset recovery advice. New states: missing-all (no draft, nothing on PyPI -> rerun, PyPI needs no cleanup) and draft-invalid (broken draft, nothing on PyPI -> delete draft + rerun, no cleanup there); 'partial' and its yank/burn advice are reserved for states where validated PyPI files actually exist. Tests assert state AND the recovery message properties (the word 'yank' may not appear in any nothing-was-published state). Reconciler suite: 29 -> 37 tests. - P2 lock convergence: regen-locks.sh now regenerates pip-tools.lock from pip-tools.in in the same run and the CI drift check diffs all THREE locks — the generator bootstrap input can no longer drift silently. - P2 generator operational safety: repo root derived from BASH_SOURCE (caller-cwd independent), EXIT trap cleans scratch on every path, every output validated non-empty before ANY worktree copy, and the base image reference is parsed from the Dockerfile itself (single canonical source). Executed end to end locally: only pip-tools.lock changed (now self-consistent). - P2 action pinning: producers-ci actions are now SHA-pinned too, so the PR description's claim holds for both workflows. * fix(release): empty-release semantics, convergent generator, transactional copyback (#356 review round 9) The P1 + all four P2s: - P1: an HTTP-200 PyPI body with urls: [] is a new 'empty-release' state, never absence — the release record exists with deleted files and PyPI forbids filename reuse, so same-version rerun advice would fail. Recovery: burn (bump + re-tag), explicitly no yank and no rerun (asserted by tests for both release-present and release-missing). pypi=None (explicit 404 via --pypi-missing) is now the SOLE confirmed-absence predicate. - P2: two-phase generator convergence — phase 1 (env from the reviewed lock) produces a candidate generator lock; phase 2 runs in a FRESH container installed from the candidate, regenerates it (resolution content must match; the pip-compile header legitimately embeds each phase's output path) and only then compiles the downstream locks with the NEW toolchain. Running it live immediately caught a real stale generator lock (pip-tools had moved) — iterated to the fixed point and the script now proves convergence in-run. - P2: single canonical base — the Dockerfile declares ARG PYTHON_BASE consumed by BOTH stages; the generator parses every literal reference and refuses unless the set is exactly one value (executed test injects a divergent second stage). - P2: transactional copyback — staged next to destinations, originals backed up, rollback on ERR/INT/TERM; the executed test injects a second-copy failure and proves all three destination hashes remain unchanged with no stray staging files. - P2: the module documents the complete nine-state contract from ONE mapping (_STATE_ACTIONS feeds KNOWN_STATES, dispatch(), and the docstring; tests assert the vocabularies match); the stale '{}' marker comments are gone. 443 tests green; full two-phase regen + image rebuild + hygiene self-test executed locally end to end. * fix(release): signal-safe commit boundary, recoverable convergence, consumer validation (#356 review round 10) The P1 + all P2s, each with executed proof: - P1 signal-safe transaction: rollback is armed ONLY until every destination rename succeeded; at the commit point the transaction is marked committed and ERR/INT/TERM disarm before best-effort backup cleanup; INT exits 130 and TERM 143 after rolling back. The executed test injects TERM after EVERY rename and during EVERY cleanup deletion (six positions): the result is always all-old (nonzero exit) or all-new — the reproduced one-new-two-old-exit-0 window is closed. - P2 recoverable convergence: the fixed-point loop iterates in fresh hash-locked containers with an explicit bound (MAX_PHASES, default 5), each phase seeding the next; on bounded exhaustion the last seed/candidate pair is preserved in a diagnostics directory instead of being deleted by the EXIT trap (a rerun from the unchanged checked-in seed was a deterministic dead end). Stubbed-generator tests cover three-phase convergence and non-convergence with preserved diagnostics; the real regen ran end to end (converged in one phase from the checked-in fixed point). - P2 FROM-consumer validation: the guard now requires exactly one digest-pinned ARG PYTHON_BASE and exactly two FROM instructions that BOTH consume ${PYTHON_BASE} — the reproduced bypass (both stages on ubuntu:latest with the unused ARG intact) plus unpinned ARG, second variable, duplicate ARG, and wrong stage counts are all executed negative tests. - P2 exact state contract: the nine states are constants defined once (returns, dispatch keys, and the doc table all use them); the contract test asserts exact bidirectional set equality between constants, KNOWN_STATES, and the parsed docstring rows, with mutation negatives proving a stale extra row and a removed row both fail (the old one-way substring check accepted both). 451 tests green; regen + suite executed locally end to end. * fix(release): rerun-safe draft guard, visibility + TestPyPI reconciliation, distinct convergence diagnostics (#356 review round 11) - github-release: the stale-draft delete now goes through PR-tested guard_existing_release.py — deletion is allowed ONLY when PyPI and TestPyPI both return an explicit 404 for the version. Once an index accepted files, a rebuilt anchor can never byte-match them, so the draft is preserved and the operator reruns the FAILED jobs from the original workflow attempt. Full release × index matrix tested, including the reproduced draft-present + PyPI-complete full rerun. - finalize: fetches release METADATA before assets — visibility is part of the reconciled state. A draft published early during the approval pause becomes the new premature-publication state (re-draft advice), never "keep the draft". Publication reasserts the canonical title, body, prerelease=false, latest=false. - finalize: TestPyPI is a reconciled surface — schema-validated exactly like production, with absent/complete/partial in the tested recovery matrix. Production-404 + exact draft + partial TestPyPI is the new testpypi-partial state (version burned there; bump + re-tag), not "rerun publication". Pruned TestPyPI files never block a complete production release; a digest conflict does (the gate installed different bytes). - regen-locks: the convergence loop preserves the phase input as previous-seed.lock before reseeding, so exhaustion diagnostics carry the last two DISTINCT resolutions; the test asserts the contents differ and represent the final phase's input and output. - requirements.lock: google-auth 2.55.2 → 2.56.0 (upstream release; regenerated via regen-locks.sh so the drift check matches again). * fix(release): draft-aware discovery, snapshot-bound immutable publish, rerun-safe uploads (#356 review round 12) P1 fixes: - Draft discovery: the by-tag endpoint returns only PUBLISHED releases, so the normal draft 404'd and finalize could never publish a successful release. Guard and finalize now discover via authenticated List Releases filtered on the exact tag (ambiguity fails closed), retain the release ID, and download assets BY ASSET ID. - finalize is gated on github-release success: when the rerun guard deliberately fails to preserve the original draft, finalize no longer reconciles it against the rebuilt, byte-different anchor — the guard's original-attempt recovery stays authoritative. - Snapshot-bound publication: publish_release_body.py fetches the exact release ID, verifies every asset digest (GitHub asset `digest` field) against anchor bytes immediately BEFORE and AFTER an ID-based PATCH, reasserts tag/title/body/prerelease/make_latest, and asserts the published release is IMMUTABLE (fails closed until the repository setting is enabled; documented as one-time setup). Idempotent rerun: published + immutable + snapshot-identical needs no edit. - TOCTOU-safe deletion: the stale draft is re-fetched by ID immediately before deletion and must still be the same unpublished draft; delete goes by ID. All index lookups now carry a unique cache-busting query string (PyPI responses are CDN-cached). P2 fixes: - reconcile: the production-complete branch validates EVERY present TestPyPI file (expected name, not yanked, digest-identical) instead of the name intersection; pruned/missing files stay tolerated. Both reviewer reproductions (extra platform wheel, yanked expected wheel) are regressions. - reconcile: visibility is orthogonal — every published non-complete outcome (indeterminate states included) becomes premature-publication with the underlying classification preserved in the detail line; the workflow's public-image mismatch override also keeps the published signal. Containment advice covers non-redraftable immutable releases. - check_index_publication.py: rerun-safe pre-upload gate for both publish jobs — absent proceeds, an exact byte-identical existing publication satisfies the stage without re-upload (skip-existing stays disabled), anything else is a burn. - regen-locks: rollback restores only the backups created by THIS invocation, so a stale .bqaa-bak stranded by a previous run's interrupted cleanup can never mix lock generations; two-run interruption regression added. * fix(release): no automated deletion, JSON publish payload, immutability as precondition (#356 review round 13) P1 fixes: - The workflow never deletes a release automatically: GitHub has no conditional delete, so the GET/DELETE pair raced publication. A stale draft (both indexes 404) now fails the github-release guard with instructions to verify and delete it manually, then re-run the job. - publish_release_body.py sends the publish edit as a JSON --input payload: `gh api -f body=@file` transmits the literal string "@file" (reproduced live via the /markdown endpoint), so the rendered notes never reached the release. Tests now assert the transmitted payload equals the rendered notes. - Immutability is a PREREQUISITE, not a postcondition: finalize checks GET /repos/{repo}/immutable-releases and refuses to publish while the setting is disabled — GitHub applies immutability only at publish time, so enabling it later cannot retroactively protect a release. A release found published-but-mutable is burned, never "enable and re-run" (RELEASING.md guidance corrected); the post-publish immutable:true assert remains as defense in depth. P2 fixes: - The idempotent and post-publish paths verify the canonical EDITABLE metadata (title, rendered body, prerelease) on every snapshot — immutable releases still allow title/notes edits. Drift on a published immutable release is repaired with a metadata-only PATCH (the fields GitHub permits); prerelease drift fails as unrepairable. The release must also never become the repository's Latest. - check_index_publication.py gains an `indeterminate` status: malformed HTTP-200 bodies, unparseable JSON, and missing local dist files exit nonzero with refetch/investigate advice — `conflict` (bump + re-tag) is reserved for validated file-set/yank/digest deviations. - finalize validates release asset METADATA before any byte is streamed: malformed entries, duplicate names, and unsafe names fail closed; only the four expected hardcoded names with sane sizes are downloaded by asset ID; unexpected or oversized assets become empty placeholders so the tested reconciler still emits its exact-set / byte-identity classification without unbounded downloads. * fix(release): Administration:read App token, byte-validated guard states, tested asset planner (#356 review round 14) P1: - The immutable-releases policy read requires repository Administration:read, which the workflow GITHUB_TOKEN can never be granted — the round-13 preflight would have failed on every release. finalize now mints a narrowly scoped GitHub App installation token (permission-administration: read, actions/create-github-app-token pinned at v3.2.0) only when a publication is about to happen, and publish_release_body.py routes ONLY the policy read through it via --admin-token-env; every other API call keeps the job token. The test fakes now enforce the credential boundary from both sides. P2: - check_index_publication requires the local publish set to be EXACTLY wheel+sdist: an extra local distribution can no longer slip past the gate into an irreversible upload. - Guard index states are byte-validated (absent | exact | deviating, via the same PR-tested gate run against the job's dist): an exact TestPyPI publication with a missing draft now PROCEEDS to recreate the draft from the preserved original-attempt artifact — consistent with the reconciler's missing-all recovery (cross-module regressions added) — while deviating bytes are an explicit burn. - The Latest assertion fails closed: only an explicit no-Latest 404 passes; auth/rate-limit/transport/malformed responses are errors. - The release object is classified BEFORE the policy setting: an already-public mutable release gets immediate burn guidance, never "still a draft — enable and re-run". - draft flags must be Boolean end to end (helper snapshot check + jq schema checks in both workflow classifications); a missing/None/ string flag can no longer fall through to publication. - The finalize asset trust boundary is PR-tested code: new plan_asset_downloads.py validates metadata (ids/names/sizes, duplicates, unsafe names incl. '.'/'..') and plans downloads only for the four expected names within the size cap; unexpected or oversized assets become placeholders. The workflow consumes the plan. - RELEASING.md github-release paragraph rewritten: the guard blocks and the operator deletes manually — no automatic deletion anywhere; App-token one-time setup documented. * fix(release): mint the policy token only for draft publications (#356 review round 15) `complete` also covers the already-published idempotent rerun, whose helper path performs no Administration:read policy lookup — minting the App token there let missing/rotated App credentials break a harmless rerun and mask the helper's burn guidance for an exact-but-public mutable release. - The finalize reconcile step exports RELEASE_IS_DRAFT (default 0, flipped to 1 only by the Boolean-checked draft classification). - The mint step is gated on `state == 'complete' && RELEASE_IS_DRAFT == '1'`. - New workflow-contract regressions (test_release_workflow_contract) pin the condition on the App-token step itself, the visibility export and its ordering, the token env wiring into the publish helper, and that no other step carries a bare complete-only guard. - RELEASING.md: the App credentials are needed only for draft publications; rotated keys cannot break idempotent reruns.
1 parent d49ecd6 commit d7b41e3

30 files changed

Lines changed: 6107 additions & 143 deletions

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# The image build needs ONLY the producers package and its lock file —
2+
# review round 4 measured a 920 MB context without this.
3+
*
4+
!producers
5+
!deploy/otlp_receiver/requirements.lock
6+
producers/.venv
7+
producers/dist
8+
producers/**/__pycache__
9+
producers/**/*.egg-info

.github/workflows/producers-ci.yml

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@ on:
2020
paths:
2121
- 'producers/**'
2222
- 'plugins/claude_code/**'
23+
- 'deploy/otlp_receiver/**'
24+
- '.dockerignore'
2325
- '.github/workflows/producers-ci.yml'
26+
- '.github/workflows/release-tracing.yml'
2427
pull_request:
2528
branches: [main]
2629
paths:
2730
- 'producers/**'
2831
- 'plugins/claude_code/**'
32+
- 'deploy/otlp_receiver/**'
33+
- '.dockerignore'
2934
- '.github/workflows/producers-ci.yml'
35+
- '.github/workflows/release-tracing.yml'
3036

3137
concurrency:
3238
group: producers-ci-${{ github.ref }}
@@ -41,9 +47,9 @@ jobs:
4147
name: Producers format check
4248
runs-on: ubuntu-latest
4349
steps:
44-
- uses: actions/checkout@v4
50+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
4551

46-
- uses: actions/setup-python@v5
52+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4753
with:
4854
python-version: "3.12"
4955

@@ -64,14 +70,14 @@ jobs:
6470
matrix:
6571
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
6672
steps:
67-
- uses: actions/checkout@v4
73+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
6874

69-
- uses: actions/setup-python@v5
75+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
7076
with:
7177
python-version: ${{ matrix.python-version }}
7278

7379
- name: Install package with dev dependencies
74-
run: pip install -e ".[dev]"
80+
run: pip install -e ".[dev,receiver]" # receiver: protobuf path must run in PR CI, not only at tag time
7581

7682
- name: Run tests
7783
run: pytest --tb=short -q
@@ -81,17 +87,20 @@ jobs:
8187
runs-on: ubuntu-latest
8288
needs: [format, test]
8389
steps:
84-
- uses: actions/checkout@v4
90+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
8591

86-
- uses: actions/setup-python@v5
92+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
8793
with:
8894
python-version: "3.12"
8995

9096
- name: Install build tools
91-
run: pip install build
97+
# Same hash-locked toolchain + no-isolation path the RELEASE
98+
# anchor build uses — a lock/backend incompatibility must fail in
99+
# PR CI, not first at tag time (#356 round 6).
100+
run: pip install --require-hashes -r build-requirements.lock
92101

93-
- name: Build sdist and wheel
94-
run: python -m build
102+
- name: Build sdist and wheel (no isolation — locked backend)
103+
run: python -m build --no-isolation
95104

96105
- name: List wheel/sdist
97106
run: ls -la dist/
@@ -100,15 +109,52 @@ jobs:
100109
# Make `importlib.metadata.version` resolve correctly so the
101110
# plugin manifest gets a real version stamped in, not the
102111
# pyproject fallback.
103-
run: pip install dist/bigquery_agent_analytics_tracing-*.whl
112+
run: pip install --no-deps dist/bigquery_agent_analytics_tracing-*.whl
104113

105114
- name: Build Claude Code plugin artifact
106115
run: python scripts/build_claude_plugin.py
107116

108117
- name: List plugin artifact
109118
run: ls -la dist/
110119

111-
- uses: actions/upload-artifact@v4
120+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
112121
with:
113122
name: producers-dist
114123
path: producers/dist/
124+
125+
image-selftest:
126+
name: Receiver image self-test
127+
runs-on: ubuntu-latest
128+
timeout-minutes: 30
129+
defaults:
130+
run:
131+
# Repo root: the Dockerfile requires the repository root as its
132+
# build context (unlike the other jobs, which run in producers/).
133+
working-directory: .
134+
steps:
135+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
136+
- name: Build the receiver image (hash-locked deps)
137+
run: |
138+
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('producers/pyproject.toml','rb'))['project']['version'])")
139+
docker build -f deploy/otlp_receiver/Dockerfile -t bqaa-pr-selftest .
140+
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
141+
- name: Run the checked-in self-test (same script the release runs)
142+
run: bash deploy/otlp_receiver/selftest.sh bqaa-pr-selftest "$VERSION"
143+
144+
lock-drift:
145+
name: Hash-lock drift check
146+
runs-on: ubuntu-latest
147+
timeout-minutes: 20
148+
defaults:
149+
run:
150+
working-directory: .
151+
steps:
152+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
153+
- name: Regenerate locks with the exact pinned generator
154+
run: bash deploy/otlp_receiver/regen-locks.sh
155+
- name: Locks must match the reviewed files (all three)
156+
run: |
157+
git diff --exit-code deploy/otlp_receiver/pip-tools.lock \
158+
deploy/otlp_receiver/requirements.lock \
159+
producers/build-requirements.lock \
160+
|| (echo "::error::checked-in locks drift from regen-locks.sh output — regenerate and review" && exit 1)

0 commit comments

Comments
 (0)