Skip to content

build(release-automation): R6 publishability + R3.5 dep version requirements#145

Merged
githubrobbi merged 1 commit intomainfrom
feat/release-auto-r6-publishability
May 7, 2026
Merged

build(release-automation): R6 publishability + R3.5 dep version requirements#145
githubrobbi merged 1 commit intomainfrom
feat/release-auto-r6-publishability

Conversation

@githubrobbi
Copy link
Copy Markdown
Collaborator

Summary

Bundles two phases of docs/architecture/release-automation-plan.md into one PR per the routing decision recorded in §8.1 deviations log:

  • R3.5 — internal-dep version = requirements (silent shadow-mode failure unblocker)
  • R6 — crates.io metadata audit + per-crate [package.metadata.docs.rs] + dry-run CI workflow + DORMANT publishing runbook

R3.5 — silent shadow-mode unblocker

R3 landed on 2026-04-25 (#67) and was expected to start producing per-merge release-plz update summaries. Twelve days passed with empty summaries on every push.

Root cause: release-plz update invokes cargo package per crate, and cargo refuses to package any crate whose [dependencies] lack a version = requirement, even with path = present. UFFS had:

  • 8 internal-dep aliases in [workspace.dependencies] (path-only)
  • 2 direct path-deps in crates/uffs-cli/Cargo.toml
  • 1 polars git pin in crates/uffs-polars/Cargo.toml

…all unversioned. The failure was swallowed by the workflow's tee-to-log-file step + the empty-diff branch of the summary template, masking it from any reviewer who didn't expand the inner step log.

Fix

 [workspace.dependencies]
-uffs-polars = { path = "crates/uffs-polars" }
+uffs-polars = { path = "crates/uffs-polars", version = "0.5.90" }
 # ...8 internal aliases total

 # crates/uffs-polars/Cargo.toml
-polars = { git = "...", rev = "1e9a63b9...", features = [...] }
+polars = { git = "...", rev = "1e9a63b9...", version = "0.53.0", features = [...] }

 # crates/uffs-cli/Cargo.toml
-uffs-client = { path = "../uffs-client", default-features = false }
-uffs-format = { path = "../uffs-format" }
+uffs-client = { path = "../uffs-client", version = "0.5.90", default-features = false }
+uffs-format = { path = "../uffs-format", version = "0.5.90" }

just polars now updates the polars version = field in lockstep with the resolved git rev, so future polars major-version bumps don't drift the version requirement.

R3.5 verification

$ release-plz update --config release-plz.toml
* `uffs-polars`: 0.5.90
* `uffs-security`: 0.5.90
* ... (12 publishable crates total) ...

12 crates enumerated cleanly. uffs-diag excluded (per-crate publish = false); uffs-ci-pipeline / uffs-gen-hooks / uffs-gen-workflow excluded (per-package release = false blocks added in this PR).

R6 — crates.io metadata audit

Per-crate [package.metadata.docs.rs]

Added to all 12 publishable crates with targets / default-target calibrated per-crate:

Tier Crates targets
Single-target Linux uffs-time, uffs-text, uffs-format, uffs-polars (unset — defaults to Linux)
Linux + Windows uffs-mft, uffs-core, uffs-daemon, uffs-client, uffs-cli, uffs-mcp ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
Linux + Windows + macOS uffs-security three targets (Keychain on macOS, DACL on Windows, flock on Unix)
Windows-only uffs-broker default-target = "x86_64-pc-windows-msvc" (whole crate is Windows-specific)

Per-package publish / release overrides

  • crates/uffs-diag/Cargo.toml — explicit publish = false (workspace-only diagnostic crate per plan §R6 step 2)
  • release-plz.toml[[package]] release = false for uffs-ci-pipeline, uffs-gen-hooks, uffs-gen-workflow (already publish = false at the crate level; this is the surgical fix to keep them out of release-plz's per-package iteration)

.github/workflows/crates-io-dry-run.yml

Weekly schedule (Mondays 06:00 UTC) + workflow_dispatch. Loops every publishable crate, runs cargo publish --dry-run -p <crate>, posts a per-crate status table to the workflow summary.

Currently runs in ADVISORY mode (FAIL_ON_DRY_RUN_ERROR=false) because two known-expected failure classes exist:

  1. Crate-name reservations not yet pushed (R6 step 6, deferred): every dry-run fails with no matching package named uffs-X found until 0.0.0 stub versions are reserved on crates.io from a throwaway external workspace.
  2. polars / chrono publishability gap: uffs-polars and any crate transitively depending on it fails with failed to select a version for chrono because the published-form polars = "0.53.0" resolution conflicts with the workspace chrono pin. Resolution is R8 dress rehearsal scope.

Flip FAIL_ON_DRY_RUN_ERROR=true once both blockers are resolved.

docs/publishing.md — DORMANT runbook

Covers:

  • Pre-publish checklist (per go-live decision)
  • Per-release checklist (every release post-R9)
  • Yank decisions log
  • Post-publish smoke checks
  • Manual fallback ordering (when release-plz is unavailable)
  • OIDC trusted-publisher section placeholder for R7
  • Four-layer dormancy stack (publish = false workspace + per-package + if: false job + missing token)

Plan dashboard updates

docs/architecture/release-automation-plan.md §8 dashboard:

§8.1 deviations log: 3 new entries

  • R3 → R4 readiness (silent failure root cause + R3.5 fix)
  • R6 step 6 (crate-name reservations deferral)
  • R6 → R8 publishability (polars/chrono gap)

docs/architecture/release-automation-baseline.md §10 R3.5 + R6 addendum (113 lines): silent-failure root cause, fix diff, validation evidence, R6 deliverables breakdown, deliberately-NOT-doing list, forward-compat assertion.

Deliberately deferred (not in this PR)

  • Crate-name reservations on crates.io — plan §R6 step 6 mandates these happen from a throwaway external workspace, not the UFFS repo, so the UFFS repo never carries a publish = true state.
  • cargo-semver-checks integration — needs a published baseline; deferred until post-R8.
  • OIDC trusted-publisher scaffolding — R7 scope.
  • Workspace-level publish = false flip — R8 scope.
  • FAIL_ON_DRY_RUN_ERROR=true — flipped once both blockers above are resolved.

Validation

  • cargo check --workspace --all-targets passes
  • cargo fmt --check --all passes
  • taplo format clean (formatting normalized)
  • actionlint .github/workflows/crates-io-dry-run.yml passes
  • typos clean
  • release-plz update --config release-plz.toml lists 12 crates without error
  • ✅ Local pre-commit hooks: file-size, typos, reuse, taplo all pass
  • ✅ Local pre-push hooks: cargo-check, lint-ci, lint-prod, lint-tests, rustdoc, doc-tests, tests, smoke, deny, lint-ci-windows all pass

Files

  • 18 modified (Cargo.toml × 14, release-plz.toml, just/test.just, plan.md, baseline.md)
  • 2 added (.github/workflows/crates-io-dry-run.yml, docs/publishing.md)
  • Net: +882 / −42 LOC

Plan refs

  • docs/architecture/release-automation-plan.md §R6 (steps 1–5; step 6 explicitly deferred)
  • docs/architecture/release-automation-plan.md §8 dashboard
  • docs/architecture/release-automation-plan.md §8.1 deviations log (three new entries)
  • docs/architecture/release-automation-baseline.md §10 (new addendum)

…rements

Bundles two phases of the release-automation plan into one PR per the routing decision recorded in release-automation-plan.md §8.1 deviations log.

Phase R3.5 — internal dependency version requirements:

After R3 landed, the shadow-mode workflow ran 12+ days with empty output. Root cause: 'release-plz update' invokes 'cargo package' per crate, and cargo refuses to package any crate whose [dependencies] entries lack a 'version =' requirement, even with 'path =' present. UFFS had 8 internal-dep aliases in [workspace.dependencies], 2 direct path-deps in uffs-cli/Cargo.toml, and the polars git pin all unversioned.

Fix:

- Add 'version = "0.5.90"' to every internal-dep alias in root Cargo.toml

- Add 'version = "0.5.90"' to uffs-cli's direct uffs-client + uffs-format path deps

- Add 'version = "0.53.0"' to the polars git dep in uffs-polars/Cargo.toml

- Update 'just polars' to keep the polars version pin in lockstep with the rev across major-version bumps

Verified locally: 'release-plz update --config release-plz.toml' now lists all 12 publishable crates without error.

Phase R6 — crates.io metadata audit + dry-run CI:

- Add [package.metadata.docs.rs] blocks to all 12 publishable crates with appropriate 'targets' / 'default-target' per platform surface

- Add explicit 'publish = false' to crates/uffs-diag/Cargo.toml (workspace-only diagnostic crate per plan §R6 step 2)

- Add per-package 'release = false' blocks for uffs-ci-pipeline, uffs-gen-hooks, uffs-gen-workflow in release-plz.toml

- Add .github/workflows/crates-io-dry-run.yml — weekly + workflow_dispatch scheduled per-crate 'cargo publish --dry-run' with summary table; advisory mode (FAIL_ON_DRY_RUN_ERROR=false) until R6 step 6 (crate name reservations) and R8 (polars/chrono resolution) land

- Add docs/publishing.md DORMANT runbook covering pre-publish checklist, per-release checklist, yank decisions log, manual fallback ordering, OIDC section placeholder for R7

Update release-automation-plan.md §8 dashboard: R3 → 🟢 (PR #67), new R3.5 row 🟡, R6 → 🟡. Add three §8.1 deviations log entries documenting the version-requirements fix, the crate-name-reservation deferral, and the known-expected polars/chrono publishability gap.

Append §10 R3.5+R6 addendum to release-automation-baseline.md with the silent-failure root cause, the fix diff, validation evidence, and forward-compat assertion (release-plz handles workspace version sync natively post-R5).

Deliberately deferred:

- crate-name reservations on crates.io (plan §R6 step 6 — happens from a throwaway workspace, not this repo)

- cargo-semver-checks integration (no published baseline yet)

- OIDC trusted-publisher scaffolding (R7 scope)

- workspace-level publish=false flip (R8 scope)
@githubrobbi githubrobbi force-pushed the feat/release-auto-r6-publishability branch from 07b6bf5 to 9f23a5e Compare May 7, 2026 23:35
@githubrobbi githubrobbi enabled auto-merge (squash) May 7, 2026 23:38
@githubrobbi githubrobbi merged commit cccf4f1 into main May 7, 2026
27 checks passed
@githubrobbi githubrobbi deleted the feat/release-auto-r6-publishability branch May 7, 2026 23:49
githubrobbi added a commit that referenced this pull request May 8, 2026
…e tags + CHANGELOG) (#148)

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 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