Skip to content

ci(release): gate publishes on a companion the built distribution works with (#1763) - #1788

Merged
bokelley merged 3 commits into
nextfrom
claude/issue-1763-release-gate
Nov 2, 2026
Merged

bokelley merged 3 commits into
nextfrom
claude/issue-1763-release-gate

Conversation

@bokelley

@bokelley bokelley commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Why

scripts/check_companion_releases.py refused an adcp publish only when no published adcp-reporting-storage range accepted it. adcp 10.0.0b4 changed the ledger column lists while the published storage (adcp>=9.0.0,<11) still accepted it, so the pair installed cleanly and failed every reporting turn (#1763). Range compatibility is not contract compatibility. Storage publishes were not gated at all.

Design

Two layers, one source of truth:

  1. A storage contract number on both sides. adcp_reporting_storage.contract.STORAGE_CONTRACT (fix(reporting-storage): upgrade 0.1.0b4/0.2.0b0 ledgers in place and pin the adcp storage contract (#1763) #1787) and adcp.reporting.ledger.storage_contract.REPORTING_STORAGE_CONTRACT (follow-up PR) name the ledger layout each side implements; they are bumped together when the shared column lists change. Releases that predate the declaration are classified deterministically: an adcp whose obligation column list carries offering_id is contract 2, else 1; a storage without the module is contract 1. The same comparison runs at service startup (runtime) and at release time (this PR).
  2. The gate installs the real pair and runs it. With --dist, the gate creates a private environment, installs the wheel just built plus the published companion, and runs scripts/companion_smoke.py there with -I. The smoke compares the contracts and, with ADCP_PG_TEST_URL, creates the ledger schema and round-trips a configuration, obligation and revision through the installed stores on PostgreSQL, the exact calls that failed on the incompatible pair. Only APIs shared by adcp 9.x and 10.x are used, so the gate runs on main and next.

Direction matters, so both are gated:

  • adcp publish: newest published storage whose range admits this adcp must pass the smoke.
  • adcp-reporting-storage publish: the built wheel must pass the smoke with the oldest and the newest published adcp its own Requires-Dist accepts, so a floor that admits an adcp of another contract is refused.

release-publish.yml now builds first, runs the gate for both packages against a postgres:16 service, then uploads.

Verified locally (PyPI + PostgreSQL 16)

pair result
adcp wheel built from next (contract 2) + published 0.2.0b0 refused: adcp reads the ledger with storage contract 2 but the storage implements contract 1
storage wheel from #1787 (floor >=10.0.0b4) + published adcp 10.0.0b4 pass, schema created, records round-trip
published storage 0.1.0b4 + published adcp 9.0.0 and 9.3.1 (SDK 9 line) pass, schema created, records round-trip

Unit tests cover the orchestration with a scripted runner (range-compatible-but-contract-incompatible companion blocks, install failure blocks, both directions, wheel metadata parsing).

Release ordering consequence

When the contract changes, publish the storage release that implements the new contract first (its floor is the first adcp of that contract), then the adcp release. Concretely for #1763: storage 0.2.0b1 (#1787) must be on PyPI before the next adcp beta can publish; this gate enforces that.

🤖 Generated with Claude Code

…ks with

scripts/check_companion_releases.py refused an adcp release only when no
published adcp-reporting-storage *range* accepted it. adcp 10.0.0b4 changed
the ledger column lists while the published storage (adcp>=9.0.0,<11) still
accepted it, so the pair installed cleanly and failed every reporting turn
(#1763). Range compatibility is not contract compatibility.

The gate now proves the pair works. With --dist, it installs the wheel just
built together with the published companion into a private environment and
runs scripts/companion_smoke.py there, which compares the storage contract
each side declares (adcp.reporting.ledger.storage_contract versus
adcp_reporting_storage.contract; releases predating the declaration are
classified by their column lists) and, with ADCP_PG_TEST_URL, creates the
ledger schema and round-trips a configuration, obligation and revision
through the installed stores on PostgreSQL, the calls that failed on the
incompatible pair. Storage publishes are gated too, in the other direction:
the built storage wheel is smoked with the oldest and the newest published
adcp its own requirement accepts, so a floor that admits an adcp of another
contract is refused.

release-publish.yml builds first, runs the gate for both packages against a
PostgreSQL service, then uploads. Verified locally against PyPI and
PostgreSQL 16: the adcp wheel built from next is refused against the
published 0.2.0b0 (contract 2 versus 1), the storage wheel from #1787
(floor 10.0.0b4) passes with 10.0.0b4, and the published 0.1.0b4 passes with
adcp 9.0.0 and 9.3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — release-gate hardening only (#1763), no blocking findings.

What I checked:

  • No no-auto-approve team match; gated_paths: false; high_risk: false — none of the hard gates apply.
  • Changes are confined to release tooling and docs: .github/workflows/release-publish.yml, RELEASING.md, docs/releasing.md, scripts/check_companion_releases.py, scripts/companion_smoke.py, and tests/test_check_companion_releases.py.
  • No public-API, wire-shape, or type-system surface touched — no adcp.* export, Pydantic model, discriminated-union, import-layering, generated-code, or ctx_metadata concerns triggered.
  • The new gate is strictly stronger than the removed range-only step: companion_smoke.py fail-closes (main() L594-601 converts any SystemExit/driver/DDL/assertion failure to a nonzero exit; smoke()/gate_adcp/gate_companion only return 0 on an actual pass); subprocess timeout=900 is bounded under the job's 120-min cap; the oldest+newest endpoint strategy is sound given monotonic contract numbering.
  • CI gates stay armed — a new test covers both publish directions, install failure, wheel-metadata parsing, and contract classification.

Reviewer returned a clean report with zero findings. No rows 1–8 fire; falls through to row 9.

…bed contracts

The release configuration test expected the companion gate to run only for
adcp publishes; it now runs for both packages after the build, with a
PostgreSQL service. The smoke classification test stubs both declaration
modules instead of assuming the workspace pair agrees, which it does not
until the storage declaration (#1787) lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — test-only delta refining the companion-smoke gate suite, no actionable findings.

This subsequent push (prior decision was approve, no findings) continues to be clean. The delta is test-only: two files refining the suite for the new companion-smoke gate. The reviewer traced both rewritten tests in test_check_companion_releases.py against companion_smoke.py — the declared-contract path, the mismatch-rejection message, and the pre-declaration fallback classifier all wire correctly to the real code paths, including the sys.modules=None import-block sentinel. test_release_configuration.py assertions match the reworked release-publish.yml (both packages gated, build-before-gate ordering, postgres service, ADCP_PG_TEST_URL).

Decision path: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); high_risk false (rows 3/5 n/a); no medium findings (rows 4/8 n/a); prior decision was approve, not escalate (row 6 n/a); no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.

…mn lists

The test asserted this checkout's column lists carry offering_id, which is
true on next and false on main; both classifications are now driven by
patched lists so the test is branch-agnostic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean subsequent pass, no blocking or medium findings.

This PR touches release tooling, docs, and tests (release-publish workflow, RELEASING.md, docs/releasing.md, companion-release scripts, and their tests). No changes to the adcp.* public surface, generated types, forward-compat unions, import layering, or credential handling — none of the repo's mandatory governance gates are implicated.

The delta since the prior approve is a single test reorder in tests/test_check_companion_releases.py: test_the_smoke_script_classifies_releases_without_a_declaration now exercises both the contract-1 pass path and the contract-2-vs-1 fail path, where the previous version only hit the failure branch. Classification logic (offering_id presence → contract 2) is correctly driven and the monkeypatched _record_rows is read via the cached module. Sound improvement in test coverage.

Gates: high_risk false, gated_paths false, review_decision APPROVED, no no-auto-approve team match. No findings. Falls through to row 9.

@bokelley
bokelley merged commit 21928ee into next Nov 2, 2026
41 checks passed
@bokelley
bokelley deleted the claude/issue-1763-release-gate branch November 2, 2026 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant