Repository navigation
ci: split release bump and tag admission - #818
Conversation
Exercise the version-only reset before tagging, preserving tagged behavior. Add refusal controls for parent count, extra paths, version direction, catalog bytes, existing tags and inherited release stamp constraints.
Require each release phase to use its own exact-commit CI evidence. Keep the bump untagged until its checks finish, then authenticate and tag that same commit. Validate the two-file transition and preserve catalog assets.
Record advisory verification and complete workflow attempt history, bind the decision to received response bytes, and refuse changed run metadata before admission. Keep release receipts on refusal and honor declared catalog paths. docs/ci.md: replaces the release-bump exemption sentence introduced by 72d9fd5 (#486).
Use timezone-aware UTC and keep exact output assertions aligned with each test input. Preserve every release safety condition and mutation control.
Cover authenticated advisory creators and first-attempt actor refusals. Reject malformed advisory states before logging them, retain sanitized unexpected-exception receipts, and distinguish invalid triggering actors from changed attempt metadata. Keep parity as the only required status.
Carry both strict admission forms in the hosted release workflow, retaining caller identity, artifact custody and exact-commit release evidence.
Keep historical matrix inputs independent from the phased release workflow and require both phase and dry-run boundaries on downstream publishing jobs.
Choose the expected guard and message from the complete dispatch declaration, while retaining strict workflow acceptance and both dry-run boundaries.
…hosted jobs WorkflowPinTests reads the #768 matrix rebuild of release.yml for its hosted-matrix contracts. That rebuild keeps the shipped two-phase release job, a combination the validator correctly refuses, and its single prebuilt job has no tag-phase guard. Four checks therefore failed. They now read the shipped workflow: - test_two_release_phases_stop_before_publication_until_tag_phase pins the exact job-level tag-phase if: of prebuilt-x86, prebuilt-arm and upload, and the closed job list. The notify-hub, gate, step-condition and writer pins are unchanged. - test_prebuilt_requires_release_success_without_failure_bypasses holds the release-success pin for the release job, both build jobs and the publishing upload job: needs: release on each build job, upload's exact needs: [release, prebuilt-x86, prebuilt-arm], no always(), !cancelled() or continue-on-error: outside comments, and no release_bump_workflow_findings. - test_prebuilt_failure_bypass_mutations_are_refused, both subtests, inserts each bypass into each build job and into upload and requires the same refusal. Each of those jobs first passes with the shipped "no continue-on-error" comment in it, so the pin is still shown to read keys, not words. The validator compares every split job header, so the pin now scans the build jobs and upload before the release job, and a refusal names its job and bypass. The other WorkflowPinTests tests keep reading the matrix rebuild. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve phase-aware release admission while incorporating the current main toolchain contract. Keep both Eigen flag constants and the mode parameter, and update the planned-phase fixture to the typed PATH flag. Co-Authored-By: Codex <[email protected]>
Retain the tagged VERSION and catalog baseline before exact-head validation. Co-Authored-By: Codex <[email protected]>
There was a problem hiding this comment.
Graphify reviewed this change.
Code graph out of date: Health delta baseline: last indexed commit f5e929d, 10 commit(s) behind this PR's base. Impact and hotspot numbers below may be wrong.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 23 changed function(s) (5 sampled, not proven, 18 not verified).
Formal verification. PR-changed functions: 5/23 verified (0 proven, 5 may-equivalent, 0 distinguished) · 18 not verified (7 vacuous, 11 unsupported).
Not verified on this run: \_release\_split\_job\_findings (vacuous: never exercised), \_release\_split\_steps (vacuous: never exercised), runner\_findings (vacuous: never exercised), \_ci\_post\_jobs (unsupported), \_ci\_post\_proof (unsupported), \_ci\_post\_runs (unsupported), \_phase\_one (unsupported), \_read\_json (unsupported), \_read\_text (unsupported), \_release\_bump (unsupported), and 8 more.
Graphify review — findings
Splits the release workflow into two dispatched phases: bump gates the dispatch SHA and pushes only the untagged VERSION/reset commit to main, and tag publishes that exact checked bump commit once its own full post-merge CI has ended green. The gate now runs ci_release_gate.py gate with phase, dispatch SHA and dry-run inputs, writes a receipt kept as an artifact, and plan replaces the old inline version computation; the tag phase never writes VERSION or makes a commit, and a retry reuses only a tag already at that same SHA. The final revalidation of the checked run and concurrent tag-phase runs creating the same GitHub Release are the places where this two-step handoff could go wrong.
Worth a look
- Final run revalidation has a rerun race —
docs/ci.md:1985· Escalate · high- unverified hypothesis — survived the adversarial refute pass; needs human review
- Concurrent tag runs can race to create the same GitHub Release —
.github/workflows/release.yml:376· Escalate · medium- unverified hypothesis — survived the adversarial refute pass; needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1907 functions depend on the 1849 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
ci_workflow_findings()— 93 callers, 25 callees - worse:
admit_rule_findings()— 9 callers, 9 callees - worse:
_job_block()— 22 callers, 3 callees - worse:
build_diff()— 8 callers, 8 callees - worse:
do_check()— 3 callers, 17 callees - worse:
_holds_credentials()— 6 callers, 7 callees - worse:
do_stamp()— 2 callers, 20 callees - worse:
main_admission_data_findings()— 11 callers, 3 callees - …and 15 more — each is listed as a finding
Verification — 1907 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit f5e929d, 10 commit(s) behind this PR's base.
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1875 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
1404 of 1404 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety (
noncode-change,uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.
benchmarks/tests/test_aggregate.py— full-run-safetybenchmarks/tests/test_check_provenance.py— full-run-safetybenchmarks/tests/test_compare.py— full-run-safetybenchmarks/tests/test_public_names.py— full-run-safetybenchmarks/tests/test_run_all.py— full-run-safetybenchmarks/tests/test_run_strategy_feeds.py— full-run-safetydocker/pineforge_sink_finalize_test.py— full-run-safetydocker/pineforge_sink_replay_rq_test.py— full-run-safetydocker/pineforge_sink_replay_test.py— full-run-safetydocker/pineforge_sink_to_json_test.py— full-run-safetydocker/quota_columns_test.py— full-run-safetydocker/quota_convert_test.py— full-run-safetydocker/quota_finalize_test.py— full-run-safetydocker/replay_rq_engine_test.py— full-run-safetydocker/run_json_artifact_window_test.py— full-run-safetydocker/run_json_bin_test.py— full-run-safetydocker/run_json_bounded_transport_test.py— full-run-safetydocker/run_json_bulk_equity_fuzz_test.py— full-run-safetydocker/run_json_bulk_equity_test.py— full-run-safetydocker/run_json_codes_test.py— full-run-safetydocker/run_json_converter_metadata_test.py— full-run-safetydocker/run_json_converter_session_test.py— full-run-safetydocker/run_json_diagnostics_test.py— full-run-safetydocker/run_json_equity_scan_test.py— full-run-safetydocker/run_json_fused_test.py— full-run-safetydocker/run_json_harnesses_test.py— full-run-safetydocker/run_json_metrics_only_test.py— full-run-safetydocker/run_json_misc2_test.py— full-run-safetydocker/run_json_misc_test.py— full-run-safetydocker/run_json_pfbar_test.py— full-run-safetydocker/run_json_provenance_test.py— full-run-safetydocker/run_json_quota_supervisor_test.py— full-run-safetydocker/run_json_replay_test.py— full-run-safetydocker/run_json_selected_sink_test.py— full-run-safetydocker/run_json_sink_lexical_test.py— full-run-safetydocker/run_json_sink_summary_test.py— full-run-safetydocker/run_json_sink_test.py— full-run-safetydocker/run_json_sink_vectors_test.py— full-run-safetydocker/run_json_spool_work_test.py— full-run-safetydocker/run_json_stringio_test.py— full-run-safetydocker/run_json_symbol_feeds_test.py— full-run-safetydocker/run_json_syminfo_test.py— full-run-safetydocker/run_json_trade_scan_test.py— full-run-safetydocker/run_json_window_budget_test.py— full-run-safetydocker/run_json_wrapper_work_test.py— full-run-safetyinclude/pineforge/native_run_spec.hpp— full-run-safetyscripts/derive_corpus_feeds_self_test.py— full-run-safetyscripts/fingerprint_self_test.py— full-run-safetyscripts/run_corpus_self_test.py— full-run-safetyscripts/test_abi_receipt_skips.py— full-run-safety- … and 1354 more
non-code file(s) changed (
.github/workflows/release.yml,docs/ci.md,docs/reference/merge-gate-and-promotion.md,scripts/fixtures/ci_preflight/release_split_phase_planned.yml) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
.github/workflows/release.yml,docs/ci.md,docs/reference/merge-gate-and-promotion.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
AGENTS.md§ Build (lines 12-25): references changed symbolsreceiptAGENTS.md§ What CI runs, per trigger (lines 46-70): references changed symbolsdiagnosticCHANGELOG.md§ Unreleased (lines 8-217): references changed symbolsinventoryCHANGELOG.md§ Report sink and scratch spill (lines 925-1136): references changed symbolsreceiptCHANGELOG.md§ run_json and CLI (lines 1137-1229): references changed symbolsreceiptCHANGELOG.md§ Run-failure codes ([#351]) (lines 2378-2468): references changed symbolsinputsCHANGELOG.md§ Migration (lines 2632-2668): references changed symbolsinputsCHANGELOG.md§ 1.3.0 — 2026-10-06 (lines 2669-2830): references changed symbolsreceiptCHANGELOG.md§ State hashes (lines 2831-2866): references changed symbolstagsCHANGELOG.md§ State hashes (lines 3109-3141): references changed symbolstags
…and 10 more.
Formal verification
Could not verify: Could not verify \_release\_split\_job\_findings.
The verifier did not have enough to check \_release\_split\_job\_findings, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 63 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_release\_split\_steps.
The verifier did not have enough to check \_release\_split\_steps, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_release\_split\_toolchain\_reader\_is\_pinned (not a proof).
The verifier ran both versions of \_release\_split\_toolchain\_reader\_is\_pinned on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in \_release\_split\_workflow\_findings (not a proof).
The verifier ran both versions of \_release\_split\_workflow\_findings on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in release\_bump\_workflow\_findings (not a proof).
The verifier ran both versions of release\_bump\_workflow\_findings on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify runner\_findings.
The verifier did not have enough to check runner\_findings, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_ci\_post\_jobs.
The verifier did not have enough to check \_ci\_post\_jobs, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_ci\_post\_proof.
The verifier did not have enough to check \_ci\_post\_proof, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `history` is annotated `History` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_ci\_post\_runs.
The verifier did not have enough to check \_ci\_post\_runs, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_phase\_one.
The verifier did not have enough to check \_phase\_one, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_read\_json.
The verifier did not have enough to check \_read\_json, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_read\_text.
The verifier did not have enough to check \_read\_text, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_release\_bump.
The verifier did not have enough to check \_release\_bump, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_require\_ci\_post.
The verifier did not have enough to check \_require\_ci\_post, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_require\_release\_push.
The verifier did not have enough to check \_require\_release\_push, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `api` is annotated `API` — outside the synthesizable primitive/collection set
Could not verify: Could not verify gate.
The verifier did not have enough to check gate, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 83 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify gh\_api.
The verifier did not have enough to check gh\_api, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 41 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly FileNotFoundError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify main.
The verifier did not have enough to check main, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 73 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_\_init\_\_ (not a proof).
The verifier ran both versions of \_\_init\_\_ on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in \_run (not a proof).
The verifier ran both versions of \_run on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify do\_check.
The verifier did not have enough to check do\_check, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `cfg` is annotated `Config` — outside the synthesizable primitive/collection set
Could not verify: Could not verify do\_stamp.
The verifier did not have enough to check do\_stamp, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `cfg` is annotated `Config` — outside the synthesizable primitive/collection set
Could not verify: Could not verify newest\_tag.
The verifier did not have enough to check newest\_tag, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 3 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
· 15 grounded finding(s) anchored inline below; 8 more finding(s) on lines outside this diff (see the check run).
|
|
||
| def _release_split_toolchain_reader_is_pinned(workflow: str, body: str) -> bool: | ||
| """Admit only the reviewed OIDC reader; scan all other raw credential inputs.""" | ||
| def _release_split_current_toolchain_reader_is_pinned(workflow: str, body: str) -> bool: |
There was a problem hiding this comment.
_release_split_current_toolchain_reader_is_pinned()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return not _holds_credentials(workflow, token_only_body, strict=True) | ||
|
|
||
|
|
||
| def _release_split_contract_findings(workflow: str, mode: str) -> list[str]: |
There was a problem hiding this comment.
_release_split_contract_findings()
fans out to 11 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -5084,17 +5534,71 @@ def release_rulesets_probe_workflow_findings(workflow: str, release: str) -> lis | |||
|
|
|||
|
|
|||
| def release_bump_workflow_findings(workflow: str) -> list[str]: | |||
There was a problem hiding this comment.
release_bump_workflow_findings()
fans out to 10 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _fields(run, ("created_at", "updated_at", "run_started_at", "html_url"))) | ||
|
|
||
|
|
||
| def _observe_run(api: API, repo: str, run: dict, record: dict) -> None: |
There was a problem hiding this comment.
_observe_run()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| validation["outcome"] = "pending" | ||
|
|
||
|
|
||
| def _revalidate_runs(api: _GateReads) -> None: |
There was a problem hiding this comment.
_revalidate_runs()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| "previous": release_version.previous_tag(parsed, release_version.parse_tags(tags))} | ||
|
|
||
|
|
||
| def _phase_one(api: API, repo: str, sha: str, *, allow_missing_pr: bool = False, |
There was a problem hiding this comment.
_phase_one()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
|
|
||
| def do_check(cfg: Config, git, require_tags: str | None = None) -> Outcome: | ||
| def do_check(cfg: Config, git, require_tags: str | None = None, *, |
There was a problem hiding this comment.
do_check()
fans out to 17 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return data | ||
|
|
||
|
|
||
| def release_stamp_diffs(cfg: Config, git, new_bytes: bytes, diff: dict, |
There was a problem hiding this comment.
release_stamp_diffs()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return diffs | ||
|
|
||
|
|
||
| def validate_release_transition(cfg: Config, git, *, expected_parent: str | None = None, |
There was a problem hiding this comment.
validate_release_transition()
fans out to 12 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return parent, version | ||
|
|
||
|
|
||
| def stamp_findings(diffs: list, to_version: str) -> list: |
There was a problem hiding this comment.
stamp_findings()
high coupling complexity (Ca·Ce = 15).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Release admission separates version bumping from tagging and retains the evidence behind each decision. The bump phase pushes only the untagged VERSION/catalog-reset commit. After that commit's own CI completes, the tag phase validates its checked parent and publishes that same commit without another bump. Dry runs retain admission checks and publish nothing.
This member retains caller identity, artifact custody, exact-commit release evidence and the strict admission forms while combining the phase split with the hosted toolchain release builds. Downstream publishing jobs require the tag-phase and dry-run boundaries. Release-control tests distinguish the historical legacy, split hosted and phase-planned workflow forms.
The release gate requires
pineforge/parity. The existingpineforge/verifyproducer remains in place; its authenticated state or absence is printed and retained as advisory. Verification-profile evidence cannot supply a missing status. The guard retains rerun history, requires the original App push and authenticated maintainer rerun permissions, rejects red or duplicate proving jobs and incomplete history, and preserves the established docs-only classification. Relied-on run identities are read again uncached before admission. Refusals, unexpected exceptions and receipt-write failures remain nonzero; the always-run upload retains sanitized decision evidence.Validation ran natively on linux/arm64 at exact commit
d42bc21609f8bb7096f2ac1246c82eda8422a198, tree17a6ac762d3a43594af2c3be519f3f4a537c0538, after merging the integration branch into the release-admission member:UNPROVEN_HOSTEDskip; 31 failure-catalog tests passed.The fixed release-gate corpus differential replay remains open. Asset uploads and complete release execution were not performed by these native checks. Existing general merge-gate references in AGENTS.md and the documentation still describe verification as required; the release-admission sections are updated here, with the general references remaining a separate follow-up. The existing ARM tarball retry branch and tag-phase catalog re-uploads use
--clobber; their overwrite semantics and retry coverage remain an explicit follow-up.Refs #789
🤖 Generated with Claude Code