ci(release): release-plz derives the bump and tags a merged release PR - #1007
Conversation
Claude Security ReviewHead: Findings: 0 — No findings The diff touches a trust boundary (new release-plz.yml CI workflow handling a write-scoped RELEASE_PLZ_TOKEN PAT), but it follows secure patterns (SHA-pinned action, no pull_request/pull_request_target trigger, minimal permissions blocks, secret scoped to single steps, env-indirection instead of direct ${{ }} interpolation in shell, and semver_check/compilation explicitly disabled in the secret-bearing steps to avoid running dependency build scripts with the token) with no exploitable issue found. Automated read-only review by Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1007 +/- ##
==========================================
- Coverage 95.79% 95.79% -0.01%
==========================================
Files 199 199
Lines 59163 59163
==========================================
- Hits 56678 56673 -5
- Misses 2485 2490 +5
🚀 New features to boost your workflow:
|
Claude Security ReviewHead: Findings: 0 — No findings The diff adds a release-plz.yml CI workflow, guards it with a repository-scoped PAT (never GITHUB_TOKEN) via env-var indirection into shell steps, restricts triggers to push-to-main/workflow_dispatch (no pull_request/pull_request_target), and adds matching OPA policy guards; the rest is UI-only version-popup rework with no trust-boundary contact, and I found no verified vulnerability. Automated read-only review by Claude Code |
Claude Security ReviewHead: Findings: 0 — No findings The diff adds a new PAT-driven release-plz CI workflow (touching the credential-handling trust boundary) plus unrelated UI/version-popup and doc changes; the new workflow follows safe patterns (SHA-pinned action, persist-credentials: false, minimal job permissions, repo/ref-gated triggers, secrets passed only via env to the pinned action step, no untrusted-PR code paths) and no exploitable vulnerability or invariant violation was found. Automated read-only review by Claude Code |
Review dispositions (two-lens gate, round 1)Both lenses returned APPROVE-WITH-NITS. Design lens: 6 MEDIUM. Correctness lens: 1 MEDIUM, 1 LOW. FIXED
REFUTED (with the mechanism)
SURFACED (owner's call, not done here)
|
Addendum — the one leg the correctness lens left open, now doc-citedThe lens flagged that GitHub's reusable-workflows reference settles it:
The caller is Free confirmation on the first release PR: |
Claude Security ReviewHead: Findings: 0 — No findings The diff's only trust-boundary-relevant surface is CI/CD credential handling in the new release-plz.yml workflow and the ci-builds.yml semver-gate change; both follow existing safeguards (SHA-pinned action, secrets passed via env not string-interpolated into shell, fork-safe repository/ref guards, minimal job permissions with the write-capable PAT scoped separately, and OPA policy enforcing the token-must-be-a-secret and dangerous-setting kill-switches), so no exploitable vulnerability or invariant violation was found; the remainder of the diff (TUI version-popup rewording, docs, justfile) has no security-relevant content. Automated read-only review by Claude Code |
Claude ReviewHead: Findings: 1 (0 high, 1 medium) Clean, well-tested PR that migrates releases to release-plz (CI workflows, justfile, Rego policy contracts) and simplifies the version popup to an "Updated to vX.Y.Z" notice linking to the GitHub release page, dropping the curated release_notes()/bump machinery it depended on; extensive cross-file consistency (cliff.toml skip pattern, release.yml tag glob, require-jobs per-step guarding) checks out under manual verification, and all removed call-sites/tests are fully accounted for — only a minor self-contradicting doc comment was found. 1. MEDIUM —
|
The only judgement left in the release flow was the bump level, picked by
hand in `just bump` and upheld "in review". release-plz derives it from
the conventional-commit log plus cargo-semver-checks, so the human steps
become: dispatch, regenerate the committed art on the PR, merge.
release-plz owns versions and the tag ONLY; release.yml is untouched and
still owns every irreversible publish (crates.io + npm over OIDC, the
GitHub release, provenance). `publish = false` keeps it that way, and
`git_tag_name = "v{{ version }}"` is what release.yml's `v*` trigger and
homebrew-core's autobump need — release-plz's multi-package default
(`{{ package }}-v{{ version }}`) would match neither.
`semver_check = false`: the step holding the PAT must run no compiler,
or every dependency's build script runs with a write credential in its
environment. ci-builds' `semver` job answers the same question on the
release PR instead, guarded per STEP because require-jobs reds a skipped
job. That is also where the per-PR gate went: between releases main
carries unreleased changes at the shipped version, where a baseline
check reds by design.
`release-pr` is dispatch-only. release-plz refreshes an open release PR
on every push to main, closing and re-opening one that carries non-bot
commits — which would orphan the human's regenerated stills on the next
dependabot merge.
Two silent failures get mechanisms, both being classes that reach green:
- a tag created with GITHUB_TOKEN starts no workflow, so `release.yml`
would never fire. Policy pins both release-plz steps to a repository
secret (never the automatic token), by the effective step<job<workflow
env the zizmor token rule already resolves, generalised over the name.
- release-plz tags only a merged `release-plz-*` branch and DECLINES
silently otherwise. The `tag` job asserts main's workspace version is
tagged — true at rest on every push, false exactly when a bump landed
untagged.
The six policy rules pin what is otherwise invisible: two live action
steps carrying the exact `{release-pr, release}` command set (the action
runs BOTH when `command:` is absent, so a step that loses the input
opens a release PR on every push while the count still reads two), each
`[workspace]` kill switch with the reason true for IT, exactly one
tagging package, and the two cross-file pairs a one-sided edit breaks —
`git_tag_name` against release.yml's tag glob, `pr_name` (the squash
subject) against cliff.toml's skip parser. `release-plz.toml` and
`cliff.toml` join the conftest document set; yq fixes its input format
per call, so the TOML pair rides a second call joined with jq.
`pr_branch_prefix` is declared rather than inherited: it is release-plz's
own default, and three of our mechanisms key on that literal.
Needs the RELEASE_PLZ_TOKEN secret (fine-grained PAT, this repository,
Contents + Pull requests read/write) BEFORE this reaches main — the
`tag` job runs on every push and the action refuses an empty token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CBh53j1hrMRy1WXJttrgc
`gh api .../git/ref/tags/v0.5.139` returns the annotated TAG object's sha; Actions resolves `uses:` against commits, so the pin named an object no checkout could reach. zizmor's `ref-version-mismatch` says so — but only ONLINE, which is why a green local run (offline by default) and a red CI hygiene job disagreed. Reproduced both ways with GH_TOKEN set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`conftest fmt` reformats Rego with the OPA it embeds, and a brew dev build embeds a newer one than the pinned v0.68.2 (OPA 1.15.2): the two disagree on whether a one-entry object literal wraps, so a locally formatted file reds CI's `fmt --check`. Reproduced by installing the pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`pr_branch_prefix` decides which PRs release-plz tags AND which PRs run the bump gate, but ci-builds.yml's `semver` guard carried the literal independently: either side edited alone leaves every release PR ungated, silently. The two sibling pairs were already pinned; this is the third. Verified by mutation — changing the guard literal alone now fails ci-observability, and the real files stay silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… draft release Three of the four toggles this migration opened at `false` were closing a feature the repository then re-implemented elsewhere. Upstream's own shape (and Zebra's, and augurs') is to let release-plz own them: - the changelog moves into `release-plz.toml`'s `[changelog]` section, so `cliff.toml` is DELETED and one format now renders both `CHANGELOG.md` and the GitHub release body. release.yml's git-cliff step goes with it. Verified against a real run: four crates produce ONE changelog file. - `cargo-semver-checks` runs where the bump is chosen — release-plz writes the verdict into the release PR. ci-builds' `semver` job, its per-step release-PR guard, `just semver` and the `pr_branch_prefix` pair-pin that existed only to hold that guard's literal all retire with it. - release-plz creates the GitHub release as a DRAFT carrying the changelog, and release.yml fills it. `softprops/action-gh-release` retains every release key it is not given and publishes a draft once assets are attached, so the version is never visible without its binaries and the job loses its `body:` and `prerelease:` inputs (the latter to `git_release_type = "auto"`). `git_tag_name` moves to the WORKSPACE level: every package's changelog entry builds a compare link from it, and the per-package default names `pixtuoid-core-v0.18.0`, a tag that does not exist. Verified — the link is now `compare/v0.18.0...v0.19.0`. `publish` stays false, for a different reason than before. The ordering argument was weak (crates.io ships source, the matrix ships binaries, and a musl link failure does not make the published crate wrong). The real constraint is that crates.io Trusted Publisher records are bound to the workflow presenting the OIDC claim, and all four crates name release.yml — flipping it needs a release-plz.yml publisher added to each crate FIRST, or the release fails after the tag exists. Policy follows: the kill-switch map drops the two keys that are now true, gains the reason crates.io imposes on `publish`, and pins that exactly one package drafts the release. The skip-parser pair-pin becomes in-file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
09d46dc to
717cea4
Compare
Reworked: three of the four toggles now let release-plz own the thingFollow-up to the review above. The design gate and both lenses judged the original shape (everything release-plz does disabled, re-implemented in Now true
A bug the real run exposed:
Policy follows: the kill-switch map drops the two keys that are now true, carries the crates.io reason on Gates: |
Claude Security Review — did not runHead: The automated second lens is ABSENT, not clean. No findings were In the run log, |
Claude ReviewHead: Findings: 1 (0 high, 1 medium) This PR replaces the manual 1. MEDIUM —
|
The `semver` job left ci-builds.yml in this PR, but both reviewer prompts still named it among the gates a diff can red — a future reviewer would have gone looking for a gate that no longer exists on ordinary PRs. CONTRIBUTING's own CI-gates section was updated; these two were not. Found by the automated reviewer on the reworked head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude Security ReviewHead: Findings: 0 — No findings The diff only touches CI/release tooling (release-plz workflow, CI job config, rego policy, docs, justfile) and a test message — none of it touches the hook shim, socket/pipe transport, config writes, untrusted transcript/JSONL/pack ingestion, or app-level credential/subprocess handling, so there are no security-relevant changes. Automated read-only review by Claude Code |
Claude ReviewHead: Findings: 0 — No findings Coherent migration of the release process from the manual Automated read-only review by Claude Code |
The last of the four toggles. My original reason for holding it — don't publish until all six targets build — was weak: crates.io ships SOURCE and the matrix ships BINARIES, so a musl link failure does not make the published crate wrong, and `cargo publish` verifies the source by building it. Of ten surveyed repos only cargo-generate gates it. The real constraint was crates.io's, not ours: a Trusted Publisher record is keyed on the workflow FILENAME (`NewGitHubConfig.workflow_filename` in crates_io_api_types), and all four crates named release.yml. The owner has added release-plz.yml records, so the claim now verifies. release.yml keeps everything release-plz does not produce: the six-target build matrix, the debs, the assets, the provenance attestation, and npm — whose packages wrap those binaries. Its `publish` job (59 lines, its own index-propagation retry) retires in favour of release-plz's `wait_until_published`. The `tag` job grows `id-token: write` for the OIDC exchange and an ALSA step, because `cargo publish` builds each crate and the default `audio` feature reaches alsa-sys. Its timeout goes 10 -> 30 minutes to match. Policy drops `publish` from the kill switches; the reason it carried now lives on the config key, where it names the filename coupling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude Security ReviewHead: Findings: 0 — No findings This diff only touches CI/release-pipeline files (workflows, justfile, Rego policy, release-plz.toml, docs) and the credential-handling changes it does make — SHA-pinned release-plz action, per-job least-privilege permissions, fork/ref-guarded triggers, and env-var (not inline Automated read-only review by Claude Code |
Claude ReviewHead: Findings: 1 (1 high, 0 medium) One high-confidence regression: the new release-plz-based flow makes the irreversible crates.io publish unconditional on merge to main, dropping the previous explicit gate that required the full 6-target build+deb matrix to succeed first. 1. HIGH —
|
…nd-tag path Round-2 review of the rework. Three real findings: The `semver_check = false` pin this PR deleted carried the WHY for a threat that flipping it REALIZES: cargo-semver-checks compiles inside the step holding RELEASE_PLZ_TOKEN, so every dependency build script runs with a repository-scoped write credential. `publish = true` moves `cargo publish`'s build there too — previously that build ran in release.yml's OIDC-only job with no PAT. The decision stands; its record should not have gone with the pin. release-plz.yml now states the accepted risk where the credential is declared, and names the mechanism that bounds it (a GitHub App installation token). The hand-tag recovery path — which this workflow's own error message offers — has no draft release to inherit from, so `action-gh-release` creates one with `prerelease` unset and an `-rc.N` tag lands as the latest stable release. The `prerelease:` input deleted with the draft handoff comes back; on the normal path it restates what `git_release_type = "auto"` already chose. The error message now says that path also loses the changelog body, and that re-running is the fix that keeps it. `git_tag_name` was declared twice, and policy pinned only the package copy — so the workspace copy, the one every changelog compare link reads, could drift unpinned. One declaration now, and the rule resolves it the way release-plz does (package override, else workspace). Also five stale references the sweep found, including CONTRIBUTING contradicting itself about whether `semver-checks` is a CI job, and the two rego identifiers still named after the deleted cliff.toml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-2 design lens. The job that publishes four crates to crates.io and then tags was called `tag`, which hid the irreversible half from anyone reading the Actions list; it is `release` now. The crates.io Trusted Publisher record is keyed on this workflow's FILENAME, so renaming the file stops publishing until every record is updated. That was stated in release-plz.toml and in CONTRIBUTING — neither is where someone renaming a workflow looks. It moves to the file's own header. `semver` was filed under CONTRIBUTING's #ci-gates, a list headed "these run only in CI"; it is not a job at all. It moves to #releasing, with what it actually does: a detected break RAISES the bump rather than failing, so the release PR already carries the corrected version. `just semver` comes back, marked local-only and not a gate. Deleting it left no way for a human to see that verdict before dispatching, which is the one thing the old CI job gave them. Refuted, with the citation: the lens read release-plz's semver check as advisory, so a breaking change under a `fix` subject would ship as a patch. Upstream's semver-check.md says the opposite — "release-plz updates the version of the package with a major semver Bump", 0.4.21 -> 0.5.0 — so it corrects the bump instead of reporting on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 2 dispositionsTwo lenses reviewed the delta FIXED
REFUTED, with the citation The design lens read release-plz's semver check as advisory — "a breaking change landing under a VERIFIED by the lenses against pinned sources, not READMEs
OPEN — needs an owner decision, and it is the last one
Three shapes, owner's call:
|
Claude Security ReviewHead: Findings: 0 — No findings The diff replaces the manual crates.io release process with release-plz.yml (new workflow with RELEASE_PLZ_TOKEN, id-token OIDC, and OPA policy guards); permissions are least-privileged (top-level permissions:{}, job-scoped grants), the token is enforced by policy to be a real repo secret rather than the automatic GITHUB_TOKEN, the action is SHA-pinned, checkouts use persist-credentials:false, and the one real risk (dependency build scripts executing in a token-bearing step during cargo publish/semver-checks) is explicitly documented in-file as an accepted risk with a stated mitigation path — no exploitable vulnerability or unverified credential/permission gap was found. Automated read-only review by Claude Code |
Round-2's open HIGH: `publish = true` leaves the crates.io upload with no CI gate — the `release` job has no `needs` and `ci.yml` runs on the same push concurrently, so release-plz can publish before `ci-gate` finishes. CONTRIBUTING asked the human to update the branch first, and CLAUDE.md is explicit that prose has no failure mode. `main` now requires branches to be up to date (owner's call, applied to the branch protection). That makes the guarantee structural: a release PR cannot merge unless its tree IS main's tree, and that tree is the one its own CI already passed. The runbook says what the setting buys instead of asking for the behaviour, and #releasing now states why the ungated upload is safe. Two references to the job's old name follow the rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude Security ReviewHead: Findings: 1 (0 high, 1 medium) The diff touches a credential-handling trust boundary — a new release-plz.yml workflow runs cargo compilation (semver-checks / publish) inside the step that holds a repo-write-scoped RELEASE_PLZ_TOKEN, a self-acknowledged but real exposure of that token to arbitrary dependency build scripts, while the rest of the CI/workflow/policy changes are safe refactors with no new attack surface. 1. MEDIUM —
|
Claude ReviewHead: Findings: 2 (1 high, 1 medium) Release automation migration to release-plz is well-designed (rego policy pins the config/token/tag-prefix contract tightly, with matching tests), but the new 1. HIGH —
|
|
SURFACED, merging anyway — the security reviewer's MEDIUM at Owner's call to accept it for now. The bound is real but narrow: the PAT is fine-grained, scoped to this repository, and carries only Contents + Pull requests. The named mechanism is a GitHub App installation token (~1 h, Ecosystem check for the record: of ~1968 workflow files invoking |
…first crate (#1012) * fix(release): the changelog covers the whole workspace, not just the first crate The v0.19.0 release PR's CHANGELOG.md held 12 entries — every one a commit touching `pixtuoid-core`. The board revamp (#1009), the version popup's move to GitHub release notes (#1006) and everything else in scene/bin were absent, and that file is the draft release's body: the very notes the popup now sends users to. Cause: `changelog_path = "./CHANGELOG.md"` sat at WORKSPACE level, so all four packages wrote the same `## [0.19.0]` heading to one file; the first writer won and the PR body showed four identical sections. release-plz's documented lever is `changelog_include` ("include commits that belong to other packages"), and its own release-plz.toml (fetched) uses exactly this shape: the released package owns the root changelog and names the rest. Workspace `changelog_update = false` keeps the other crates from writing per-crate files. Verified with `release-plz update` 0.3.169 on a throwaway worktree of main: 12 -> 19 entries, incl. #1009 tagged **breaking**, #1006, #1010, #1007; no per-crate CHANGELOG.md appears. Commits touching no workspace member (site/, ci) stay out by design. Pinned, because the failure is silent: `changelog_update` joins the workspace kill-switches, and the package that carries the draft release must write the changelog AND name every other member of Cargo.toml's `[workspace].members` — Cargo.toml becomes a policy input. Checked against the real files: dropping `pixtuoid-web` from the list reds `just ci-observability` by name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(release): review round 1 — the census counts PUBLISHED members and fails closed Both lenses approved with nits; no HIGH. Each read release-plz 0.3.169's source (the version the pinned action installs), and one ran `release-plz update` in a scratch clone with probe commits. - `pixtuoid-web` in `changelog_include` was inert: release-plz drops a `publish = false` crate from an update entirely (`takes_part_in_release`), and an included name it cannot find is skipped without a warning — a web-only probe commit stayed out of the notes either way. The gate nevertheless REQUIRED the entry and its message claimed the crate "vanishes" without it. The census now reads each member's own manifest (name + `publish`), which also retires the unpinned "directory name == package name" assumption; web is dropped from the list. - The census failed OPEN: with Cargo.toml missing from the policy input the set difference was empty and the rule green. Two existence rules close it — the release package must itself be a published member, and every member's manifest must be in the input. Checked on the real files: dropping `pixtuoid-hook` reds by name; a recipe that stops feeding the member manifests reds both rules. - The kill-switch's WHY described the old failure, not what a flip does now (every other crate grows its own CHANGELOG.md). The `git_tag_name` rationale went stale with this change (only one package has changelog entries now; every package still finds its last release by that tag). The recipe is cited to the doc that matches it ("extra/single-changelog", One package), not to upstream's own config, which keeps per-crate files. - Runbook: when `main` moves under a release PR, RE-DISPATCH — never "Update branch". Only a dispatch recomputes CHANGELOG.md, and the merge commit counts as a human's, so the next dispatch closes the PR and opens a new number (what happened to #1008). Also: a user-facing change that touched no packaged file is not in the generated notes; add it by hand as the last commit. Not re-run locally after the fold: `release-plz update` needs ~GBs for its rustdoc builds and the disk is full. The only config delta since the reviewed run is the removal of the entry proven inert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Summary
The only judgement left in the release flow was the bump level: a human picked it, typed
just bump X.Y.Z, and "features also bump minor" was upheld in review. release-plz derives it from the conventional-commit log plus cargo-semver-checks and opens the release PR, so the human steps become dispatch →just genon the PR → merge.release-plz owns versions and the tag only.
release.ymlis untouched and still owns every irreversible publish (crates.io + npm over OIDC, the GitHub release, provenance).just bumpis deleted.Related issue
n/a — owner call, 2026-09-20. Stacked on #1006 (the version popup no longer ships notes, so the release PR has nothing to hand-curate). Merge that one first.
Type of change
Config, and why each key
release-plz.tomlcarries the WHY on every key; the load-bearing ones:publish = falserelease.yml's OIDC jobs — no standing tokengit_tag_name = "v{{ version }}"onpixtuoidalone{{ package }}-v{{ version }}, which matches neitherrelease.yml'sv*trigger nor homebrew-core's autobumpchangelog_update = falserelease_always = falserelease-plz-*branch tagsfeatures_always_increment_minor = truesemver_check = falseNo
version_group: release-plz already takes the max next version for everyversion.workspace = truepackage before the group is consulted (updater.rs), verified by running it here with and without — identical output.Where "what bump?" is answered now
ci-builds'
semverjob, narrowed to release PRs and guarded per step (a job-levelifwould make itskipped, whichrequire-jobsreds). Between releases main carries unreleased changes at the shipped version, where a baseline check reds by design — the question only exists on the release PR. A red there means raise the bump on the branch withcargo set-version --workspace.Two silent failures, two mechanisms
Both are classes that reach green while the release quietly does not happen:
GITHUB_TOKENstarts no workflow, sorelease.ymlwould never fire. Policy pins both release-plz steps to a repository secret (never the automatic token), resolved through the effective step<job<workflowenvthe zizmor token rule already walks — generalised over the variable name rather than copied.release-plz-*branch. Thetagjob asserts main's workspace version is tagged on origin: true at rest on every push, false exactly when a bump landed untagged.Policy also pins what else is invisible: two live action steps carrying the exact
{release-pr, release}command set — the action runs BOTH commands whencommand:is absent, so a step that loses the input opens a release PR on every push while the count still reads two — each[workspace]kill switch with the reason true for it, exactly one tagging package, and the two cross-file pairs a one-sided edit breaks (git_tag_nameagainstrelease.yml's tag glob,pr_nameagainstcliff.toml's skip parser). Sevendenyrules with tests;release-plz.tomlandcliff.tomljoin the conftest document set (yq fixes its input format per call, so the TOML pair rides a second call joined with jq).Why
release-pris dispatch-onlyrelease-plz refreshes an open release PR on every push to main, and closes and re-opens one that carries non-bot commits (
release_pr.rs) — which would orphan the human's regenerated stills on the next dependabot merge, and re-run the full CI matrix each time.How I tested it
main: 0.18.0 → 0.19.0, "(✓ API compatible changes)", diff = exactly the filescargo set-versiontouched.release --dry-run:pixtuoid: creation of tag 'v0.19.0'; core/scene/hook "no release method enabled".just lintgreen (includesci-observability: 133 Rego unit tests, 113 policy checks).Review (two-lens gate, round 1 folded)
Design-gate lenses (best-practice / adversarial / deepening) ran on the spec before any code; two review lenses ran on the diff. Folded: the command-census hole above, a step-level
if:that could empty the existence rule, one shared kill-switch message that was false for four of five keys,pr_branch_prefixdeclared rather than inherited, a${{ }}interpolated into arun:block (zizmor), the secret's real deadline, and five one-home-per-story trims.SURFACED to the owner, not fixed here:
tagjob is the shallow half: with publish/release/changelog off,release-plz releasereduces to a tag-exists check plus a create-ref, and the assertion step then re-checks the same invariant. Agh apicreate-ref would delete the silent-decline class and the per-push install of three cargo tools — at the cost that a hand-bumped merge would publish with no second human click.cargo semver-checks --release-type <t>keyed off the PR's conventional marker would restore it, as its own PR through the design gate.Checklist
docs/CONTRIBUTING.md#releasing/#versioning/#ci-gates,CLAUDE.md).just preflightpasses locally.AI assistance
🤖 Generated with Claude Code