Skip to content

build(release-automation): R4 release-plz active mode (workspace-style tags + CHANGELOG)#148

Merged
githubrobbi merged 2 commits intomainfrom
feat/release-auto-r4-active-mode
May 8, 2026
Merged

build(release-automation): R4 release-plz active mode (workspace-style tags + CHANGELOG)#148
githubrobbi merged 2 commits intomainfrom
feat/release-auto-r4-active-mode

Conversation

@githubrobbi
Copy link
Copy Markdown
Collaborator

Summary

Phase R4 of docs/architecture/release-automation-plan.md — flips .github/workflows/release-plz.yml from R3 SHADOW mode (release-plz update, local-only) to ACTIVE mode (release-pr opens/updates the release PR; release creates the tag + GitHub Release on PR merge).

This is the workflow-flip-only R4 PR. The first-release v0.5.91 bootstrap (Cargo.toml version bump + hand-written CHANGELOG entry + tag push) is performed manually by the maintainer in a separate operation — see plan §R4 decision 7 + baseline §11 "Maintainer bootstrap procedure".

Settled-pre-execution decisions

Recorded in release-automation-plan.md §R4 + release-automation-baseline.md §11 for durability:

# Decision Rationale
D1 Workspace-style tags (single v{{ version }}, not per-crate) UFFS is one product (12 crates lockstep); existing release.yml on: push: tags: [v*] trigger; matches cargo + rustls
D2 Workspace-style CHANGELOG (12 [[package]] changelog_path overrides) UFFS has had single CHANGELOG since v0.4.x; release-plz forbids workspace-level changelog_path
D3 git_only = true baseline UFFS unpublished through R8; flips back in R8 once ≥1 crate live (forward-compat note in config)
D4 release_commits regex (feat/fix/perf/security only) Mirrors cliff.toml commit_parsers; suppresses chore/docs/ci/build/refactor/test/style/revert churn
D5 Two-job workflow (release-plz-pr + release-plz-release) release-plz/action has no "do both" command; mirrors release-plz's own workflow
D6 Default GITHUB_TOKEN, NOT GitHub App / PAT (DOCUMENTED LIMITATION) Tags via GITHUB_TOKEN don't trigger downstream release.yml (anti-loop policy); 3 workarounds documented; "R4.5" follow-up sets up Option A
D7 First-release v0.5.91 bootstrap is OUT OF SCOPE v0.5.90 worktree predates R3.5 dep-version fix; baseline check fails on first run; maintainer manually bumps + tags from current main to bootstrap

Files modified

File Change LOC
release-plz.toml + R4 fields (git_only, git_tag_name, git_release_name, release_commits); + 12 [[package]] blocks with changelog_path = "CHANGELOG.md"; header rewritten for R4 phase +220
.github/workflows/release-plz.yml Replaced R3 shadow body with two-job active pattern using release-plz/action@1528104d (v0.5.128 SHA-pinned). contents: read → write, pull-requests: read → write at job level. R3 manual diff-capture removed. rewritten
docs/architecture/release-automation-plan.md §R4 rewritten with D1-D7; dashboard updated (R3.5 + R6 promoted 🟡→🟢 with PR #145; R4 🟡 in-progress); 2 new §8.1 deviations entries +181 / -150
docs/architecture/release-automation-baseline.md §11 R4 addendum appended (D1-D6 with rationale, ecosystem precedent, reversibility cost; maintainer bootstrap procedure) +119

Net diff: 4 files, +662 / -275 = +387 LOC (mostly comments + docs).

Publishing dormancy (UNCHANGED from R3 → R6)

Belt-and-suspenders dormancy through Phase R8. See plan §6.

  1. release-plz.toml workspace publish = false (first layer).
  2. No CARGO_REGISTRY_TOKEN env var passed to the action (second layer).

Both layers stay through R7 (OIDC scaffolding) and only flip in R8 (publishing dress rehearsal for uffs-time).

Validation

  • cargo check --workspace --all-targets
  • cargo fmt --check --all
  • taplo format --check release-plz.toml
  • actionlint .github/workflows/release-plz.yml
  • typos / reuse lint
  • just lint-pre-push20/20 gates green (52s post-commit)

What this PR deliberately does NOT do

  • Does NOT bump any crate version. Bootstrap is out-of-band (maintainer task).
  • Does NOT delete auto-tag-release.yml. R5 scope (after ≥2 R4-flow releases bake in).
  • Does NOT set up a GitHub App / PAT. R4.5 follow-up.
  • Does NOT publish anything to crates.io. Workspace publish = false + missing CARGO_REGISTRY_TOKEN unchanged.
  • Does NOT modify release.yml. Trigger contract preserved.

Rollback

git revert flips the workflow back to R3 shadow mode. The release-plz.toml R4 additions are additive — revert independently if needed. Any tags created by future release-plz active runs are idempotent (tag-exists guard in release.yml).

Bake-in plan

After merge, observe one push to main:

  1. release-plz-pr runs and either opens a release PR (if any qualifying commits since v0.5.90) or no-ops.
  2. release-plz-release runs and silently no-ops (HEAD is not yet a release-PR merge).

Then maintainer performs the v0.5.91 bootstrap per baseline §11 procedure.

References

…e tags + CHANGELOG)

