chore(release): 0.2.0 bump, pipx-primary docs, customer-first release notes (#349 PR4)#356
Conversation
caohy1988
left a comment
There was a problem hiding this comment.
Self-review gate: changes required before merge/tag. The current CI checks are green, but the release review found three inherited pre-tag blockers plus two out-of-hunk documentation gaps.
- [P0] Public-registry approval bypass. The unprotected
build-imagejob and protectedpromotejob impersonate the samebqaa-release-publisherservice account. Live IAM read-back shows that account can write bothbqaa-stagingand publicbqaa, while WIF trusts any workflow in this repository. Split staging and promotion service accounts, grant the stager only staging write, and bind the public promoter to therelease-promoteenvironment subject. - [P0] Tag provenance is unenforced. Any
tracing-v*tag triggers the workflow; verification checks only tag text versuspyproject.toml, not that the SHA is on protectedmain. Onlymaincurrently has a ruleset. Add a tag ruleset restricting creation/update/deletion and a main-ancestry check before credentials are minted. - [P1] GitHub release publication is premature.
promotepublishes the draft before the public-default smoke and before the protected PyPI upload. Keep the release draft, publish PyPI after approval, then publish the GitHub release in a rerunnable finalization job. - [P1] The primary no-checkout guide still verifies through a checkout.
deploy/otlp_receiver/README.md:145usesPYTHONPATH=producers/src python3 -m ... verify. Replace it with the installedbqaa-otel verifycommand and shell variables. - [P1] The lifecycle runbook also dry-runs teardown.
producers/RELEASING.md:90omits--confirm, so the approval evidence can pass without deleting or existence-verifying resources.
Inline comments cover the PR-local renderer, generated notes/latest-release behavior, cleanup commands, token substitution, and published links.
| template = pathlib.Path( | ||
| "producers/scripts/release_notes_template.md" | ||
| ).read_text() | ||
| codex_min = "0.142.5" |
There was a problem hiding this comment.
[P2] Add a PR-time renderer test and remove the duplicated compatibility source. This tag-only inline renderer is not executed by Producers CI, so placeholder or str.format drift first fails after the release tag is pushed. Extract it into a checked-in script, test a representative render with no placeholders left, and source the Codex version from the canonical compatibility metadata instead of repeating 0.142.5 here.
| @@ -264,9 +282,7 @@ jobs: | |||
| draft: true | |||
| generate_release_notes: true | |||
There was a problem hiding this comment.
[P1] Generic generated notes are wrong for the repository-wide tag stream. A live releases/generate-notes preview for tracing-v0.2.0 selected v0.4.0 as the base and included unrelated dashboard, Dependabot, skill-lab, and demo PRs. Disable generic notes and add a curated tracing changelog covering #316, #324, #317, #340, #342, and #343, or use a tracing-only filtered generator.
| body: | | ||
| **Receiver image (pinned by digest):** | ||
| `${{ env.PUBLIC_IMAGE }}:${{ needs.verify.outputs.version }}@${{ needs.build-image.outputs.digest }}` | ||
| body_path: release_body.md |
There was a problem hiding this comment.
[P1] Preserve the SDK release as the repository Latest release. softprops/action-gh-release leaves make_latest at the GitHub API default, which is true for a newly published release. This tracing 0.2.0 release would displace the SDK v0.4.0 release. Set make_latest: false and preserve it when the draft is finalized.
| bqaa-otel bootstrap --project my-proj --dataset agent_analytics --region us-central1 --execute | ||
|
|
||
| # Clean removal later (dry-run by default; consumes the written inventory.json): | ||
| bqaa-otel teardown --project my-proj --dataset agent_analytics |
There was a problem hiding this comment.
[P1] This is only a dry run, not clean removal. The CLI returns success without deleting or probing anything unless --confirm is present. Show both commands: this preview first, then the same teardown with --confirm so customers actually remove resources and receive existence verification.
| ``` | ||
|
|
||
| Enterprise admins deploying the OTel-native receiver: see | ||
| [deploy/otlp_receiver/README.md](../deploy/otlp_receiver/README.md) — |
There was a problem hiding this comment.
[P1] This relative link breaks in the PyPI long description. producers/README.md is the package metadata README, so ../deploy/... is no longer rooted in the GitHub source tree when rendered on PyPI. Use an absolute GitHub URL to the primary deployment guide.
|
|
||
| # 3. Distribute the generated config artifacts (Claude Code managed | ||
| # settings via admin console/MDM; Codex config.toml via managed | ||
| # dotfiles) — written to --out with a do-not-commit token warning. |
There was a problem hiding this comment.
[P1] Add the token retrieval and replacement step before distribution. Bootstrap writes literal <token> placeholders, and Codex does not expand environment variables in headers. As written, an admin can distribute configs that always receive 401s. Show the gcloud secrets versions access command and require replacing the placeholder before rollout.
| --project $PROJECT --dataset $DATASET | ||
|
|
||
| # 5. Clean removal when done (dry-run by default, existence-verified): | ||
| bqaa-otel teardown --project $PROJECT --dataset $DATASET |
There was a problem hiding this comment.
[P1] The advertised cleanup never executes. Without --confirm, teardown only prints its plan and returns success; it performs no deletion or post-delete verification. Keep this as the preview, then show an explicit second command with --confirm.
| ## Evaluation path | ||
|
|
||
| The rehearsed end-to-end demo (both products, real telemetry, privacy | ||
| proofs) lives in [`demo/hero_story/`](../../demo/hero_story/). |
There was a problem hiding this comment.
[P1] This release-page link resolves to a 404. GitHub leaves the relative href unchanged, so from /releases/tag/tracing-v0.2.0 it resolves to /BigQuery-Agent-Analytics-SDK/demo/hero_story/, not a repository tree URL. Render an absolute tag-specific URL such as https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/tree/tracing-v{version}/demo/hero_story.
…rections (GoogleCloudPlatform#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 (GoogleCloudPlatform#316, GoogleCloudPlatform#324, GoogleCloudPlatform#317, GoogleCloudPlatform#340, GoogleCloudPlatform#342, GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Fresh full review — changes required before merge/tag
I re-reviewed head 240c1c6 from scratch, including live IAM, WIF/environment protections, Artifact Registry immutability, GitHub release settings, rerun behavior, the clean release-test dependency set, renderer output, and issue #349. Current CI is green, but the following findings remain.
P1 — release blockers
- Promoter cannot read private staging. Live IAM gives
bqaa-release-promoterwriter on publicbqaa, but no role onbqaa-stagingand no project-level grant.crane copytherefore cannot pull its source. Grant the promoterroles/artifactregistry.readeron staging. - Claude config retains
<token>. Bootstrap emits the placeholder in bothcodex.config.tomlandclaude-code.managed-settings.json; the notes edit only Codex and then tell admins to distribute both. Replace both, fail on an empty token, and assert neither file retains the placeholder. - The release test silently skips the documented protobuf path.
pip install -e ".[dev]"omitsopentelemetry-proto; the only protobuf receiver test usespytest.importorskip. A clean reproduction is 389 passed, 5 skipped, with the fifth skip being protobuf—not 390/4. Install.[dev,receiver], make that import mandatory, and send a real protobuf request through the built container. - Reruns reuse immutable identities.
github.run_idis unchanged across attempts, while the live staging repository enforces immutable tags;release-tracing-distis also reused. Includegithub.run_attemptin the candidate tag and artifact identity, propagate the exact artifact name, and make public promotion idempotently accept only an existing matching digest. - A full rerun can mutate an already-published GitHub release. Immutable releases are live-verified disabled.
softprops/action-gh-releasereuses an existing published release and defaults to overwriting its body/assets. Enable immutable releases, fail if the release is already public, and setoverwrite_files: false. - Credentialed third-party actions use mutable refs. GCP auth, GitHub Release, checkout/setup/download/upload actions, and PyPI publication use movable tags/branches; repository SHA-pinning enforcement is disabled. Pin all actions to reviewed full SHAs and enable enforcement.
- The TestPyPI lifecycle install mixes indexes unsafely. pip gives
--index-urland--extra-index-urlno priority. Install dependencies from PyPI, download the exact TestPyPI candidate with--no-deps, verify its SHA256, then install that local artifact with--no-deps. - Partial PyPI publication can strand release assets in a draft.
finalizeis skipped whenever the publish action fails, even if one distribution already became public. Add analways()reconciliation job that queries the exact PyPI file set and four GitHub assets, finalizes only on complete state, and emits the yank/version-burn recovery for partial state.
P2 — hardening and contract corrections
- Scope the promoter WIF identity to
job_workflow_refplus the tracing tag/ref, not only the reusable environment subject; also restrict the environment to tracing tags and prevent self-review. - Define
PROJECT,DATASET, and the receiverURLbefore the customer commands. Bootstrap prints a URL but does not export$URL. - Remove the duplicate public image coordinate from the renderer; pass the workflow's authoritative
$PUBLIC_IMAGEthrough a validated CLI argument. - The ancestor check accepts stale or reverted protected-main commits. Require the exact approved release commit or an equivalently protected release ref, not merely any historical ancestor.
- Update the workflow header/job name,
RELEASING.md, PR description, and #349: they still describe generic notes orpromotepublishing the release and omit the actual finalization/reconciliation state.
Issue #349 gate
Keep #349 open. Besides these code findings, the trusted publishers/2FA, security artifacts and package-level hardening guidance, real lifecycle evidence, post-release smoke, public-pull proof, source-build proof, and second owner remain outstanding.
Because this PR is authored by the reviewing account, GitHub does not allow a formal REQUEST_CHANGES state; this comment is the equivalent merge-gate verdict.
| service_account: ${{ env.RELEASE_SA }} | ||
| # The promoter: writable ONLY on the public repo, impersonable | ||
| # ONLY by this environment-protected job's OIDC subject. | ||
| service_account: ${{ env.PROMOTER_SA }} |
There was a problem hiding this comment.
[P1] The promoter cannot read the source repository. Live IAM gives this SA roles/artifactregistry.writer only on public bqaa; private bqaa-staging has only the stager writer, and the promoter has no project-level role. The following crane copy must pull the staging manifest/blobs and will fail. Grant this SA roles/artifactregistry.reader on bqaa-staging while retaining public-only write.
| workload_identity_provider: ${{ env.WIF_PROVIDER }} | ||
| service_account: ${{ env.RELEASE_SA }} | ||
| # The promoter: writable ONLY on the public repo, impersonable | ||
| # ONLY by this environment-protected job's OIDC subject. |
There was a problem hiding this comment.
[P2] The WIF binding is environment-scoped, not workflow/ref-scoped. Any workflow in the repository can request the release-promote environment and receive this subject after approval or admin bypass. Map and condition job_workflow_ref plus the tracing tag/ref, restrict the environment deployment policy to tracing-v*, and enable self-review prevention.
| # a literal <token> placeholder means every export gets a 401): | ||
| TOKEN=$(gcloud secrets versions access latest \ | ||
| --secret=bqaa-otlp-token --project $PROJECT) | ||
| sed -i.bak "s/<token>/${{TOKEN}}/g" codex.config.toml # never commit this file |
There was a problem hiding this comment.
[P1] This fills only the Codex artifact. bootstrap --source claude-code,codex also writes <token> into claude-code.managed-settings.json, and the next instruction distributes that unchanged file, producing Authorization: Bearer <token> and 401s. Fail unless TOKEN is non-empty, replace both files, and assert neither still contains the placeholder.
| # repository admins by the "tracing release tags" ruleset. | ||
| run: | | ||
| git fetch origin main --quiet | ||
| if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then |
There was a problem hiding this comment.
[P2] is-ancestor proves historical membership, not the approved release state. A stale or later-reverted commit remains an ancestor of protected main forever. Require the tag to equal the explicitly approved release commit/current protected release ref, or record and verify an equivalent immutable release-commit approval.
| tag_name: ${{ github.ref_name }} | ||
| draft: true | ||
| generate_release_notes: true | ||
| make_latest: false |
There was a problem hiding this comment.
[P1] Guard reruns from mutating an already-published release. Immutable releases are currently disabled, and softprops/action-gh-release defaults overwrite_files to true. On a full rerun it finds the published release, preserves draft: false, overwrites the body, and deletes/reuploads matching assets before TestPyPI fails. Enable immutable releases, fail early when the tag already has a public release, and set overwrite_files: false.
| @@ -340,7 +369,9 @@ jobs: | |||
| - uses: google-github-actions/auth@v2 | |||
There was a problem hiding this comment.
[P1] Pin credentialed actions to full commit SHAs. This movable @v2 reference executes with id-token: write; the PyPI publisher and GitHub Release action are also movable, and live repository settings do not require SHA pinning. Pin every action in this release workflow and enable repository SHA enforcement.
| # approvals (release-promote, pypi) and the PyPI upload succeeded. | ||
| # --latest=false preserves the SDK's vX.Y.Z release as the | ||
| # repository's Latest release. | ||
| needs: [verify, publish-pypi] |
There was a problem hiding this comment.
[P1] Reconcile external PyPI state even when publication reports failure. Twine can upload one distribution and fail on the next; default needs behavior then skips this job, leaving a public wheel/sdist subset while the plugin and checksums remain draft-only. Add an if: always() reconciliation job that verifies the exact PyPI file set and release assets before publishing or emitting a yank/version-burn recovery.
| TOKEN=$(gcloud secrets versions access latest \ | ||
| --secret=bqaa-otlp-token --project $PROJECT) | ||
| BQAA_OTLP_TOKEN=$TOKEN bqaa-otel verify \ | ||
| --endpoint $URL --project $PROJECT --dataset $DATASET |
There was a problem hiding this comment.
[P2] These variables are undefined in the primary guide. The deployment examples use literal my-proj/agent_analytics, then verification switches to $PROJECT, $DATASET, and $URL; bootstrap prints the receiver URL but does not export it. Define/export all three or derive URL explicitly before this command.
| sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "src")) | ||
| from bigquery_agent_analytics_tracing.otlp import config_artifacts | ||
|
|
||
| PUBLIC_IMAGE = "us-docker.pkg.dev/bqaa-releases/bqaa/otlp-receiver" |
There was a problem hiding this comment.
[P2] This creates a second source of truth for the public coordinate. Injection and promotion use workflow PUBLIC_IMAGE, while release notes use this constant; either can drift and the renderer test will bless its own copy. Add a validated --public-image input and pass ${PUBLIC_IMAGE} from the workflow.
| ) | ||
|
|
||
|
|
||
| def test_lifecycle_commands_include_token_fill_and_confirmed_teardown(): |
There was a problem hiding this comment.
[P2] This test can pass while the executable lifecycle is still broken. It checks disconnected substrings, so it missed that token replacement covers only Codex and that $URL is never defined. Assert the complete token-fill commands for both generated files, the non-empty/no-placeholder guard, URL initialization, and the exact confirmed teardown command.
… honest gates (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Fresh full review at 952f079: changes required before merge/tag
The 20 PR checks are green, but they do not execute the tag-only release workflow. Fresh local verification produced 392 passing tests with four expected live-E2E skips, while the exact no-ADC container startup failed with DefaultCredentialsError.
P1 release blockers
- The pre-push HTTP self-test cannot boot without ADC or a Pub/Sub emulator.
- Downstream-only reruns increment
github.run_attemptand request an artifact the successful build job never uploaded. - A full rerun can update the draft body while retaining same-named assets from an earlier attempt.
- Finalization treats two PyPI URLs and four GitHub assets as proof of identity, so stale or unrelated bytes can publish.
- The published-release lookup fails open on API, authentication, network, or parsing errors.
P2 follow-ups
- The self-test proves only bad-token rejection, not successful authenticated protobuf decoding/publication.
- PR CI still installs
.[dev], leaving the receiver/protobuf path tag-only. - The TestPyPI checksum step prints a digest instead of mechanically comparing it, and the later verification section still mixes indexes.
- The rendered token-fill shell can continue after missing/unreadable file errors; its tests inspect strings rather than execute the lifecycle snippet.
- The primary deployment guide still uses undefined
PROJECT,DATASET, andURL. - Workflow/runbook metadata still describes the old publication contract and retains an unnecessary
contents: writepermission onpromote. - The released container remains non-reproducible because
python:3.12-slimand pip-resolved runtime/build dependencies are mutable.
The live split IAM/WIF, environment/tag protections, action SHA pins, exact-main provenance, and accepted single-maintainer self-review policy were rechecked and are not blockers in this round. Because the authenticated account authored this PR, GitHub cannot record a formal REQUEST_CHANGES; this COMMENTED review is the equivalent merge-gate verdict.
| # only captured for an image that passed. | ||
| run: | | ||
| IMG="${STAGING_IMAGE}:${{ needs.verify.outputs.version }}-candidate.${{ github.run_id }}.${{ github.run_attempt }}" | ||
| docker run -d --name selftest -p 18080:8080 \ |
There was a problem hiding this comment.
[P1] Make this self-test boot without production credentials. The container starts before google-github-actions/auth and receives neither ADC nor PUBSUB_EMULATOR_HOST. make_app() eagerly constructs PubSubPublisher(), whose PublisherClient() raises DefaultCredentialsError before gunicorn can serve the request; I reproduced that failure at this exact head. Start a Pub/Sub emulator and point the container at it, inject a credential-free publisher for the test, or defer publisher construction until after authentication.
| -e BQAA_OTLP_TOKEN=selftest-token \ | ||
| -e BQAA_OTLP_MAIN_TOPIC=projects/selftest/topics/none \ | ||
| "$IMG" | ||
| for i in $(seq 1 20); do |
There was a problem hiding this comment.
[P2] A 401-only probe does not exercise the advertised OTLP path. Even after the startup failure is fixed, a reject-all implementation can pass this test without protobuf decoding or Pub/Sub publication ever working. Keep the wrong-token 401 assertion, then send a valid authenticated protobuf payload against an emulator/stub and require the expected successful response and publish.
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | ||
| with: | ||
| name: release-tracing-dist | ||
| name: release-tracing-dist-a${{ github.run_attempt }} |
There was a problem hiding this comment.
[P1] Consume the artifact identity emitted by the producing job. GitHub increments github.run_attempt when only a failed downstream job is rerun, but the already-successful build job is not recreated. That retry asks for release-tracing-dist-a2 although only ...-a1 exists, so the documented recovery path fails before publication. Export the actual artifact name or ID from build and consume that output in every downloader instead of reconstructing it from the consumer's current attempt.
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --json isDraft -q .isDraft 2>/dev/null | grep -q false; then |
There was a problem hiding this comment.
[P1] Fail closed when release state is indeterminate. Redirecting errors and using the lookup only as an if condition makes authentication, rate-limit, network, API, and parsing failures indistinguishable from an absent/draft release. If the API recovers for the following action, an already-published release can still be modified. Capture the response/status, allow only an explicit 404 or a confirmed draft, and fail every other state.
| draft: true | ||
| generate_release_notes: true | ||
| make_latest: false | ||
| overwrite_files: false |
There was a problem hiding this comment.
[P1] overwrite_files: false can preserve a mixed-attempt draft. On a full rerun, the pinned release action updates the existing draft body but skips same-named assets. The body can therefore describe the new image digest while the wheel, sdist, plugin, and checksums remain from the previous attempt. Reject a pre-existing draft unless every asset name/hash and rendered digest match the current immutable candidate, or resume exclusively from the original candidate manifest.
| ') | ||
| ASSETS=$(gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --json assets -q '.assets | length' || echo 0) | ||
| echo "PyPI files for ${VERSION}: ${PYPI_FILES} (want 2) | GitHub release assets: ${ASSETS} (want 4)" | ||
| if [ "${PYPI_FILES}" = "2" ] && [ "${ASSETS}" = "4" ]; then |
There was a problem hiding this comment.
[P1] Counts are not artifact identity. Because finalize runs after failed/skipped publication too, any version with two PyPI URLs and any draft with four assets is marked complete. Verify the exact expected wheel/sdist/plugin/checksum filenames, compare PyPI and downloaded release-asset SHA-256 values with the current candidate manifest, reject yanked files, and revalidate the public image/body digest before making the release visible. A failed upload is idempotent only when byte identity is proven.
| python -m venv gate && source gate/bin/activate | ||
| pip download --no-deps --index-url https://test.pypi.org/simple/ \ | ||
| --dest /tmp/gate bigquery-agent-analytics-tracing==${VERSION} | ||
| sha256sum /tmp/gate/*.whl # MUST match the draft release SHA256SUMS |
There was a problem hiding this comment.
[P2] Make the TestPyPI integrity gate mechanical. This command only prints a checksum; it never compares it with the draft release's SHA256SUMS, and /tmp/gate may contain stale or multiple wheels. Use a fresh temporary directory, download the exact expected filename plus the checksum manifest, run sha256sum -c (or an equivalent helper), and install only that verified file. Also remove the later mixed --index-url/--extra-index-url verification recipe so there is one safe path.
| for f in codex.config.toml claude-code.managed-settings.json; do | ||
| sed -i.bak "s/<token>/${{TOKEN}}/g" "$f" && rm "$f.bak" # never commit these | ||
| done | ||
| grep -l '<token>' codex.config.toml claude-code.managed-settings.json \ |
There was a problem hiding this comment.
[P2] File errors can bypass this token-fill guard. sed ... && rm and grep ... && abort place failing commands in conditional lists; a missing or unreadable artifact can return an error and the snippet can continue toward distribution. Require both files up front, run with strict shell error handling, perform replacement without exposing the token in argv where practical, and use an explicit if grep -q ...; then abort; fi per file. Execute missing-file and surviving-placeholder cases in tests instead of checking only substrings.
| TOKEN=$(gcloud secrets versions access latest \ | ||
| --secret=bqaa-otlp-token --project $PROJECT) | ||
| BQAA_OTLP_TOKEN=$TOKEN bqaa-otel verify \ | ||
| --endpoint $URL --project $PROJECT --dataset $DATASET |
There was a problem hiding this comment.
[P2] The primary no-checkout path still leaves these variables undefined. Earlier commands use literal my-proj and agent_analytics, and bootstrap prints the receiver URL without exporting $URL. Copying this block expands PROJECT, DATASET, and URL to empty strings. Define/export them before the deployment sequence or use the same concrete values and derive the Cloud Run URL here.
| # publish-pypi - PyPI (gated by the `pypi` environment). Version-burn | ||
| # rule: a candidate that failed the gate burns its | ||
| # version; never re-tag — bump and rebuild. | ||
| # finalize - always(): reconciles PyPI files + release assets; |
There was a problem hiding this comment.
[P2] Finish updating the release contract metadata and permissions. The earlier workflow header still says the draft is published by promote, promote is still named Promote image + publish release, and RELEASING.md still claims generate_release_notes: true. Rename/update those descriptions, and remove contents: write from promote because only finalize now needs release-write permission.
…fecycle tests (GoogleCloudPlatform#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).
caohy1988
left a comment
There was a problem hiding this comment.
Round-4 fresh review at ef56f45: changes required before merge/tag
Fresh verification: all 20 PR checks pass; local suite is 395 passed with four expected live-E2E skips; the exact image rebuild produced bad-token 401, authenticated protobuf 200, package version 0.2.0, and both factories importable. Live split IAM/WIF, tag policies, required reviewers, and immutable staging/public tags also read back correctly.
P1 release blockers
- Finalization trusts the mutable draft's own manifest, does not require every expected file to appear in that manifest, and does not reject extra GitHub/PyPI files.
- The
id-token: writeimage job installs/imports mutable host packages after starting the tested container but before pushing its tag, so host code can retag an untested image. - The mutable Pub/Sub emulator image shares host networking and can spoof both receiver responses.
- Emulator readiness has no bound, curl timeout, cleanup trap, or job timeout.
- Promotion treats every
crane digestfailure as an absent public tag and enters the mutating copy path.
P2 follow-ups
- The rewritten image gate dropped packaged-version and consumer-factory checks, and the Docker self-test still is not PR-executed.
- Token-fill leaves both secret-bearing configuration files at the normal
0644mode. - The crane access token remains in a process argument instead of stdin.
- Image and package build dependencies remain mutable;
pip freezerecords drift but does not prevent it. - Workflow synopsis, runbook candidate-tag/self-test description, PR body, and contributor install command remain stale.
- There is no
.dockerignore; the local review sent a 920 MB context. - The PyPI long-description README retains broken relative repository links, and the manual plugin fallback still uses
VERSION=0.1.0.
The documented single-maintainer self-review choice was treated as an accepted residual rather than re-counted as a blocker. Because the authenticated account authored the PR, GitHub cannot record a formal REQUEST_CHANGES; this COMMENTED review is the equivalent merge-gate verdict.
| for f in "$WHEEL" "$SDIST" "$PLUGIN" SHA256SUMS; do | ||
| test -f "$f" || { echo "state=partial" >> "$GITHUB_ENV"; echo "missing asset $f"; exit 0; } | ||
| done | ||
| sha256sum -c SHA256SUMS || { echo "state=partial" >> "$GITHUB_ENV"; echo "asset bytes do not match the attested SHA256SUMS"; exit 0; } |
There was a problem hiding this comment.
[P1] Anchor reconciliation to the immutable build artifact and require exact sets. This checks only entries that SHA256SUMS happens to list. I reproduced a present plugin omitted from the manifest while sha256sum -c still exited 0. The later PyPI loop likewise checks the expected wheel/sdist but accepts extra distributions, and the release download accepts extra assets. Because the manifest, plugin, and body all come from the same mutable draft, replacing them together is self-consistent but not current-build identity. Propagate the upload-artifact ID/digest from build, download that artifact in finalize, require exact manifest/release/PyPI filename sets, compare bytes and the freshly rendered body to that anchor, and add hermetic state-matrix tests.
| sleep 2 | ||
| done | ||
| test "$CODE" = "401" || (docker logs selftest; echo "::error::bad-token probe got ${CODE:-none}, want 401" && exit 1) | ||
| pip install --quiet opentelemetry-proto protobuf |
There was a problem hiding this comment.
[P1] Do not install or import mutable host code between testing and push. This job already has id-token: write, and the receiver container was started from the candidate tag before this unpinned install/import. Compromised package code can request the job's OIDC token, invoke the Docker-capable host, and retag the candidate while the already-running original container still returns 200; the later push then publishes untested bytes. Generate the payload inside the built image or use a checked-in binary fixture, and verify the local image ID immediately before push.
|
|
||
| - uses: google-github-actions/auth@v2 | ||
| IMG="${STAGING_IMAGE}:${{ needs.verify.outputs.version }}-candidate.${{ github.run_id }}.${{ github.run_attempt }}" | ||
| docker run -d --name pubsub-emu --network host \ |
There was a problem hiding this comment.
[P1] Pin and isolate the emulator used as the release oracle. google-cloud-cli:emulators is a mutable tag and runs on the host network before the receiver starts. A compromised image can bind port 18080 itself, return the expected 401/200 responses, and hide a receiver container that failed to boot. Pin the emulator by digest, put emulator and receiver on a private bridge network, and expose only the receiver port to the host.
| docker run -d --name pubsub-emu --network host \ | ||
| gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators \ | ||
| gcloud beta emulators pubsub start --host-port=0.0.0.0:8085 | ||
| until curl -s http://localhost:8085 > /dev/null; do sleep 2; done |
There was a problem hiding this comment.
[P1] Bound readiness and clean up every exit path. If the emulator starts but never becomes healthy, this loop runs until GitHub's multi-hour job limit. Failures before the explicit docker rm also leave both containers running for the rest of the job. Use finite retries with curl connection/request timeouts, print emulator logs on failure, install a cleanup trap immediately after container creation, and set timeout-minutes on the job.
| crane copy \ | ||
| "${STAGING_IMAGE}@${{ needs.build-image.outputs.digest }}" \ | ||
| "${PUBLIC_IMAGE}:${{ needs.verify.outputs.version }}" | ||
| if EXISTING=$(crane digest "${PUBLIC_IMAGE}:${{ needs.verify.outputs.version }}" 2>/dev/null); then |
There was a problem hiding this comment.
[P1] Fail closed on indeterminate registry state. Every crane digest failure, including authentication, rate limiting, registry errors, and network loss, enters the crane copy branch as if the tag were explicitly absent. Live immutable tags currently prevent an overwrite, but this still turns a read failure into a mutation attempt and breaks idempotent recovery. Capture stderr/status, copy only for an explicit manifest-not-found response, and fail every other state. Also send the access token through --password-stdin instead of the process argument vector.
| -t "${STAGING_IMAGE}:${{ needs.verify.outputs.version }}-candidate.${{ github.run_id }}" . | ||
| -t "${STAGING_IMAGE}:${{ needs.verify.outputs.version }}-candidate.${{ github.run_id }}.${{ github.run_attempt }}" . | ||
|
|
||
| - name: Self-test image BEFORE anything is pushed |
There was a problem hiding this comment.
[P2] Restore the complete image contract and execute it before tags. The previous gate asserted installed package version and imported both Cloud Run factories; this rewrite tests only the receiver role. I independently verified the current image is 0.2.0 and both factories import, but those properties can regress without failing this workflow step, and the full Docker path remains tag-only. Restore the metadata/factory assertions, probe the consumer role where practical, extract the self-test into a checked-in script, and run it from PR CI as well as the release workflow.
| python3 - "$f" << 'FILL_EOF' | ||
| import os, pathlib, sys | ||
| path = pathlib.Path(sys.argv[1]) | ||
| path.write_text(path.read_text().replace("<token>", os.environ["TOKEN"])) |
There was a problem hiding this comment.
[P2] Restrict the files before writing the bearer token. The generated artifacts use the normal umask; executing this block locally left both token-bearing files at 0644, readable by other users on a multi-user administration host. Set umask 077 before generation or chmod 600 both files before replacement, and assert the modes in the executed-snippet test.
| # google-cloud-pubsub, opentelemetry-proto). | ||
| COPY producers /src/producers | ||
| RUN pip install --no-cache-dir "/src/producers[receiver]" | ||
| RUN pip install --no-cache-dir "/src/producers[receiver]" \ |
There was a problem hiding this comment.
[P2] pip freeze audits dependency drift after it happens; it does not prevent it. The base image is now digest-pinned, but the build backend and every receiver dependency are still resolved from mutable lower bounds. A compromised or incompatible upstream release changes production bytes at the same source commit. Build reviewed hash-locked constraints, install with --require-hashes, and similarly lock the wheel/sdist build toolchain.
| # publish-pypi - PyPI (gated by the `pypi` environment). Version-burn | ||
| # rule: a candidate that failed the gate burns its | ||
| # version; never re-tag — bump and rebuild. | ||
| # finalize - always(): reconciles PyPI files + release assets; |
There was a problem hiding this comment.
[P2] Finish synchronizing the release contract documentation. The earlier synopsis still says the self-test checks package version/factory imports and that promote publishes the draft. RELEASING.md still documents <version>-candidate.<run_id> instead of <run_id>.<run_attempt>, and the PR body still describes the pre-round-3 401-only/count-based implementation. Update these together, plus the contributor install command to .[dev,receiver], so the runbook matches the code operators will execute.
…ocked image (GoogleCloudPlatform#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.
| name: producers-dist | ||
| path: producers/dist/ | ||
|
|
||
| image-selftest: |
There was a problem hiding this comment.
[P1] The new image self-test is not selected for its own inputs. Both workflow path lists omit .dockerignore and deploy/otlp_receiver/**, so a Dockerfile, dependency-lock, or self-test-only PR skips Producers CI and the next execution is at release-tag time. Add those inputs to both push.paths and pull_request.paths; include .github/workflows/release-tracing.yml too if changes to the shared release invocation should exercise this gate.
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| case "$state" in |
There was a problem hiding this comment.
[P1] Fail closed on every reconciliation state. reconcile_release.py can return invalid-anchor, but this dispatcher handles only complete, unpublished, and partial. I reproduced state=invalid-anchor falling through with exit code 0, which can leave PyPI public, the GitHub release draft, and the release run green. Add an explicit invalid-anchor failure plus a *) catch-all that exits nonzero, and test the state-to-workflow mapping.
| rec.body.string_value = 'release-selftest' | ||
| sys.stdout.buffer.write(req.SerializeToString()) | ||
| " > "${TMPDIR:-/tmp}/bqaa-selftest-payload.bin" | ||
| GOOD=$(curl -s --connect-timeout 2 --max-time 10 -o /dev/null -w '%{http_code}' \ |
There was a problem hiding this comment.
[P1] HTTP 200 does not prove a successfully decoded export. The receiver can return 200 with published=0, and per-record decode failures can return 200 with dead_lettered>0; this curl discards the JSON body and checks only the status. I reproduced ReceiverResult(status=200, published=0, dead_lettered=0). Capture and parse the response body, then require published > 0 and dead_lettered == 0 before emitting SELFTEST_IMAGE_ID.
| COPY producers /src/producers | ||
| RUN pip install --no-cache-dir "/src/producers[receiver]" | ||
| RUN pip install --no-cache-dir --require-hashes -r /src/requirements.lock \ | ||
| && pip install --no-cache-dir --no-deps /src/producers \ |
There was a problem hiding this comment.
[P1] The runtime hash lock does not cover the PEP 517 build backend. pip install --no-deps /src/producers still creates an isolated build environment and downloads the unpinned hatchling declared in pyproject.toml; the successful CI log shows Installing build dependencies after the hash-locked install. Hash-lock hatchling and its build dependencies, install them explicitly, then install the local package with --no-build-isolation --no-deps.
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| needs: [verify, build-image] | ||
| outputs: |
There was a problem hiding this comment.
[P1] The immutable artifact anchor is still produced by mutable PyPI code. This job installs build without a version or hash, python -m build resolves an unpinned isolated hatchling, and the later wheel install resolves runtime dependencies before the plugin tarball is created. A compromised upstream package can therefore alter the bytes that become the trusted anchor. Use a reviewed hash-locked build toolchain with python -m build --no-isolation, and install the built wheel with --no-deps for the metadata step.
| --dir release --pattern "*" 2> download.err; then | ||
| echo "state=unpublished" >> "$GITHUB_ENV" | ||
| echo "release/assets not downloadable: $(head -1 download.err)" | ||
| exit 0 |
There was a problem hiding this comment.
[P2] Lookup failures are being classified as publication absence. Any GitHub release download failure becomes unpublished, while any PyPI 404, authentication error, rate limit, 5xx, malformed response, or network failure becomes {} and receives the same recovery advice. This stays fail-closed, but it sends the operator down the wrong recovery path. Distinguish an explicit 404 from an indeterminate lookup and fail with surface-specific diagnostics.
…spatch (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Round-6 fresh review at 861c8d4: changes required before merge/tag
The 21 visible checks are green, and fresh local verification is 409 passed with four expected live-E2E skips. The locked image self-test proves a real decoded publication. Four release-integrity blockers still remain:
- Finalization anchors assets but publishes the mutable draft body unchanged.
- The hash-locked trusted-anchor build command is still tag-only; PR CI exercises a different build path.
- Malformed PyPI success responses are converted into confirmed absence.
- A missing GitHub draft short-circuits the PyPI lookup and hides a possible cross-surface partial state.
P2 follow-ups are inline: the missing build-toolchain input, transport-error diagnostics, the stale locking comment, and the build toolchain/source retained in the runtime image.
Release metadata also needs one synchronized cleanup: the PR description still says two rounds and 26 findings, claims eight renderer tests, and describes the older 401-only gate; the workflow synopsis still says promote publishes the draft. Refresh those before merge so the permanent release record matches this head.
Separate pre-tag note from live read-back: Artifact Registry vulnerability scanning is disabled. Issue #349 asks for scan evidence if practical, so enable it or explicitly record the deferral before closing the release issue.
Because the authenticated account authored this PR, GitHub cannot record a formal REQUEST_CHANGES review. This COMMENTED review is the equivalent merge-gate verdict.
| EXIT_CODE=$(echo "$VERDICT" | cut -d'|' -f2) | ||
| MESSAGE=$(echo "$VERDICT" | cut -d'|' -f3-) | ||
| if [ "$PUBLISH" = "1" ]; then | ||
| gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ |
There was a problem hiding this comment.
[P1] Re-anchor the customer-facing release body at finalization. Asset reconciliation never reads the draft body, and this command publishes whatever body exists after the manual approval pause. A contents-write principal or an accidental draft edit can replace the documented commands or digest while every artifact check still reports complete. Re-render release_body.md here from the protected checkout using VERSION, PUBLIC_IMAGE, and the digest extracted from the anchor wheel, then publish atomically with gh release edit --notes-file release_body.md --draft=false --latest=false. Reset other mutable release metadata here as appropriate.
| # toolchain must not resolve mutable upstream code. hatchling + | ||
| # build come from the reviewed lock; --no-isolation below makes | ||
| # the locked backend authoritative. | ||
| run: pip install --require-hashes -r build-requirements.lock |
There was a problem hiding this comment.
[P1] Execute this exact trusted-anchor build path in PR CI. The Producers build job still installs mutable build and runs an isolated python -m build; the Docker gate installs this lock but never invokes the build frontend. A lock or backend incompatibility can therefore merge green and first fail after the release tag is pushed. Make Producers CI install build-requirements.lock with --require-hashes, run python -m build --no-isolation, and install the wheel with --no-deps, matching this job.
| args = parser.parse_args(argv) | ||
| try: | ||
| pypi = json.loads(args.pypi_json.read_text()) | ||
| except ValueError: |
There was a problem hiding this comment.
[P1] Do not convert a malformed HTTP-200 response into confirmed absence. Truncated or invalid PyPI JSON lands here, becomes an empty object, and is classified as unpublished even though only an explicit 404 proves absence. That can give rerun advice after a partial upload instead of treating the lookup as indeterminate. Let decoding fail nonzero or introduce an explicit invalid-response state, and add CLI-level tests for malformed, truncated, and unexpected success bodies.
| if ! gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ | ||
| --dir release --pattern "*" 2> download.err; then | ||
| if grep -qi "release not found" download.err; then | ||
| echo "state=unpublished" >> "$GITHUB_ENV" |
There was a problem hiding this comment.
[P1] Missing GitHub release is not the same state as unpublished on PyPI. This exits before querying PyPI and feeds dispatch a state whose recovery says the PyPI upload did not happen. If the draft is deleted after a complete or partial PyPI upload, finalization hides the actual cross-surface partial state and recommends the wrong operation. Classify GitHub-release existence and PyPI state independently, then dispatch a distinct missing-release or combined partial state.
| # This file is autogenerated by pip-compile with Python 3.12 | ||
| # by the following command: | ||
| # | ||
| # pip-compile --generate-hashes --output-file=producers/build-requirements.lock --strip-extras build-toolchain.in |
There was a problem hiding this comment.
[P2] Check in the lock input used by this regeneration command. build-toolchain.in does not exist at the repository root or under producers, so the reviewed lock cannot be regenerated from repository state. Add the exact input file and make the command copy-pastable with its real path, preferably through a checked-in script that also pins the base-image digest used to compile it.
| echo "::error::GitHub release lookup indeterminate: $(head -1 download.err)" | ||
| exit 1 | ||
| fi | ||
| HTTP=$(curl -s -o pypi.json -w '%{http_code}' \ |
There was a problem hiding this comment.
[P2] Report PyPI transport failures explicitly. This assignment runs under set -e, so a DNS, TLS, or connection failure exits before the HTTP-state branch; curl -s also suppresses the reason. Wrap the request in an explicit transport-error conditional, use curl error output plus bounded connect/request timeouts, and reserve the following branch for actual HTTP 200 and 404 responses.
|
|
||
| FROM python:3.12-slim | ||
| # Pinned by digest for release reproducibility (python:3.12-slim as of | ||
| # 2026-07-10). Full hash-locked pip dependencies are follow-up work; the |
There was a problem hiding this comment.
[P2] Update this comment to the contract the Dockerfile now enforces. Runtime dependencies and the PEP 517 backend are both installed from hash-locked files below, so calling that work a follow-up and describing freeze as the control is now false. Document the digest-pinned base plus the runtime/build lock pair instead.
| # would still download an unpinned hatchling into an isolated build env | ||
| # (#356 round 5). --no-build-isolation makes the locked one authoritative. | ||
| RUN pip install --no-cache-dir --require-hashes -r /src/requirements.lock \ | ||
| && pip install --no-cache-dir --require-hashes -r /src/producers/build-requirements.lock \ |
There was a problem hiding this comment.
[P2] Keep the build toolchain out of the shipped runtime image. This single-stage install leaves build, Hatchling, their dependencies, and the copied producers source/tests/scripts in the production image even though only the installed receiver package is needed at runtime. Use a pinned builder stage to build the wheel, then install only the runtime lock and wheel into a clean pinned final stage.
…image (GoogleCloudPlatform#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 GoogleCloudPlatform#349 security-artifacts item.
caohy1988
left a comment
There was a problem hiding this comment.
Round-7 fresh review at 406db09: changes required before merge/tag
Fresh verification: all required checks pass; the local producer suite is 414 passed, 4 expected live-E2E skips; and the image CI log proves bad-token rejection plus a real authenticated protobuf decode and Pub/Sub publication. Live required-reviewer environments, split IAM/WIF, the tag ruleset, and Container Scanning also read back correctly.
P1 release blockers
- The GitHub-release-missing branch classifies every PyPI HTTP 200 as
missing-releasewithout validating the anchor or parsing the response. Malformed or empty success responses can therefore produce destructive yank/version-burn advice instead of an indeterminate or unpublished state. - The canonical lock-regeneration script downloads and executes an unpinned latest
pip-toolswith the repository mounted read-write. The installed artifacts are hash-locked, but their generator is still a mutable supply-chain input with source-tree write access.
P2 follow-ups
- Object-shaped invalid PyPI schemas such as
{"urls": null}and{"urls": [{}]}crash reconciliation instead of returninginvalid-response. The workflow fails closed, but loses the declared diagnostic and recovery semantics. - PR CI does not execute the complete publish-time body re-anchor command path.
- The image self-test does not assert the new multi-stage security property that build tools and the source tree are absent from the runtime image.
- The PR description says 24 reconciliation tests, while this exact head collects 19. The reconciler module's state list also omits
invalid-responseandmissing-release.
Operationally, the PR is currently BEHIND current main; the active main ruleset requires strict up-to-date status checks. Update the branch and rerun CI after resolving the findings.
Because the authenticated account authored this PR, GitHub cannot record a formal REQUEST_CHANGES review. This COMMENTED review is the equivalent merge-gate verdict.
| echo "::error::PyPI lookup indeterminate (HTTP ${HTTP}) — not treating as absent" | ||
| exit 1 | ||
| fi | ||
| if [ "$RELEASE_MISSING" = "1" ]; then |
There was a problem hiding this comment.
[P1] Do not classify an unvalidated PyPI HTTP 200 as a cross-surface partial. When the GitHub release is missing, this branch bypasses both immutable-anchor validation and JSON/schema parsing. A truncated success body or even {"urls": []} becomes missing-release, which emits destructive yank/version-burn advice. Always validate the anchor and parse the PyPI response before combining surface states; move this branch into the tested reconciler and cover the release-present/missing × PyPI-404/empty/files/malformed × anchor-valid/invalid matrix.
| set -euo pipefail | ||
| BASE="python:3.12-slim@sha256:423ed6ab25b1921a477529254bfeeabf5855151dc2c3141699a1bfc852199fbf" | ||
| docker run --rm -v "$PWD:/src" "$BASE" bash -c " | ||
| pip install -q pip-tools && |
There was a problem hiding this comment.
[P1] The lock generator itself is an unlocked supply-chain input. This downloads and executes the latest pip-tools from PyPI while /src is a read-write mount of the repository. A compromised resolver can rewrite the locks or any source/workflow file before review. Pin and hash pip-tools plus its dependencies, mount repository inputs read-only, generate into a separate writable temporary directory, then compare/copy only the two expected lock outputs; add a CI lock-drift check using that exact generator.
| return "partial", "release SHA256SUMS differs from the build anchor" | ||
|
|
||
| # --- PyPI: exact distribution set + digests + not yanked ------------------ | ||
| urls = {u["filename"]: u for u in pypi.get("urls", ())} |
There was a problem hiding this comment.
[P2] Validate object-shaped PyPI schemas before iterating. Fresh reproduction shows {"urls": null} raises TypeError and {"urls": [{}]} raises KeyError. This fails closed, but it bypasses the declared invalid-response state and its retry-first recovery advice. Require urls to be a list of objects with string filenames and valid digest objects, reject duplicates, and return invalid-response for every schema violation.
| # could have edited it during the approval pause. Re-render | ||
| # from the protected checkout with the digest extracted from | ||
| # the ANCHOR wheel, then publish atomically. | ||
| ANCHOR_DIGEST=$(python3 -c " |
There was a problem hiding this comment.
[P2] Execute this complete publish-time re-anchor path in PR tests. Wheel extraction and rendering are tested separately, but no test composes the exact anchor-wheel lookup, renderer CLI, output file, and gh release edit --notes-file ... --draft=false --latest=false command. A quoting/path regression first appears after PyPI publication. Extract a small helper and test it with a fake anchor wheel plus a stubbed gh invocation.
| } | ||
| trap cleanup EXIT | ||
|
|
||
| echo "==> metadata + factory assertions (inside the image)" |
There was a problem hiding this comment.
[P2] Assert the multi-stage image property, not only runtime behavior. This proves version, factories, HTTP decoding, and publication, but it still passes if a later Dockerfile edit copies /build/producers or installs build/hatchling in the final stage. Add assertions that the source/build tree and build distributions are absent while the tracing package and required runtime dependencies remain installed.
… notes (GoogleCloudPlatform#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.
…rections (GoogleCloudPlatform#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 (GoogleCloudPlatform#316, GoogleCloudPlatform#324, GoogleCloudPlatform#317, GoogleCloudPlatform#340, GoogleCloudPlatform#342, GoogleCloudPlatform#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.
…spatch (GoogleCloudPlatform#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.
…image (GoogleCloudPlatform#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 GoogleCloudPlatform#349 security-artifacts item.
…oogleCloudPlatform#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.
406db09 to
50268fc
Compare
caohy1988
left a comment
There was a problem hiding this comment.
Round-7 fresh review at 50268fc: changes required before merge/tag
Fresh verification: the live PR head and base are exact, the branch is no longer behind, all visible CI checks pass, the producer suite is 427 passed with 4 expected live-E2E skips, and the focused release suite is 43 passed. Two release-state correctness blockers remain; the inline comments include concrete fixes.
P1 release blockers
- PyPI schema validation still accepts malformed HTTP 200 bodies as trusted states. In particular, correct filenames/digests with
yankedomitted can reachcomplete. - Release-asset validation runs before confirmed PyPI absence, so a broken draft with no PyPI files emits yank/version-burn advice, while a missing draft with no PyPI files emits advice to keep a draft that does not exist.
P2 follow-ups
pip-tools.inis not consumed or included in lock convergence, so the generator bootstrap input can drift without failing CI.regen-locks.shdepends on the caller's working directory, cleans up only after success, and can replace one output before discovering a problem with the other.- Update the permanent PR description before merge. It still says six rounds, 24 reconciler tests, and that all actions are SHA-pinned. This head is round 7, collects 29 reconciler tests, and Producers CI still uses version tags for several actions. Either pin those actions too or qualify the statement as release-workflow-only.
Because the authenticated account authored this PR, GitHub cannot record a formal REQUEST_CHANGES review. This COMMENTED review is the equivalent merge-gate verdict.
| [{}], duplicates, missing digests) must become 'invalid-response' — | ||
| never a crash and never a destructive classification (#356 round 7). | ||
| """ | ||
| urls = pypi.get("urls", []) |
There was a problem hiding this comment.
[P1] Require the complete PyPI success schema before trusting this response. pypi.get("urls", []) makes an HTTP 200 {} indistinguishable from the workflow's explicit 404 marker; the digest check accepts any 64 characters; and entry.get("yanked") later treats missing/null yanked as false. I reproduced correct filenames and SHA-256 values with yanked omitted reaching complete.
Suggested fix: represent HTTP 404 with a separate --pypi-missing input instead of {}. For every HTTP 200, require urls to exist and be a list, require each SHA-256 value to fully match [0-9a-f]{64}, and require yanked to exist with Boolean type. Add negative tests for missing urls, missing/null/string yanked, non-hex/uppercase/wrong-length digests, and assert that none can publish or trigger destructive recovery.
| "PyPI carries validated files but the GitHub release is missing", | ||
| ) | ||
|
|
||
| # --- GitHub release assets: exact set + byte identity --------------------- |
There was a problem hiding this comment.
[P1] Classify confirmed PyPI absence before choosing release recovery advice. With a partial/broken draft and zero PyPI files, this asset check returns partial, whose dispatch tells the operator to yank PyPI and burn the version even though nothing was published. With no draft and no PyPI files, the current unpublished dispatch instead says to keep a draft that does not exist.
Suggested fix: distinguish at least missing-all (no release, no PyPI), draft-invalid (release exists but assets differ, no PyPI), and unpublished (exact draft, no PyPI). Reserve partial plus yank/version-burn advice for states where validated PyPI files actually exist. Extend the Cartesian state tests to assert both the state and the exact recovery action/message, not only classification.
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| - name: Regenerate locks with the exact pinned generator | ||
| run: bash deploy/otlp_receiver/regen-locks.sh | ||
| - name: Locks must match the reviewed files |
There was a problem hiding this comment.
[P2] Include the generator bootstrap lock in the convergence contract. The exact generator installs from pip-tools.lock, but regen-locks.sh never consumes pip-tools.in and this diff checks only the two downstream locks. A change to pip-tools.in with a stale generator lock therefore passes unchanged.
Suggested fix: add a pinned bootstrap/convergence step that regenerates pip-tools.lock from pip-tools.in, then regenerates the runtime/build locks with that environment and diffs all three files. If pip-tools.lock is intentionally a manually reviewed trust root, remove the unused .in file and document/test that lifecycle explicitly instead of advertising automatic convergence.
| # /tmp or /var/folders — a mount outside the share silently swallows the | ||
| # outputs inside the VM. | ||
| mkdir -p "$HOME/.cache" | ||
| OUT=$(mktemp -d "$HOME/.cache/bqaa-regen-locks.XXXXXX") |
There was a problem hiding this comment.
[P2] Make regeneration independent of the caller and safe on failure. The script mounts $PWD, writes back through relative paths, removes $OUT only after success, and copies the two lock outputs one at a time without first validating both. Running it outside the repository root or failing between copies can leave leaked scratch data or a mixed lock pair.
Suggested fix: derive the repository root from ${BASH_SOURCE[0]} (or git -C that directory), install an EXIT cleanup trap immediately after mktemp, verify both generated outputs exist and are non-empty before touching the worktree, then stage and replace the reviewed pair only after all validation succeeds. Also source the base-image reference from one canonical checked-in location so the Dockerfile and generator cannot drift independently.
…t locks (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Round-9 fresh review at 644b08a: changes required before merge/tag
Fresh exact-head verification: all live CI checks pass; the producer suite is 435 passed with 4 expected live-E2E skips; the focused release suite is 51 passed; and the reconciler collects 37 tests. The omitted-yanked, strict-digest, explicit --pypi-missing, action-pinning, and absence-first draft fixes are present. One release-state blocker remains, plus four P2 hardening items. Concrete fixes are inline.
P1 release blocker
An HTTP-200 PyPI release with urls: [] is still classified as confirmed absence. PyPI can retain a release record after every file is deleted, and deleted filenames cannot be reused, so the current same-version rerun advice is wrong. This needs a distinct burned-version/no-yank recovery state.
P2 follow-ups
- The three-lock generator is not one-run convergent when the generator version changes.
- The Dockerfile still duplicates the supposedly canonical base reference while the parser reads only the first.
- Copyback is sequential rather than all-or-nothing despite the comment.
- Reconciler state documentation still describes the older six-state/
{}-marker model.
Because the authenticated account authored this PR, GitHub cannot record a formal REQUEST_CHANGES state. This COMMENTED review is the equivalent merge-gate verdict.
| urls, why = _validated_urls(pypi) | ||
| if urls is None: | ||
| return "invalid-response", f"invalid PyPI response schema: {why}" | ||
| pypi_absent = not urls |
There was a problem hiding this comment.
[P1] Do not collapse an HTTP-200 empty release into the explicit-404 absence state. pypi_absent = not urls makes {"urls": []} produce unpublished or missing-all, both of which can advise a same-version rerun. PyPI can retain a release after its files are deleted, and PyPI forbids reusing deleted filenames, so that rerun will fail even though there is currently nothing to yank. The test at test_reconcile_release.py:365 currently locks in this unsafe equivalence.
Suggested fix: make pypi is None the sole confirmed-absence predicate. For a non-None HTTP-200 response whose validated urls list is empty, return a distinct empty-release state. Its dispatch should say: no files exist to yank, but the version is burned; bump, rebuild, and cut a new tag. Add release-present and release-missing tests asserting that this state contains neither same-version rerun advice nor a yank instruction.
References: https://docs.pypi.org/api/json/ shows releases with empty file lists; https://pypi.org/help/#file-name-reuse documents that deleted filenames cannot be reused.
|
|
||
| docker run --rm -v "$REPO_ROOT:/src:ro" -v "$OUT:/out" "$BASE" bash -c " | ||
| set -euo pipefail | ||
| pip install -q --require-hashes -r /src/deploy/otlp_receiver/pip-tools.lock |
There was a problem hiding this comment.
[P2] Generate downstream locks with the newly resolved generator, not the old installed one. This container installs the checked-in pip-tools.lock, writes a potentially newer candidate pip-tools.lock, then immediately runs both downstream compilations with the already-installed old toolchain. A generator/backend update therefore is not one-run convergent and can give the downstream locks provenance that differs from the new generator lock.
Suggested fix: split regeneration into two phases. Phase 1 installs the reviewed checked-in lock only to produce a candidate generator lock. Phase 2 starts a fresh container/environment, installs that candidate with --require-hashes, regenerates the candidate generator lock again plus both downstream locks, and requires the second generator lock to be byte-identical before any copyback. CI should execute this exact two-phase path and diff all three outputs.
| set -euo pipefail | ||
|
|
||
| REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" | ||
| BASE=$(grep -m1 -oE 'python:3\.12-slim@sha256:[0-9a-f]{64}' \ |
There was a problem hiding this comment.
[P2] The base image is still duplicated, and this parser silently trusts only the first copy. The Dockerfile repeats the full base reference for the builder and runtime stages. grep -m1 reads only the builder FROM, so the runtime stage can drift while the generator continues claiming a single canonical source.
Suggested fix: define one global Dockerfile value, for example ARG PYTHON_BASE=python:3.12-slim@sha256:..., and use FROM ${PYTHON_BASE} for both stages; make this script parse that single ARG. If keeping literal FROM lines, parse every matching reference and fail unless the complete set contains exactly one identical value. Add a shell test that intentionally makes the second stage differ and requires regeneration to fail.
| for f in pip-tools.lock requirements.lock build-requirements.lock; do | ||
| test -s "$OUT/$f" || { echo "generated $f is missing or empty — aborting"; exit 1; } | ||
| done | ||
| cp "$OUT/pip-tools.lock" "$REPO_ROOT/deploy/otlp_receiver/pip-tools.lock" |
There was a problem hiding this comment.
[P2] The copyback still is not all-or-nothing. Validating all generated files first prevents generation-time partial updates, but these three cp calls remain sequential. A permission, disk, interruption, or later-copy failure after the first replacement leaves the worktree with a mixed lock generation, exactly what the comment says cannot happen.
Suggested fix: stage destination-local temporary files, back up all three originals, and install a rollback trap covering ERR, INT, and TERM. Replace the set only after staging succeeds; if any replacement fails, restore every original before exiting. Add a test with an injected second-copy failure proving all three destination hashes remain unchanged.
| exact filename sets (no subsets, no extras), byte identity, nothing | ||
| yanked. | ||
|
|
||
| States: |
There was a problem hiding this comment.
[P2] Synchronize the public state contract with the implementation. This module still documents six states and the old broad partial/unpublished meanings, omitting missing-all and draft-invalid; the CLI comment near line 304 also still says {} is the explicit-404 marker even though --pypi-missing replaced it. Operators debugging finalize will read the wrong recovery model.
Suggested fix: after adding the HTTP-200 empty-release state, document all nine states here with their exact recovery semantics and replace the stale {} comment with the --pypi-missing/None representation. Prefer a single state-to-action mapping used by dispatch() plus a test asserting its keys equal the documented/produced state set, so the vocabulary cannot drift again.
…ional copyback (GoogleCloudPlatform#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.
| DOCKERFILE="$REPO_ROOT/deploy/otlp_receiver/Dockerfile" | ||
|
|
||
| # --base-ref-start-- (executed verbatim by the test suite) | ||
| REFS=$(grep -oE 'python:[0-9.]+-slim@sha256:[0-9a-f]{64}' "$DOCKERFILE" | sort -u) |
There was a problem hiding this comment.
[P2] Validate the Dockerfile FROM consumers, not merely matching literals.
This guard only proves that one Python digest literal appears somewhere. It never proves that the actual stages consume PYTHON_BASE. I replaced both stages with FROM ubuntu:latest while leaving the pinned ARG in place; this parser returned success and selected the unused Python digest. The current divergent-literal test misses every base that does not match its narrow Python regular expression.
Potential fix: parse exactly one global ARG PYTHON_BASE=, require the expected two FROM instructions to use ${PYTHON_BASE} (with only the optional builder alias), and reject every literal or alternate-variable stage. Add negatives for an unpinned FROM, an unrelated literal base, a second variable, missing or duplicate ARG declarations, and too few or too many stages.
| unknown = reconcile_release.dispatch("not-a-state") | ||
| assert not unknown.publish and unknown.exit_code != 0 | ||
|
|
||
| def test_docstring_documents_every_known_state(self): |
There was a problem hiding this comment.
[P2] Enforce the exact produced, dispatched, and documented state vocabulary.
The current head happens to produce the same nine strings that KNOWN_STATES contains, but the claimed one-mapping contract is not what this test proves. reconcile() still returns independent string literals, the module table is handwritten, and this assertion is one-way substring matching. A removed row can still pass when its word appears elsewhere in the prose, and an obsolete extra documented state is never rejected.
Potential fix: use shared state constants or structured StateSpec metadata for every reconciliation return and dispatch key. Parse exact state-table row tokens, then assert equality in both directions among produced states, dispatch keys, and documented rows. Include a mutation-style negative demonstrating that a stale extra row and a produced typo both fail.
| /src/deploy/otlp_receiver/pip-tools.in | ||
| # Convergence compares RESOLUTION content: the pip-compile header | ||
| # embeds each phase's --output-file path, which legitimately differs. | ||
| diff <(grep -v '^#' /out/pip-tools.candidate.lock) \ |
There was a problem hiding this comment.
[P2] Make a genuine generator disagreement recoverable.
If phase 2 differs from the phase-1 candidate, this branch exits before copyback and the EXIT trap deletes both generated locks. A rerun starts from the same unchanged checked-in seed, produces the same candidate, and reaches the same mismatch, so the current rerun-after-review advice is a deterministic dead end.
Potential fix: iterate in fresh hash-locked containers with a small explicit bound: install seed N, generate candidate N+1, and stop only when two consecutive resolution bodies match. Then start one final fresh environment from the converged lock to generate the downstream locks. On bounded exhaustion, preserve the last two diagnostic locks and print their location instead of deleting the only evidence. Add a stubbed-generator test that requires more than two phases and another that proves bounded non-convergence fails with usable diagnostics.
| rm -f "$dest.bqaa-new" | ||
| done | ||
| } | ||
| trap 'rollback' ERR INT TERM |
There was a problem hiding this comment.
[P1] Make the copyback commit boundary signal-safe.
The new rollback covers staging failures, but it remains armed while backups are deleted one at a time, and the INT/TERM handlers return instead of terminating. I reproduced a TERM immediately after the first backup deletion: the command exited 0 with pip-tools.lock new while requirements.lock and build-requirements.lock were restored old. That violates the all-or-nothing guarantee and can leave a reviewed lock set with mixed provenance.
Potential fix: keep rollback armed only until all three destination renames succeed. At that point mark the transaction committed and disarm ERR/INT/TERM before best-effort backup cleanup. Use distinct signal handlers that roll back and exit nonzero, for example 130 for INT and 143 for TERM. Extend the executed test to inject failure or a signal after every rename and during every backup-cleanup position, asserting that the result is always all-old or all-new and that a signal never exits 0.
caohy1988
left a comment
There was a problem hiding this comment.
Round 10 verdict: changes requested.
GitHub does not permit this account to submit a formal request-changes review on its own PR, so this comment records the same merge gate.
I posted four inline findings with reproductions and potential fixes. The signal-window lock corruption is merge-blocking: a TERM during backup cleanup can leave one new lock and two old locks while the script exits 0.
[P2] Refresh the PR description with the current contract. The body still says eight review rounds, eight exhaustive states, and 37 reconciler tests. Exact head 31fa026 contains the round-9 commit, nine reconciliation states, 41 reconciler tests, and four generator-guard tests.
Potential fix: after resolving this review, update the description to the final review-round count and exact test/state totals. Replace the current one-run-convergent and transactional wording with the implemented bounded fixed-point and signal-safe commit semantics, so the public claim matches the executable guarantees.
Inline findings:
- P1 signal-safe transaction boundary: #356 (comment)
- P2 recoverable convergence: #356 (comment)
- P2 actual FROM-consumer validation: #356 (comment)
- P2 exact state-contract enforcement: #356 (comment)
Verification at this head: 443 producer tests passed with 4 skips; focused release suite 59 passed; ShellCheck, bash syntax, Python compilation, and diff checks passed; all GitHub checks are green. Those checks do not cover the reproduced cleanup signal window.
…onsumer validation (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Fresh full review at ae916b7: changes required before merge/tag
I posted two P1 release blockers and two P2 correctness findings inline. The strongest blocker is the full-rerun path: it deletes a recoverable draft and rebuilds a different immutable artifact anchor after PyPI may already contain the original bytes.
Verification on the exact head: 451 passed, 4 expected live-E2E skips; 67 focused release tests passed; ShellCheck, syntax, compile, formatting, current PR checks, live IAM/WIF, registry immutability, and environment/tag protections were rechecked.
Because the authenticated account authored this PR, GitHub cannot record a formal REQUEST_CHANGES state. This COMMENTED review is the equivalent merge-gate verdict.
| fi | ||
| IS_DRAFT=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["draft"])') | ||
| if [ "$IS_DRAFT" = "True" ]; then | ||
| echo "stale draft from an earlier attempt — deleting so assets cannot mix" |
There was a problem hiding this comment.
[P1] Preserve the existing release state before a full rerun.
This deletes every draft before checking PyPI or TestPyPI. If PyPI accepted the original wheel/sdist and only finalize failed transiently, a full rerun deletes the matching draft and builds a new anchor. I reproduced the failure mode: identical source and injected image digest built with different checkout timestamps produced different wheel and sdist SHA-256 hashes. Finalization then compares valid PyPI bytes against the replacement anchor and emits partial/yank/version-burn recovery.
Fix: never auto-delete an existing draft, or permit deletion only after both indexes return explicit 404s. Otherwise preserve the draft and instruct operators to rerun failed jobs from the original workflow attempt. Add a regression for draft present + PyPI complete + full rerun.
There was a problem hiding this comment.
Fixed in 2f6d1af. The delete/preserve/fail decision now lives in PR-tested producers/scripts/guard_existing_release.py: a stale draft is deleted ONLY when PyPI and TestPyPI both return an explicit 404 for the version (indeterminate lookups fail before reaching the guard). Once either index holds files, the draft is preserved with instructions to rerun the FAILED jobs from the original workflow attempt; a published release always fails. The requested regression is test_draft_present_and_pypi_complete_full_rerun_preserves_draft, plus a full release×index matrix invariant test. RELEASING.md documents the original-attempt rerun path.
| # cross-surface partial, not "unpublished". | ||
| RELEASE_MISSING=0 | ||
| mkdir -p release | ||
| if ! gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ |
There was a problem hiding this comment.
[P1] Reconcile release visibility metadata, not only downloaded assets.
gh release download succeeds for a published release too, but the reconciler receives no draft/visibility state. If the draft is accidentally published during an approval pause while PyPI remains absent, this path classifies the exact assets as unpublished and tells the operator to keep "the draft" even though incomplete artifacts are already customer-visible. The final edit also does not reassert title or prerelease=false.
Fix: fetch release metadata through the API, pass it into reconciliation, add an explicit premature-publication state, and reassert the canonical title, tag, prerelease=false, latest=false, and body at publication.
There was a problem hiding this comment.
Fixed in 2f6d1af. finalize now fetches the release METADATA via the API before downloading assets and passes --release-published into the reconciler. Every determinate non-complete state on a published release becomes the new premature-publication state (re-draft immediately; the underlying state survives in the detail line) — the reviewer scenario (published during the approval pause + PyPI absent) is test_published_release_with_prod_absent_is_premature. Indeterminate states (invalid-anchor/invalid-response) keep retry-first advice. Publication now reasserts the canonical title (--title "Tracing <tag>"), body, --prerelease=false, and --latest=false in publish_release_body.py, with test assertions.
| fi | ||
| # Surface 2: PyPI. Transport failures (DNS/TLS/connect) are | ||
| # indeterminate and must not fall into the HTTP branches. | ||
| if ! HTTP=$(curl -sS --connect-timeout 10 --max-time 30 \ |
There was a problem hiding this comment.
[P2] Include TestPyPI in cross-surface reconciliation.
The TestPyPI uploader can publish one distribution before failing on the next, which burns that version. finalize queries only production PyPI, so production 404 plus an exact draft becomes unpublished and recommends rerunning publication even though the failed TestPyPI prerequisite cannot succeed at the same version.
Fix: query and validate the exact TestPyPI filename/digest set alongside PyPI and GitHub, then make its partial/complete/absent states part of the tested recovery matrix.
There was a problem hiding this comment.
Fixed in 2f6d1af. finalize now queries TestPyPI with the same fail-closed HTTP handling (explicit 404 = absent, 200 = schema-validated body, anything else = indeterminate error) and the reconciler validates the exact TestPyPI filename/digest set against the anchor. The reviewer scenario — production 404 + exact draft + partial TestPyPI — is the new testpypi-partial state: version burned there, bump + re-tag, never same-version rerun advice (test_prod_absent_testpypi_partial_is_burned_not_unpublished). The absent/complete/partial matrix is tested, including: pruned TestPyPI files never block a complete production release, while a digest conflict on a present file does fail closed (the full-lifecycle gate installed different bytes).
| cp "$OUT/candidate.lock" "$OUT/pip-tools.lock" | ||
| break | ||
| fi | ||
| cp "$OUT/candidate.lock" "$OUT/seed.lock" |
There was a problem hiding this comment.
[P2] Preserve two distinct convergence diagnostics.
On the final mismatch this overwrites seed.lock with candidate.lock; the exhaustion branch then copies both names, so the promised "last two locks" are byte-identical. The stubbed non-convergence reproduction saved seed.lock = v3-unique and candidate.lock = v3-unique, eliminating the diagnostic delta.
Fix: preserve the pre-overwrite seed as a separate previous lock and copy that plus the final candidate on exhaustion. Extend the test to assert the diagnostic contents differ and represent the last two phases.
There was a problem hiding this comment.
Fixed in 2f6d1af. The loop now copies the phase input to previous-seed.lock before reseeding, and exhaustion preserves previous-seed.lock + candidate.lock — the final phase's input and output, i.e. the last two distinct resolutions whose diff is the non-convergence. test_bounded_exhaustion_preserves_diagnostics now asserts the two files differ and carry exactly the phase-2 and phase-3 outputs of the stub (v2-unique/v3-unique); a new first-phase-exhaustion test covers the checked-in-seed edge.
…ation, distinct convergence diagnostics (GoogleCloudPlatform#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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation, distinct convergence diagnostics (GoogleCloudPlatform#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).
2f6d1af to
d2ab512
Compare
caohy1988
left a comment
There was a problem hiding this comment.
Fresh re-review of d2ab512 after the four prior findings were addressed. Those fixes are present and the local/CI verification is green, but the end-to-end release path still has four blocking correctness/security issues plus four P2 recovery/transaction issues. Inline comments contain the reproductions and requested fixes.
Verification: exact PR head/base reviewed; 479 producer tests passed (4 optional-dependency skips); 84 focused release tests passed; pyink/isort, compile checks, shellcheck, workflow parsing, and current CI passed. Live GitHub/GCP controls were also rechecked.
Requesting changes because the normal finalize path currently cannot discover the draft release, and the rerun path can override the new guard's correct recovery decision.
| run: | | ||
| set -euo pipefail | ||
| set +e | ||
| RESP=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" 2>&1) |
There was a problem hiding this comment.
P1 — the normal release path cannot discover this draft. GitHub documents GET /repos/{owner}/{repo}/releases/tags/{tag} as getting a published release, so a draft is returned as 404 here. The same lookup is used by finalize; it therefore classifies the valid draft as missing and can never publish the normal successful release. Please use authenticated List Releases, filter the exact tag, retain the release ID, and use ID-based asset operations. Docs: https://docs.github.com/en/rest/releases/releases?apiVersion=2026-03-10#get-a-release-by-tag-name
There was a problem hiding this comment.
Fixed in 3782959. Both the guard and finalize now discover the release via authenticated List Releases (gh api --paginate .../releases) filtered on the exact tag — never the by-tag endpoint. More than one release/draft sharing the tag fails closed as ambiguous. The release ID is retained (exported to the publish step), and finalize downloads assets BY ASSET ID (.../releases/assets/{id} with Accept: application/octet-stream). Verified live against this repo: the list pipeline finds tracing-v0.1.0 with its ID, and a by-ID asset download reproduced the API's digest byte-for-byte.
| # path, not stranded silently in a draft. Publishes the release only | ||
| # on COMPLETE state; --latest=false preserves the SDK's vX.Y.Z | ||
| # release as the repository's Latest. | ||
| if: always() && needs.verify.result == 'success' && needs.build.result == 'success' |
There was a problem hiding this comment.
P1 — finalize still runs after the new release guard deliberately fails. It does not directly need or gate on github-release. On a full rerun, the guard correctly preserves the original draft and fails, the publication jobs skip, but always() lets finalize reconcile that original draft against the newly rebuilt, byte-different anchor. That overrides the guard's correct "rerun the original attempt" advice with invalid-draft/yank/burn guidance. Please make github-release a direct need and skip this reconciliation when its guard fails; recovery must use the original attempt's anchor.
There was a problem hiding this comment.
Fixed in 3782959. finalize now has github-release as a direct need and its if: requires needs.github-release.result == 'success' alongside verify/build. On a full rerun where the guard deliberately fails, publication jobs skip AND finalize skips — the guard's "rerun the failed jobs from the original attempt" advice stays authoritative, and reconciliation only ever runs against the anchor of the attempt that created the draft.
| # composed helper (anchor-wheel digest -> renderer -> atomic | ||
| # gh release edit): anything with contents:write could have | ||
| # edited the draft body during the approval pause. | ||
| python3 producers/scripts/publish_release_body.py \ |
There was a problem hiding this comment.
P1 — publication is not bound to the release snapshot that was reconciled. Assets are downloaded and verified earlier, but this publishes later by mutable tag. A contents:write actor can replace the draft assets between those steps, and the repository's live immutable-releases setting is currently disabled, so published assets and SHA256SUMS remain replaceable afterward. Please enable/assert immutable releases, retain the exact release and asset IDs plus digest inventory, and revalidate immediately before/after publishing the exact release. Premature-publication recovery will also need to account for immutable releases being non-redraftable. Docs: https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases
There was a problem hiding this comment.
Fixed in 3782959. publish_release_body.py is now snapshot-bound and ID-based: it fetches the exact release ID that was reconciled, verifies the complete asset inventory (names + the API digest field against sha256 of the ANCHOR bytes, including SHA256SUMS) immediately BEFORE the edit, publishes via PATCH /releases/{id} (reasserting tag/title/body/prerelease/make_latest), re-fetches and re-verifies the same snapshot AFTER, and asserts the published release has immutable: true — failing closed with instructions until the repository setting is enabled (documented as required one-time setup in the workflow header and RELEASING.md; it cannot be enabled from a PR). Premature-publication recovery now covers non-redraftable immutable releases (burn + corrected follow-up). An already-published, immutable, snapshot-identical release is an idempotent success without an edit. All paths are unit-tested with a stubbed gh (pre/post tamper, extra/missing assets, mutable publication, idempotent rerun).
| fi | ||
| if grep -q '^delete_draft=1$' guard.out; then | ||
| echo "stale draft from an earlier attempt — deleting so assets cannot mix" | ||
| gh release delete "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --yes |
There was a problem hiding this comment.
P1 — stale-draft deletion has a destructive TOCTOU window. Once draft discovery is fixed, the release can become published between the initial lookup, the two index requests, and this tag-based delete. PyPI also documents that its APIs are CDN-cached, so one 404 is not necessarily a fresh absence proof. The safest fix is to remove automatic deletion. If it remains automated, retain the exact release ID, re-fetch that ID immediately before deletion, require the same object still has draft=true, and use a suitably fresh absence check. https://docs.pypi.org/api/
There was a problem hiding this comment.
Fixed in 3782959. Deletion remains automated but is now TOCTOU-guarded per your conditions: the guard retains the release ID from discovery, re-fetches that exact ID immediately before deletion, requires the same object to still be draft=true on the same tag (any change aborts), and deletes by ID — never by tag. Freshness: every index lookup (guard, finalize, and the new pre-upload gates) appends a unique run-scoped cache-busting query string so the CDN cannot serve a cached 404 as absence proof (per https://docs.pypi.org/api/).
| # but a file that IS present must carry the anchor's bytes: the | ||
| # full-lifecycle gate installed from TestPyPI, and a digest conflict | ||
| # means the gate exercised different bytes than customers receive. | ||
| for name in sorted(expected_pypi & set(tp_urls)): |
There was a problem hiding this comment.
P2 — the production-complete branch accepts extra and yanked TestPyPI files. This loop inspects only expected_pypi & set(tp_urls). I reproduced both an unexpected platform wheel and a yanked expected wheel returning complete, contrary to the module/test contract and potentially allowing the lifecycle gate to install different bytes. Missing expected files can remain tolerated for pruning, but every present filename should be expected, non-yanked, schema-valid, and digest-identical. Please add both regressions.
There was a problem hiding this comment.
Fixed in 3782959. The production-complete branch now iterates over every PRESENT TestPyPI file: an unexpected filename, a yanked file, or a digest mismatch each return partial; only MISSING files stay tolerated (pruning). Both of your reproductions are regressions: test_testpypi_extra_file_with_prod_complete_is_partial (unexpected platform wheel) and test_testpypi_yanked_file_with_prod_complete_is_partial (yanked expected wheel).
| return COMPLETE, "byte identity proven against the build anchor" | ||
|
|
||
|
|
||
| _PREMATURE_WRAPPABLE = frozenset( |
There was a problem hiding this comment.
P2 — published visibility is not orthogonal to the underlying state. invalid-anchor and invalid-response are excluded from this wrapper, so a published release plus malformed index response gets retry-only advice and remains public. Separately, the workflow's public-image mismatch overwrites the state with plain partial, also losing the published signal. Please preserve visibility independently: every published, non-complete outcome should emit immediate containment guidance plus the underlying recovery classification.
There was a problem hiding this comment.
Fixed in 3782959. Visibility is now orthogonal: EVERY non-complete state — including invalid-anchor and invalid-response — becomes premature-publication when the release is publicly visible, with the underlying classification and its recovery preserved verbatim in the detail line (containment guidance first, then the underlying recovery). The workflow's public-image digest-mismatch override also keeps the published signal: it emits premature-publication instead of plain partial when the release is published. Regressions: test_indeterminate_states_are_wrapped_too, plus the workflow branch.
| ) | ||
| if asset_problem: | ||
| return DRAFT_INVALID, f"nothing on PyPI and {asset_problem}" | ||
| return ( |
There was a problem hiding this comment.
P2 — complete TestPyPI plus a failed uploader job has no viable recovery path. If TestPyPI accepted both files but the action lost the response/failed afterward, this returns unpublished and dispatch says to rerun failed jobs. The same-version uploader rerun then fails because those files already exist (skip-existing is not enabled), so promotion cannot resume. Please let an exact verified TestPyPI publication satisfy the failed upload stage, or provide an explicit non-reupload recovery path and cover the lost-response case.
There was a problem hiding this comment.
Fixed in 3782959. New PR-tested scripts/check_index_publication.py runs as a pre-upload gate in BOTH publish jobs: absent (fresh explicit 404) → proceed with the upload; an EXACT verified existing publication (same filename set, nothing yanked, digests identical to the local dist files) → the stage is satisfied and the upload step is skipped; anything else → the version is burned there, fail closed. skip-existing stays disabled, so the byte-identity contract is unchanged — the lost-response rerun now converges instead of dying on "version already exists" (test_exact_existing_publication_is_satisfied), and promotion can resume.
| # left one new + two old locks with exit 0). | ||
| if [ "$COMMITTED" = "0" ]; then | ||
| for dest in "$DEST_PIP_TOOLS" "$DEST_REQUIREMENTS" "$DEST_BUILD"; do | ||
| [ -f "$dest.bqaa-bak" ] && mv -f "$dest.bqaa-bak" "$dest" |
There was a problem hiding this comment.
P2 — rollback can restore a stale backup from a previous run. A post-commit interruption may strand .bqaa-bak files. If a later run fails before overwriting every destination's backup, this loop restores stale backups for destinations the current transaction never touched, mixing lock generations. Please use per-run backup names or track and restore only destinations actually backed up in this invocation, with a two-run interruption regression.
There was a problem hiding this comment.
Fixed in 3782959. The staging loop records each destination it backs up in a BACKED_UP array, and rollback restores ONLY those — a stale .bqaa-bak stranded by a previous run's interrupted post-commit cleanup is never restored for a destination the current transaction didn't touch (it's left for forensics; the next successful run's cleanup removes it). The requested two-run interruption regression is test_stale_backup_from_a_previous_run_is_never_restored: run 1 commits with TERM during cleanup, run 2 fails after backing up only the first destination, and all three locks must remain run-1's generation.
…, rerun-safe uploads (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Round 13 fresh review of 3782959. I found three P1 release blockers and three P2 findings. Local verification is green (508 passed, 4 skipped; pyink/isort, shellcheck, bash syntax, workflow YAML, compileall, and diff checks are clean), but the findings below are runtime/API-semantic gaps. The Producers CI workflow is currently queued rather than failing.
| exit 1 | ||
| fi | ||
| echo "stale draft from an earlier attempt — deleting release id ${RELEASE_ID} so assets cannot mix" | ||
| gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" |
There was a problem hiding this comment.
[P1] The pre-delete re-fetch does not close the check/delete race. The GET at lines 421-426 and this unconditional DELETE are separate requests. A maintainer or API client can publish after the GET reports draft=true but before this request, so the workflow deletes the now-public release by ID. Workflow concurrency does not serialize external release edits; if release immutability is enabled, publishing also protects the tag name from reuse even after release deletion. GitHub exposes no condition in this call tying DELETE to the fetched draft snapshot. Please remove automatic draft deletion or add an exclusive operational control that makes publication impossible across this interval; a second read narrows the race but does not eliminate it.
There was a problem hiding this comment.
Fixed in ac106d2 by taking your safest option: automatic deletion is REMOVED entirely. The guard's (draft, 404, 404) cell now fails the job with instructions to manually verify the release is still an unpublished draft, delete it, and re-run the job — no GET/DELETE pair remains in the workflow. Regression: test_stale_draft_requires_manual_deletion_never_automated, plus a matrix invariant that the ONLY proceeding state is release-absent + both indexes 404 and that GuardAction no longer even has a deletion field.
| "-f", | ||
| f"name=Tracing {tag}", | ||
| "-f", | ||
| f"body=@{body_path}", |
There was a problem hiding this comment.
[P1] This sends the literal string @release_body.md, not the rendered notes. gh api -f/--raw-field adds a static string; only -F/--field interprets a leading @ as a filename. I reproduced the exact semantics with gh api markdown -f text=@producers/scripts/release_notes_template.md -f mode=gfm, which returned <p>@producers/scripts/release_notes_template.md</p>. The test at test_publish_release_body.py:159 currently locks in the faulty argv shape. Use -F body=@... or pass a JSON body containing the rendered text, and assert the transmitted value equals the rendered notes.
There was a problem hiding this comment.
Fixed in ac106d2. Confirmed your reproduction independently (gh api markdown -f text=@file → <p>@file</p>; -F reads the file). The publish edit now sends a JSON --input payload built by publish_release_body.py containing the rendered notes plus tag/name/draft/prerelease/make_latest — no =@ field syntax anywhere. test_payload_carries_rendered_notes_not_a_filename asserts the transmitted body equals the rendered notes (and contains the pinned image reference), and test_happy_path_publishes_by_id_with_json_payload locks in the full payload shape.
| f" {'; '.join(problems)}" | ||
| ) | ||
| return 1 | ||
| if release.get("immutable") is not True: |
There was a problem hiding this comment.
[P1] Immutability is checked only after the release has already been made public, and the documented recovery cannot repair it. The PATCH at lines 210-230 includes draft=false; only afterward does this branch reject a mutable publication. The live repository endpoint currently returns {"enabled":false,"enforced_by_owner":false}, so this sequence exposes customer-visible, replaceable artifacts and then fails. GitHub also documents that enabling release immutability applies only to future releases, so RELEASING.md lines 204-208 cannot fix the already-published release by merely enabling the setting and rerunning this same PATCH. Make an attested immutable-release policy a prerequisite before tagging or before publication (for example, a protected admin-read credential or enforced organization policy), retain this postcondition as defense in depth, and correct the recovery guidance.
There was a problem hiding this comment.
Fixed in ac106d2. Immutability is now a PREREQUISITE: before anything becomes public, publish_release_body.py reads GET /repos/{repo}/immutable-releases and refuses to publish while enabled is false — the release stays an unpublished draft and the error instructs enabling the setting then re-running finalize (which is then correct, because our release publishes AFTER the setting is on). A release found already published-but-mutable is classified as burned with delete/yank/bump/re-tag guidance — never "enable and re-run" (RELEASING.md corrected accordingly, including a new troubleshooting entry for that state). The post-publish immutable: true assert is retained as defense in depth. Tests: test_disabled_immutable_setting_blocks_before_anything_is_public (asserts nothing else is even fetched), test_published_mutable_release_is_burned_not_repaired, test_unreadable_immutable_setting_fails_closed.
| f" publish — refusing to publish: {'; '.join(problems)}" | ||
| ) | ||
| return 1 | ||
| if release.get("draft") is False: |
There was a problem hiding this comment.
[P2] The idempotent and post-publish paths never verify the canonical editable metadata. verify_snapshot() checks only ID, tag, and asset state/digests, then this branch returns success for any published immutable release. GitHub immutable releases still allow title and release-note edits, and the post-PATCH check has the same omission, so the literal-body defect above or later drift in name, body, prerelease, or Latest status is accepted forever. Render the expected body before this branch, compare all canonical metadata on both fetched snapshots, and repair the fields GitHub permits editing or fail with exact remediation.
There was a problem hiding this comment.
Fixed in ac106d2. The canonical body and title are rendered BEFORE any branch, and canonical_problems() (title, normalized rendered body, prerelease flag) now runs on both the idempotent snapshot and the post-publish snapshot alongside the asset-digest check. On a published immutable release, title/body drift is REPAIRED with a metadata-only PATCH (the edits GitHub permits) and re-verified for convergence; prerelease drift fails as unrepairable with exact remediation. The release must also not be the repository's Latest (checked via releases/latest when readable). Tests: test_editable_metadata_drift_is_repaired, test_repair_that_does_not_converge_fails, test_prerelease_drift_is_unrepairable, test_post_publish_canonical_drift_is_detected, test_becoming_repository_latest_fails.
|
|
||
| urls, why = reconcile_release._validated_urls(index) # pylint: disable=protected-access | ||
| if urls is None: | ||
| return CONFLICT, f"invalid index response schema: {why}" |
There was a problem hiding this comment.
[P2] A malformed HTTP-200 response is mislabeled as a proven version burn. This schema-failure path and the JSON parse path at lines 120-121 both return conflict, whose contract tells the operator to bump and re-tag. A transient malformed API/CDN response proves neither absence nor incompatible published files; the final reconciler correctly models the equivalent condition separately as retryable invalid-response. Add an indeterminate or invalid-response status that still exits nonzero but instructs a clean refetch, and reserve conflict for validated file-set, yank, or digest mismatches.
There was a problem hiding this comment.
Fixed in ac106d2. The gate gains an indeterminate status: malformed HTTP-200 bodies, schema violations, and unparseable JSON exit nonzero with explicit "refetch the index and re-run before taking any recovery action" advice; a missing local dist file is likewise indeterminate (investigate the artifact). conflict — and its bump/re-tag contract — is now reserved for VALIDATED file-set, yank, or digest deviations, matching the reconciler's invalid-response modeling. Tests: test_invalid_schema_is_indeterminate_not_a_burn, test_missing_local_distribution_is_indeterminate, test_unparseable_body_is_indeterminate.
| PUBLISHED_ARG=--release-published | ||
| fi | ||
| mkdir -p release | ||
| jq -r '.[0].assets[] | "\(.id)\t\(.name)"' <<<"$MATCHES" \ |
There was a problem hiding this comment.
[P2] Finalize downloads every release asset before validating the expected set. This iterates attacker- or operator-controlled release metadata and streams all assets to disk; exact-set rejection happens only later in reconcile_release.py. One unexpected large asset or many extras can exhaust runner disk or the 15-minute job timeout before the intended recovery classification, and every rerun repeats the download. Validate metadata first against exactly the four expected hardcoded names, reject duplicate/invalid IDs and unreasonable sizes, then download only those four asset IDs to hardcoded destinations and keep the existing byte verification afterward.
There was a problem hiding this comment.
Fixed in ac106d2. Finalize now validates asset METADATA before any byte is streamed: malformed entries (non-numeric ids, empty/non-string names, negative sizes), duplicate names, and names outside [A-Za-z0-9._+-] fail closed with no download. Only the four expected hardcoded names are downloaded — by asset ID, to hardcoded destinations — and only when the reported size is within a 100 MiB cap. Unexpected or oversized assets are never streamed; they become EMPTY placeholder files so the tested reconciler still sees the exact-set / byte-identity mismatch and emits its existing partial/draft-invalid classification instead of the job dying on disk or timeout. Reruns therefore never repeat an unbounded download.
…ty as precondition (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Round 14 fresh review of exact head ac106d2: 1 P1 and 7 P2 findings. I re-read the full 5,275-line PR diff plus the round-13 delta, rechecked the live PR head and all 23 checks, ran the producer suite locally (517 passed, 4 skipped), and reran formatting, shellcheck, compile, YAML-parse, and diff-hygiene checks. The green PR checks do not exercise the tag-only GitHub credential boundary that causes the P1 below.
I also discarded one adversarial checkout-permission candidate after verifying this is a public repository (public checkout works without contents: read), so it is not included.
The PR description itself is now stale: it still says ten rounds, a nine-state contract, old test counts, and automated stale-draft deletion. Please refresh it after addressing the inline findings.
| # guidance. Recovery must use the original attempt's anchor. | ||
| if: always() && needs.verify.result == 'success' && needs.build.result == 'success' && needs.github-release.result == 'success' | ||
| needs: [verify, build, github-release, publish-pypi] | ||
| permissions: |
There was a problem hiding this comment.
[P1] The new immutable-policy preflight cannot authenticate with this job token. publish_release_body.py:243 calls GET /repos/{owner}/{repo}/immutable-releases, but GitHub requires repository Administration: read for that endpoint (endpoint contract). This job grants and passes only the workflow GITHUB_TOKEN with contents: write; Administration is not one of the permissions a workflow can grant that token (available GITHUB_TOKEN permissions). Therefore enabling immutable releases does not unblock finalize: the helper's first API call still fails and every complete release remains a draft. The unit fake always returns enabled=true, so all 517 tests miss the credential boundary. Use a narrowly scoped GitHub App installation token with Administration-read for this policy read (GitHub's prescribed fallback); keep the standard GITHUB_TOKEN for release reads/PATCHes rather than broadening the admin token.
There was a problem hiding this comment.
Fixed in f83011a. finalize mints a GitHub App installation token via actions/create-github-app-token (SHA-pinned v3.2.0) with permission-administration: read, only when state == complete (non-publishing paths never need it, so a missing App configuration cannot break classification-only runs). publish_release_body.py takes --admin-token-env and routes ONLY the immutable-releases read through that credential; every release read/PATCH keeps the job GITHUB_TOKEN. The unit fakes now enforce the boundary from both sides — the job-token fake refuses the policy path and the admin fake refuses everything else — plus regressions for a failing/absent admin credential (TestCredentialBoundary). One-time App setup (BQAA_RELEASE_POLICY_APP_ID variable + BQAA_RELEASE_POLICY_APP_PRIVATE_KEY secret, App installed with only Administration:read) is documented in RELEASING.md and the workflow header.
| index: dict | None, | ||
| ) -> tuple[str, str]: | ||
| """Returns (status, detail). ``index=None`` = an explicit 404.""" | ||
| wheel = f"bigquery_agent_analytics_tracing-{version}-py3-none-any.whl" |
There was a problem hiding this comment.
[P2] Extra local distributions bypass the precheck and are uploaded irreversibly. check() verifies that the expected wheel and sdist exist, but never requires the local directory's file set to equal those two names. After the workflow strips the plugin and SHA256SUMS, gh-action-pypi-publish uploads every distribution remaining in the default dist/ directory (action contract). I reproduced the checker returning absent with a third local wheel present. A future build change that emits an extra platform wheel can therefore publish it to TestPyPI/PyPI; finalize only then rejects the extra anchor entry, after the version is burned. Require the local publish set to be exactly wheel+sdist before either absent or satisfied, or copy exactly those files into a fresh publish directory and point both the checker and action at it.
There was a problem hiding this comment.
Fixed in f83011a. check() now requires the local publish directory's file set to be EXACTLY wheel + sdist before returning either absent or satisfied; any extra local distribution returns indeterminate (exit nonzero, upload step never runs) with detail naming the extras — reproducing your third-wheel scenario is test_extra_local_distribution_blocks_the_upload, asserted for both the 404 and 200 index paths.
| "production PyPI is absent, the GitHub release is missing," | ||
| f" and TestPyPI {tp_problem}", | ||
| ) | ||
| return ( |
There was a problem hiding this comment.
[P2] An exact TestPyPI publication plus a missing GitHub release gets an impossible recovery. Reproduction: release_dir=None, production 404, and a complete digest-identical TestPyPI response returns missing-all, whose dispatch says to restart github-release from the original attempt. But guard_existing_release.decide('absent', 'absent', 'present') rejects that same state as a cross-surface partial and says not to rebuild; I executed both paths and got the contradictory messages. A full rebuild cannot help because timestamps change the bytes, and the guard does not validate whether TestPyPI matches the preserved anchor. Either let the original-attempt guard validate exact TestPyPI bytes against dist/ and recreate the draft, or classify this as burned and give one consistent recovery. Add a cross-module regression covering reconciler output followed by guard behavior.
There was a problem hiding this comment.
Fixed in f83011a with your first option: the guard's index states are now BYTE-VALIDATED. The github-release job copies wheel+sdist into a gate directory and runs check_index_publication.py against each present index, so decide() sees absent | exact | deviating. Release-missing + production-404 + TestPyPI-exact now PROCEEDS to recreate the draft from the preserved original-attempt artifact (the upload stage is then satisfied without re-upload) — exactly the reconciler's missing-all recovery — while a full rerun's rebuilt bytes naturally classify as deviating and get an explicit burn. Cross-module regressions (TestCrossModuleConsistency) execute reconciler output followed by guard behavior for the missing-all/exact, missing-release/prod-exact, and testpypi-partial/deviating states and assert the recoveries agree.
| ) -> str | None: | ||
| """The tracing release must never be the repository's Latest (that | ||
| belongs to the SDK's vX.Y.Z releases).""" | ||
| latest, why = _fetch_json(f"repos/{repo}/releases/latest", run_gh) |
There was a problem hiding this comment.
[P2] The promised Latest assertion fails open on every API error. _fetch_json() collapses auth, rate-limit, transport, and malformed responses into None, and this branch deliberately returns success; test_unreadable_latest_is_noted_not_fatal locks that in. make_latest=false constrains this PATCH only—it does not prove an already-published release was not later made Latest—so an idempotent rerun can report canonical success while the tracing release is actually Latest. Preserve HTTP/error classification, tolerate only the explicit no-Latest 404 case, and fail closed on 401/403/429/5xx/transport/unparseable responses.
There was a problem hiding this comment.
Fixed in f83011a. run_gh now returns (rc, stdout, stderr) and _latest_problem classifies: only an explicit HTTP 404 (no Latest exists — ours certainly is not it) passes; 401/403/429/5xx/transport failures and unparseable bodies all FAIL with "refusing to assume" messaging, on the idempotent path too. test_unreadable_latest_is_noted_not_fatal is replaced by TestLatestAssertion covering the 404-pass, each error class, unparseable bodies, and the idempotent rerun.
| # 0. Immutability is a PREREQUISITE, not a postcondition: GitHub does | ||
| # not retroactively protect releases published while the setting was | ||
| # off, so nothing may become public until the policy is attested. | ||
| setting, why = _fetch_json(f"repos/{repo}/immutable-releases", run_gh) |
There was a problem hiding this comment.
[P2] Checking the repository setting before the release state masks an already-public mutable release with false draft recovery. This branch returns before fetching the release and always says it is still an unpublished draft. I reproduced setting={enabled:false} plus a matching draft=false, immutable=false snapshot: only the setting call runs and the helper tells the operator to enable and rerun, instead of issuing the documented burn/containment guidance. That can leave a customer-visible mutable release exposed while the operator follows the wrong action; only the next run, after enabling the setting, discovers the burn. Fetch and classify the exact release first: a published mutable object must fail immediately with burn guidance, while the setting remains a mandatory precondition only for the draft=true publication path.
There was a problem hiding this comment.
Fixed in f83011a. The exact release object is fetched and classified FIRST; the policy setting now gates only the draft-publication path. Your reproduction — setting disabled + a published mutable snapshot — now fails immediately with the burn/containment guidance and never reaches the policy read (test_published_mutable_release_gets_burn_guidance_even_with_setting_off asserts the admin credential is never called and that no "still an unpublished draft" advice is emitted).
| ) | ||
| return 1 | ||
|
|
||
| if release.get("draft") is False: |
There was a problem hiding this comment.
[P2] A missing or malformed draft field falls through to publication. verify_snapshot() validates ID, tag, and assets but not visibility, and this condition only special-cases literal False; None, a missing field, or a string all proceed to the PATCH with draft=false. That contradicts this helper's fail-closed snapshot contract. Require draft to be a Boolean, allow only True into the publish branch and False into the published branch, and add malformed-response regressions. The workflow's earlier .draft classification should make the same schema check rather than treating every non-false value as a draft.
There was a problem hiding this comment.
Fixed in f83011a. verify_snapshot() requires draft to be a Boolean, so a missing/None/string flag is a snapshot problem that refuses publication before any PATCH; the publish branch is reached only via literal True/False. Regressions: TestDraftFlagSchema (None, "false", "true", 0, and a missing field). Both workflow jq classifications (guard discovery and finalize) now make the same schema check — .draft | type == "boolean" — and fail as indeterminate instead of treating non-false values as drafts.
| # assets become EMPTY placeholders so the reconciler still | ||
| # sees the exact-set / byte-identity mismatch and emits its | ||
| # tested classification. | ||
| ASSETS=$(jq -c '.[0].assets | map({id, name, size})' <<<"$MATCHES") |
There was a problem hiding this comment.
[P2] The round-13 asset trust boundary is tag-only inline shell with no executable coverage. These lines implement malformed-metadata rejection, duplicate/unsafe-name handling, the 100 MiB cap, the allowlist, placeholder semantics, and by-ID downloads, but no PR test executes the block or references MAX_ASSET_BYTES, releases/assets, or its error strings. The current unit tests begin only after files already exist in a release directory, so a regression here first runs while cutting a real tag. Extract asset planning into a PR-tested helper (or execute a marked shell section with stubbed gh/jq) and cover malformed metadata, duplicate/unsafe names, oversized expected assets, unexpected assets, empty sets, and a denied asset GET; assert only the four bounded expected IDs can be downloaded.
There was a problem hiding this comment.
Fixed in f83011a. The trust logic is extracted into PR-tested scripts/plan_asset_downloads.py: it validates metadata (positive integer ids — booleans rejected, non-empty safe names with '.'/'..' excluded, non-negative integer sizes, duplicate rejection) and emits a deterministic plan — download <id> <name> only for the four expected hardcoded names within the size cap, placeholder <name> for everything else. The workflow consumes the plan verbatim. Tests cover malformed metadata (10 shapes), duplicate/unsafe names, oversized expected assets, unexpected assets, the empty set, the CLI contract, and the core assertion that downloads ⊆ the bounded expected set. A denied asset GET remains covered by set -euo pipefail propagation in the consuming loop (verified in bash directly in round 12).
| 4. **`github-release`** — creates a **draft** release with all | ||
| artifacts + checksums and the pinned image reference in the body. | ||
| Nothing is customer-visible yet. | ||
| Nothing is customer-visible yet. A stale draft from an earlier |
There was a problem hiding this comment.
[P2] This still documents automatic stale-draft deletion, directly contradicting the round-13 behavior below. Lines 63-69 say the draft is deleted when both indexes return 404, while lines 73-77 and guard_existing_release.py correctly say the workflow never deletes a release and requires manual verify-and-delete. Rewrite this paragraph so both the normal flow and troubleshooting contract say the guard blocks and an operator may manually delete only after both explicit 404s.
There was a problem hiding this comment.
Fixed in f83011a. The RELEASING.md item-4 paragraph is rewritten: the guard never deletes; it classifies drafts via the authenticated release list and byte-validates each index (absent/exact/deviating), a stale draft with both indexes absent BLOCKS with manual verify-and-delete instructions, exact-index states route to original-attempt reruns (or draft recreation when only TestPyPI holds the exact bytes), and deviating bytes are a burn. The normal-flow paragraph and the troubleshooting section now state the same contract. The stale PR description is also refreshed (14 rounds, 11-state contract, 542 tests, no automated deletion, App-token + immutable-releases one-time setup).
…tes, tested asset planner (GoogleCloudPlatform#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.
caohy1988
left a comment
There was a problem hiding this comment.
Round 15 fresh review: one P2 finding after a full diff read and local verification (542 passed, 4 skipped; format/import/static workflow checks clean).
| # BQAA_RELEASE_POLICY_APP_PRIVATE_KEY (secret) — see | ||
| # RELEASING.md. | ||
| id: policy_token | ||
| if: env.state == 'complete' |
There was a problem hiding this comment.
[P2] Do not mint the policy token for an already-published idempotent rerun. complete is also the reconciler result for an exact release with release_published=True (test_published_release_with_complete_state_stays_complete), while the helper deliberately returns from the published/immutable path before its policy read and test_published_immutable_canonical_is_a_no_op asserts zero admin calls. This condition nevertheless runs the App-token action first. As a result, missing or rotated App credentials fail an otherwise idempotent rerun before the helper can recognize the finalized release (and can mask the helper’s burn guidance for an exact-but-public mutable release); it also contradicts RELEASING.md’s promise that the token is minted only when publication is about to happen. Export the matched release visibility/needs_policy_token from reconciliation and gate this step on complete && draft, then cover that workflow-level condition with a regression.
There was a problem hiding this comment.
Fixed in e11179a. The finalize reconcile step now exports RELEASE_IS_DRAFT (default 0; flipped to 1 only by the Boolean-checked draft classification), and the mint step is gated on env.state == 'complete' && env.RELEASE_IS_DRAFT == '1' — the already-published idempotent rerun and the exact-but-public mutable burn path never touch the App credentials, so missing/rotated keys cannot break them or mask the burn guidance. The requested workflow-level regression is the new test_release_workflow_contract.py: it pins the compound condition on the App-token step itself, the visibility export (default-first ordering, =1 write inside the Boolean draft branch), the ADMIN_GH_TOKEN wiring into the publish helper, and that exactly one mint exists with no other bare state == 'complete' guard that could re-widen the condition. RELEASING.md's token-mint promise updated to match.
…leCloudPlatform#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.
|
Final fresh review at I re-read the full diff against Verification:
One non-blocking bookkeeping item: the PR description still says fourteen review rounds and 542 tests; current reality is fifteen rounds and 546 tests. |
d7b41e3
into
GoogleCloudPlatform:main
Final code increment before the
tracing-v0.2.0tag — hardened across fifteen release-review rounds, every finding resolved with execution-proof.Contents
0.1.0→0.2.0; tag pipeline verifies version match AND exact protected-main-head provenance before minting credentials.release-promoteenvironment subject); environments tag-restricted; admin-only tag ruleset; all actions SHA-pinned in both workflows; crane pinned + checksummed +--password-stdin.${PYTHON_BASE}stages — literal/alternate-variable stages refused, with executed negatives); hash-locked runtime deps + build backend + the generator itself; bounded fixed-point convergence in fresh hash-locked containers with two distinct preserved diagnostics on exhaustion; signal-safe transactional copyback (six-position TERM-injection matrix proves all-old-or-all-new; per-invocation backup tracking so stale backups from an interrupted previous run are never restored); three-lock CI drift check; multi-stage image with zero build tools at runtime.yanked, strict hex digests,empty-releaseburn semantics per PyPI filename-reuse rules), absence-first recovery, release visibility as an orthogonal dimension (premature-publicationcontainment wraps every non-complete state, indeterminate included).--inputPATCH (gh raw fields do not read@files); Boolean-schemadraftflags enforced end to end; never repository Latest (only an explicit no-Latest 404 tolerated); immutable releases are a verified pre-publication prerequisite — the policy read uses a dedicated GitHub App installation token with only Administration:read (the jobGITHUB_TOKENcannot carry it), and a release published while the setting was off is burned, never "repaired".run_attemptidentities, artifact identity from the producing job, idempotent digest-checked promotion, byte-validated guard states (absent/exact/deviatingper index — an exact TestPyPI publication with a missing draft recreates it from the preserved original-attempt artifact; deviating bytes are an explicit burn), rerun-safe pre-upload gates on both indexes (absent/satisfied/conflict/indeterminate; exact local publish set enforced), fail-closed lookups with transport-error diagnostics and CDN cache-busting. The workflow never deletes a release automatically (no conditional delete exists — a stale draft blocks with manual verify-and-delete instructions).complete && draft).One-time setup required before the first release: enable immutable releases (Settings → General → Releases) and configure the policy-read GitHub App (
BQAA_RELEASE_POLICY_APP_IDvariable +BQAA_RELEASE_POLICY_APP_PRIVATE_KEYsecret) — seeproducers/RELEASING.md.Deliberate deviation:
prevent_self_reviewstays off (single-maintainer repo — it would deadlock every approval).Part of #349.