Commit d7b41e3
authored
* 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
File tree
- .github/workflows
- deploy/otlp_receiver
- producers
- scripts
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
23 | 25 | | |
| 26 | + | |
24 | 27 | | |
25 | 28 | | |
26 | 29 | | |
27 | 30 | | |
28 | 31 | | |
| 32 | + | |
| 33 | + | |
29 | 34 | | |
| 35 | + | |
30 | 36 | | |
31 | 37 | | |
32 | 38 | | |
| |||
41 | 47 | | |
42 | 48 | | |
43 | 49 | | |
44 | | - | |
| 50 | + | |
45 | 51 | | |
46 | | - | |
| 52 | + | |
47 | 53 | | |
48 | 54 | | |
49 | 55 | | |
| |||
64 | 70 | | |
65 | 71 | | |
66 | 72 | | |
67 | | - | |
| 73 | + | |
68 | 74 | | |
69 | | - | |
| 75 | + | |
70 | 76 | | |
71 | 77 | | |
72 | 78 | | |
73 | 79 | | |
74 | | - | |
| 80 | + | |
75 | 81 | | |
76 | 82 | | |
77 | 83 | | |
| |||
81 | 87 | | |
82 | 88 | | |
83 | 89 | | |
84 | | - | |
| 90 | + | |
85 | 91 | | |
86 | | - | |
| 92 | + | |
87 | 93 | | |
88 | 94 | | |
89 | 95 | | |
90 | 96 | | |
91 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
92 | 101 | | |
93 | | - | |
94 | | - | |
| 102 | + | |
| 103 | + | |
95 | 104 | | |
96 | 105 | | |
97 | 106 | | |
| |||
100 | 109 | | |
101 | 110 | | |
102 | 111 | | |
103 | | - | |
| 112 | + | |
104 | 113 | | |
105 | 114 | | |
106 | 115 | | |
107 | 116 | | |
108 | 117 | | |
109 | 118 | | |
110 | 119 | | |
111 | | - | |
| 120 | + | |
112 | 121 | | |
113 | 122 | | |
114 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
0 commit comments