Phase R4 of `docs/architecture/release-automation-plan.md`.  Flips
`.github/workflows/release-plz.yml` from R3 SHADOW mode (`release-plz
update`, local-only) to ACTIVE mode (`release-plz/action@v0.5.128`
with `command: release-pr` + `command: release` in two parallel jobs,
mirroring release-plz's own recommended workflow shape).

Settled-pre-execution decisions (recorded in plan §R4 + baseline §11):

* D1.  Workspace-style tags — single `v{{ version }}` per release
  (`git_tag_name = "v{{ version }}"`) instead of release-plz's default
  `{{ package }}-v{{ version }}` per-crate scheme.  Honors UFFS as one
  product (12 publishable crates moving in lockstep, sharing
  `[workspace.package].version`) and keeps the existing `release.yml`
  `on: push: tags: [v*]` trigger working with zero migration.  Same
  pattern as cargo and rustls workspaces; diverges from tokio.

* D2.  Workspace-style CHANGELOG — 12 per-package `[[package]]` blocks
  with `changelog_path = "CHANGELOG.md"` flatten the per-crate
  changelog into the workspace-root file.  `changelog_path` cannot be
  set at workspace level (release-plz docs explicitly forbid it).

* D3.  `git_only = true` workspace baseline — UFFS is unpublished
  through R8, so the crates.io registry has no version data to diff
  against.  release-plz uses git tags as the baseline instead.  Forward-
  compat note for R8: `git_only = true` and `publish = true` are
  mutually exclusive, so R8 will flip this back when ≥1 crate goes live.

* D4.  `release_commits` regex filter — restricts release-PR triggers
  to `feat:` / `fix:` / `perf:` / `security:` commits, matching the
  set that `cliff.toml`'s `commit_parsers` maps to changelog sections.
  Without this, every `chore:` / `docs:` / `ci:` push would re-open
  the release PR with no-op churn.  Single source of truth for the
  suppression list.

* D5.  Two-job workflow — `release-plz/action` does NOT have a single
  "do both" command.  Per the release-plz repo's own workflow, R4
  ships two parallel jobs: `release-plz-pr` (opens/updates PR) and
  `release-plz-release` (creates tag on release-PR merge, no-ops
  otherwise).

* D6.  Default `GITHUB_TOKEN`, NOT GitHub App / PAT — known limitation:
  tags created by release-plz via `GITHUB_TOKEN` do NOT trigger
  `release.yml` (per GitHub's anti-loop policy).  Three workarounds
  documented in the workflow header (GitHub App = canonical; PAT =
  simpler; `release.yml workflow_run` trigger = third option).  R4
  ships with default token to minimize new infra; an "R4.5" follow-up
  PR sets up Option A.

* D7.  First-release v0.5.91 bootstrap — out of scope for this PR.
  The v0.5.90 worktree predates R3.5's dep-version fix (cccf4f1),
  so release-plz's `git_only` baseline check fails when comparing HEAD
  against v0.5.90 — surfaces as "no version bump proposed" in the
  first few R4 runs.  Maintainer manually bumps `[workspace.package]
  .version` 0.5.90 → 0.5.91 + hand-writes the CHANGELOG entry + pushes
  the tag (user push triggers `release.yml` normally).  After
  bootstrap, release-plz takes over.

Publishing dormancy (UNCHANGED from R3 → R6, see plan §6):
  * `release-plz.toml` workspace `publish = false` (first layer).
  * No `CARGO_REGISTRY_TOKEN` env var passed to the action (second
    layer).  Both stay through R7 (OIDC scaffolding) and only flip in
    R8 (publishing dress rehearsal for `uffs-time`).

Files modified:
  * `release-plz.toml` (~220 LOC added) — `git_only`, `git_tag_name`,
    `git_release_name`, `release_commits`, 12 `[[package]]` blocks
    with `changelog_path = "CHANGELOG.md"`.  Header comment block
    rewritten for R4 phase.  R6 `release = false` blocks for internal
    tools preserved.
  * `.github/workflows/release-plz.yml` (rewritten) — two-job pattern
    (`release-plz-pr` + `release-plz-release`) using
    `release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11`
    (v0.5.128, SHA-pinned per supply-chain hygiene).  Permissions
    elevated `contents: read → write`, `pull-requests: read → write`
    at job level.  Manual diff-capture and "verify no git mutation"
    guard (R3 shadow-only) removed.
  * `docs/architecture/release-automation-plan.md` (§R4 rewritten,
    dashboard updated) — captures D1-D7 above; R3.5 + R6 dashboard
    rows promoted 🟡 → 🟢 (PR #145 cccf4f1); R4 row 🟡 in-progress
    pointing at this PR; two new §8.1 deviations log entries (R4
    baseline self-healing transient + R4 downstream-trigger
    GITHUB_TOKEN limitation).
  * `docs/architecture/release-automation-baseline.md` (§11 R4
    addendum appended) — durable record of D1-D6 with rationale,
    ecosystem precedent, reversibility cost.  Includes the maintainer
    bootstrap procedure for v0.5.91 as standalone steps.

Validation:
  * `cargo check --workspace --all-targets`: pass.
  * `cargo fmt --check --all`: pass.
  * `taplo format --check release-plz.toml`: pass.
  * `actionlint .github/workflows/release-plz.yml`: pass.
  * `typos`, `reuse lint`: pass.
  * `just lint-pre-push`: 20/20 gates green (97s).

Rollback: `git revert` flips the workflow back to R3 shadow mode.
Any tags created by future release-plz active runs stay (idempotent).
The `release-plz.toml` R4 additions are additive — revert independently.

Refs:
  * Phase R4 plan: `docs/architecture/release-automation-plan.md`
  * Settled decisions: `docs/architecture/release-automation-baseline.md` §11
  * release-plz reference workflow:
    https://github.com/release-plz/release-plz/blob/main/.github/workflows/release-plz.yml
@githubrobbi githubrobbi enabled auto-merge (squash) May 8, 2026 01:47
@githubrobbi githubrobbi merged commit 6790a81 into main May 8, 2026
24 checks passed
@githubrobbi githubrobbi deleted the feat/release-auto-r4-active-mode branch May 8, 2026 01:59
githubrobbi added a commit that referenced this pull request May 8, 2026
… tag creation through release-plz-* PRs only (#151)

Why
---

The first R4 active-mode workflow run on PR #149's merge (run 25549828912)
failed the `release-plz-release` job with:

    failed to create ref refs/tags/v0.5.91 with sha 113f188...
    Reference update failed (HTTP 422)

Root cause: release-plz's default `release_always = true` makes the
`release` job attempt a tag-creation on EVERY push to `main`, racing
`auto-tag-release.yml` -> `release.yml` (the existing R3-era path
still active until R5 retires the bespoke flow).  On PR #149's merge:

  1. release-plz fired and tried to recompute the CHANGELOG (the bespoke
     `update_all_versions.rs` rewrites `## [0.5.90]` -> `## [0.5.91]`
     in place rather than producing a cliff-style entry, leaving state
     inconsistent with what release-plz expects).
  2. release-plz committed the recomputation locally (synthetic SHA
     `113f188...`) and tried to tag that SHA.
  3. Meanwhile `release.yml` had already created the `v0.5.91` tag at
     the actual merge commit `5ff321b04`.
  4. Two tag-creators racing for the same ref -> release-plz lost ->
     workflow turned RED.

Fix
---

Set `release_always = false` workspace-level in `release-plz.toml`.

This makes `release-plz release` only fire when the latest commit on
`main` is the merge of a PR whose branch starts with
`pr_branch_prefix` (`release-plz-`).

Coexistence semantics:

  * Bespoke `just ship` cycles use `release/vX.Y.Z` branch names ->
    NOT `release-plz-*` -> `release` job NO-OPS cleanly.
    `auto-tag-release.yml` + `release.yml` remain the sole tag-creator
    during the R4 -> R5 transition window.
  * release-plz-driven cycles use `release-plz-vX.Y.Z` branches ->
    matches the gate -> `release` job fires correctly.

Steady-state (post-R5): bespoke flow deleted -> only the
`release-plz-*` path remains -> `release_always = false` continues
gating correctly without modification.  This is the recommended
setting for any project using PR-gated releases per release-plz docs:
https://release-plz.ieni.dev/docs/config (search for `release_always`).

Documentation
-------------

Updates two rows in `docs/architecture/release-automation-plan.md`
\u00a78.1 deviations log:

  * Corrects the existing "R4 baseline" row's misprediction
    ("silently treats as no-baseline" -> actual: HARD-FAIL with
    `cargo package failed`).  Records the v0.5.91 bootstrap details
    (PR #149 squash-merged to `5ff321b04`; tag created by
    `auto-tag-release.yml` -> `release.yml`, NOT release-plz).
  * Adds a new "R4 release-job race" row capturing the symptom +
    root cause + this fix + the post-R5 forward-compat note.

Verification
------------

* `taplo format --check release-plz.toml` -> ok.
* `cargo fmt --check --all` -> ok.
* `typos release-plz.toml docs/architecture/release-automation-plan.md` -> ok.
* Manual cross-check vs release-plz docs: `release_always` field
  documented in https://release-plz.ieni.dev/docs/config under
  the `[workspace]` reference; `pr_branch_prefix` already set to
  `release-plz-` in our config (line 113).

Related
-------

* PR #148 (R4 active mode landed)
* PR #149 (v0.5.91 bootstrap via bespoke flow)
* PR #145 (R3.5 dep-version + R6 publishability)
* docs/architecture/release-automation-plan.md \u00a78.1 (deviations log)
githubrobbi added a commit that referenced this pull request May 8, 2026
…153)

Phase R5 of `docs/architecture/release-automation-plan.md`.  Deletes the
parallel `auto-tag-release.yml` + `update_all_versions.rs` + `version-bump`
recipes track that has been driving releases since v0.4.x; release-plz
(R4 active since 2026-05-08) is now the sole version-bump + tag creator.
This is the point-of-no-return milestone for the release-automation
initiative; the bespoke flow can no longer be the fallback on the next
push to `main`.

Removed (~1430 LOC):

  - `.github/workflows/auto-tag-release.yml` (168 LOC).
  - `build/update_all_versions.rs` (1073 LOC).
  - `scripts/ci/ci-pipeline.rs` (53 LOC) — Phase 7 deprecation shim;
    `REMOVE-AFTER: v0.5.73` marker satisfied at v0.5.92.
  - `increment_version` + `version_bump` fns in
    `scripts/ci-pipeline/src/version.rs`.
  - `STEP_VERSION_INCREMENT` from `ALL_STEPS` in
    `scripts/ci-pipeline/src/workflow.rs`.
  - Version-bump step from `run_enhanced_phase2` (ship.rs) and
    `phase2_optimized` (phases.rs).
  - `version-bump` recipe in `just/build.just`.
  - Version-bump step from `quick-deploy` recipe in `just/dev.just`.
  - `!build/update_all_versions.rs` carve-out in `.gitignore`.

Added (~140 LOC, mostly comments + workflow YAML):

  - `detect-release-bump` short-circuit job in
    `.github/workflows/release-cache-warm.yml` — diffs
    `[workspace.package].version` between `HEAD` and `HEAD~1` and
    skips the warm matrix when the push is a version bump (saves
    ~165 runner-min/release because `release.yml` rebuilds + caches
    that same dep graph anyway).
  - Bridge step in `release-plz.yml`'s release job —
    `gh workflow run release.yml ...` after release-plz creates the
    workspace tag.  Replaces the GITHUB_TOKEN anti-loop workaround
    that R4 deferred to a future GitHub App / PAT setup; uses
    `workflow_dispatch` (explicitly carved out of the anti-loop
    policy) instead.  Flips `git_release_enable = false` in
    `release-plz.toml` so `release.yml` owns the GitHub Release page
    (avoids the body-overwrite race that softprops/action-gh-release
    would otherwise hit when run against a release-plz-created
    Release with `body_path: release-notes.md`).

Doc updates:

  - `docs/architecture/release-automation-plan.md` — flip R5 row to
    🟢, append four deviation log entries (v0.5.91 immutable-release
    lockout, R5-before-R4-bakein pragmatic acceleration, R5
    cache-warm short-circuit, R5 downstream-trigger bridge resolves
    prior R4 deferred row).
  - `docs/architecture/dev-flow-implementation-plan.md` — tick the
    final Phase 7 bake-in checkbox (deprecation shim retired,
    `REMOVE-AFTER: v0.5.73` satisfied at v0.5.92).
  - `CONTRIBUTING.md` — rewrite the Release row in the four-layer
    quality-gates matrix to describe the post-R5 release-plz flow.
  - `docs/publishing.md` — flip R3.5 / R5 / R6 status rows to landed;
    R4 stays 🟡 (active, bake-in pending first release-plz-driven
    release).
  - `docs/architecture/security/supply-chain-posture.md` — replace
    `auto-tag-release.yml` reference with the post-R5 chain.
  - Trailing comments in `release-plz.toml`, `Cargo.toml`,
    `scripts/ci-pipeline/Cargo.toml`, `.gitignore`, and
    `scripts/ci-pipeline/src/{version,workflow,ship,phases}.rs`
    rewritten to describe the post-R5 steady state and explicitly
    note the R5 retirement of any pre-R5 tooling they referenced.

Validation:

  - `cargo check --workspace --locked --all-targets` green.
  - `cargo clippy -p uffs-ci-pipeline --locked --all-targets
    -- -D warnings` green.
  - `cargo fmt --all` green.
  - `actionlint` on the two modified workflow YAML files green.
  - `just gates-drift` — manifest + consumers agree (23 gates).

R4 bake-in completes naturally on the next `feat:` / `fix:` / `perf:` /
`security:` commit to `main`, which release-plz will turn into the
first end-to-end release-plz-driven release (v0.5.93).  At that point
the R4 row in the dashboard flips to 🟢 in a follow-up commit.

Refs: #148 (R4 active-mode flip), #145 (R3.5 internal-dep version
requirements + R6 metadata), #151 (`release_always = false` gate),
#152 (v0.5.92 manual bootstrap after v0.5.91 immutable-release
lockout).
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