diff --git a/.github/workflows/crates-io-dry-run.yml b/.github/workflows/crates-io-dry-run.yml new file mode 100644 index 000000000..825765506 --- /dev/null +++ b/.github/workflows/crates-io-dry-run.yml @@ -0,0 +1,272 @@ +# SPDX-FileCopyright (c) 2025-2026 SKY, LLC. +# SPDX-License-Identifier: MPL-2.0 + +# ───────────────────────────────────────────────────────────────────────────── +# crates.io dry-run — Phase R6 of `release-automation-plan.md` step 4 +# +# What this does: +# +# Runs `cargo publish --dry-run -p ` for every workspace member +# that is publishable (i.e. doesn't have `publish = false` set in its +# Cargo.toml). A successful dry-run for a crate proves that its +# metadata (`description`, `keywords`, `categories`, `readme`, `license`, +# docs.rs config, dependency version requirements) is acceptable to +# crates.io's manifest validator AND that all of its registry-form +# dependencies resolve. +# +# Why advisory (workflow always exits 0): +# +# In the R6→R7 timeframe, several KNOWN-EXPECTED failures exist: +# +# 1. Internal crate name reservations not yet pushed. Until +# `uffs-polars`, `uffs-security`, `uffs-text`, etc. are reserved on +# crates.io with stub 0.0.0 versions (R6 step 6, deliberately +# deferred to keep this PR scope-bounded), every published-form +# dependency on a workspace member fails with "no matching package +# named `uffs-X` found". E.g. `cargo publish --dry-run -p +# uffs-broker` fails because `uffs-security = "0.5.90"` can't +# resolve. +# +# 2. polars git-pin vs crates.io version-feature mismatch. The +# `uffs-polars` facade pins polars to a specific git rev for +# feature-set determinism (see `crates/uffs-polars/Cargo.toml` +# comment block). When `cargo publish` strips the git source and +# emits `polars = "0.53.0"` for crates.io resolution, the registry +# polars 0.53.0's transitive `chrono-tz` constraints conflict with +# our workspace-pinned `chrono`. This is a real publishability +# blocker that R8 (dress rehearsal) will resolve — likely by +# either flipping uffs-polars to `publish = false` or aligning +# the chrono pin with crates.io polars expectations. +# +# For these reasons the workflow currently runs in OBSERVE mode: it +# still loops every publishable crate, captures per-crate success or +# failure, and posts a full per-crate status table to the workflow +# summary, but it does NOT fail the job. When crate-name reservations +# are pushed and the polars/chrono issue is resolved, flip +# `FAIL_ON_DRY_RUN_ERROR=true` in the env block below to convert +# advisory mode into a hard gate. +# +# Why scheduled + workflow_dispatch: +# +# Weekly schedule catches metadata drift introduced by upstream +# ecosystem changes (a new version of `chrono` lands that's +# incompatible with our pin, an upstream crate name lookup gets +# blocked by an account suspension, etc.). workflow_dispatch lets +# maintainers trigger ad-hoc validation while iterating on Cargo.toml +# edits during R6/R7/R8 hardening. No `push:` trigger because the +# dry-run takes ~5-15 minutes and the metadata-relevant changes are +# rare; weekly cadence is sufficient signal. +# +# References: +# +# • plan §R6 step 4 — workflow specification +# • plan §R6 step 6 — crate name reservation prerequisite +# • plan §R8 — dress rehearsal that will resolve known failures +# ───────────────────────────────────────────────────────────────────────────── + +name: "📦 crates.io dry-run" + +on: + schedule: + # Mondays 06:00 UTC — chosen to land before maintainers' typical + # work-week start so any drift surfaced over the weekend is visible + # in the workflow summary by the time someone looks. + - cron: '0 6 * * 1' + workflow_dispatch: + +# Read-only by design. The dry-run never writes; the workflow's +# inability to write to the registry IS the safety guarantee. +permissions: + contents: read + +# Serialise concurrent runs on the same ref so two close-together +# triggers don't fight over the runner cache. cancel-in-progress +# is false because each run captures observation-valuable state. +concurrency: + group: crates-io-dry-run-${{ github.ref }} + cancel-in-progress: false + +jobs: + dry-run: + name: cargo publish --dry-run (workspace) + runs-on: ubuntu-latest + timeout-minutes: 30 + + # Skip on forks (fork PRs can't write to the workflow summary in + # the source repo and would produce noisy permission-denied runs). + if: github.repository_owner == 'skyllc-ai' + + env: + # Flip to `true` once R6 step 6 (crate name reservations) and + # R8 (polars/chrono resolution) are both complete. In that + # state the dry-run becomes a hard gate that catches metadata + # regressions immediately rather than leaving them in a + # silent-summary state. + FAIL_ON_DRY_RUN_ERROR: 'false' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Default GITHUB_TOKEN is fine for read-only checkout. The + # workflow's `permissions: contents: read` already restricts + # write capability; explicit `persist-credentials: false` + # makes the no-write contract auditable from the workflow. + persist-credentials: false + + - name: Install pinned nightly (from rust-toolchain.toml) + # `rustup show` reads `rust-toolchain.toml` and installs the + # exact channel + components pinned there. Matches the + # convention used by the rest of UFFS's CI workflows + # (release-plz.yml, cargo-vet-refresh.yml, pr-fast.yml). + run: rustup show + + - name: Cache cargo dependencies + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + # Dedicated cache key — dry-run's compile output is small + # (per-crate package verifications) and disjoint from the + # main CI compile, so a separate key prevents the two from + # evicting each other. + shared-key: crates-io-dry-run + + - name: Enumerate publishable crates + id: enumerate + run: | + set -euo pipefail + # `cargo metadata` produces full workspace metadata including + # each package's `publish` field. Per cargo docs: + # - `publish = false` serialises as `"publish": []` + # - `publish = true` (or unset) serialises as `"publish": null` + # - `publish = ["registry"]` serialises as a non-empty array + # The jq filter `.publish != []` keeps unset (null) AND + # explicit registry lists, dropping only `publish = false`. + mapfile -t CRATES < <( + cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[] + | select(.publish != []) + | .name' + ) + # Multiline outputs require a heredoc-style delimiter in + # GitHub Actions to preserve newlines through $GITHUB_OUTPUT. + { + echo "crates<> "$GITHUB_OUTPUT" + echo "Enumerated ${#CRATES[@]} publishable crate(s):" + printf ' - %s\n' "${CRATES[@]}" + + - name: Dry-run publish each crate + id: dry_run + env: + CRATES: ${{ steps.enumerate.outputs.crates }} + run: | + set -uo pipefail + # Track results in a TSV temp file so the summary step can + # render them as a table without re-running cargo. + RESULTS_TSV="${RUNNER_TEMP:-/tmp}/dry-run-results.tsv" + : > "$RESULTS_TSV" + + ANY_FAILED=0 + while IFS= read -r crate; do + [ -z "$crate" ] && continue + echo "::group::cargo publish --dry-run -p $crate" + # `--allow-dirty` is required because release-plz never + # reaches this workflow (it's a separate scheduled run); + # the working tree may have unrelated edits in CI checkouts + # if/when other workflows queue up. Empirically the dirty + # state in CI is always the checked-out commit's tree, so + # `--allow-dirty` is safe. + # + # `--no-verify` is intentionally OMITTED. We want the + # full per-crate compile to surface metadata-vs-source + # mismatches (e.g. a `[lib]` rename that breaks doc-test + # discovery on docs.rs). Yes this is slow; the weekly + # cadence absorbs the cost. + if cargo publish --dry-run -p "$crate" --allow-dirty 2>&1; then + echo "$crate OK" >> "$RESULTS_TSV" + echo "::endgroup::" + else + # Capture exit code 0|1; non-zero means dry-run failed + # (metadata invalid, dep can't resolve, build failed). + echo "$crate FAIL" >> "$RESULTS_TSV" + echo "::warning title=dry-run failed::$crate failed cargo publish --dry-run" + echo "::endgroup::" + ANY_FAILED=1 + fi + done <<< "$CRATES" + + { + echo "results_tsv<> "$GITHUB_OUTPUT" + + - name: Post summary + if: always() + env: + MARKER: "" + ANY_FAILED: ${{ steps.dry_run.outputs.any_failed }} + RESULTS: ${{ steps.dry_run.outputs.results_tsv }} + run: | + set -euo pipefail + { + echo "${MARKER}" + echo "## crates.io dry-run results" + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Commit | \`${GITHUB_SHA}\` |" + echo "| Triggered by | \`${GITHUB_EVENT_NAME}\` |" + echo "| Any failures | \`${ANY_FAILED}\` |" + echo "| Mode | \`$(if [ "$FAIL_ON_DRY_RUN_ERROR" = "true" ]; then echo "GATE"; else echo "ADVISORY"; fi)\` |" + echo "" + echo "### Per-crate dry-run status" + echo "" + echo "| Crate | Status |" + echo "|---|---|" + # Sort so the table is stable across runs even if cargo + # metadata changes its iteration order. + printf '%s\n' "${RESULTS}" | sort | while IFS=$'\t' read -r crate status; do + [ -z "$crate" ] && continue + icon="✅" + [ "$status" = "FAIL" ] && icon="❌" + echo "| \`${crate}\` | ${icon} ${status} |" + done + echo "" + if [ "$ANY_FAILED" = "1" ]; then + echo "### Known-expected failures (R6/R7 timeframe)" + echo "" + echo "Until **plan §R6 step 6** (crate name reservations on" + echo "crates.io) and **§R8** (polars/chrono resolution) land," + echo "the following classes of failure are expected and do" + echo "NOT indicate a regression:" + echo "" + echo "- \`no matching package named \\\`uffs-X\\\` found\` —" + echo " fixed by R6 step 6 (publish stub 0.0.0 versions)." + echo "- \`failed to select a version for \\\`chrono\\\`\` on" + echo " \`uffs-polars\` and downstream — fixed by R8." + echo "" + echo "Failures of unrelated metadata (missing description," + echo "invalid category slug, readme path resolution, etc.)" + echo "ARE actionable and should be fixed in the next PR." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail job if dry-run errors and FAIL_ON_DRY_RUN_ERROR=true + if: always() + env: + ANY_FAILED: ${{ steps.dry_run.outputs.any_failed }} + run: | + set -euo pipefail + if [ "$FAIL_ON_DRY_RUN_ERROR" = "true" ] && [ "$ANY_FAILED" = "1" ]; then + echo "::error::At least one crate failed cargo publish --dry-run; FAIL_ON_DRY_RUN_ERROR is set, failing job." + exit 1 + fi + if [ "$ANY_FAILED" = "1" ]; then + echo "::notice::At least one crate failed cargo publish --dry-run; running in ADVISORY mode (FAIL_ON_DRY_RUN_ERROR=false), so exiting 0. See workflow summary for the per-crate table and the known-expected-failures explainer." + else + echo "::notice::All publishable crates passed cargo publish --dry-run." + fi diff --git a/Cargo.toml b/Cargo.toml index 9f2f8815c..e79b0110e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,14 +66,44 @@ categories = ["filesystem", "command-line-utilities"] # ───────────────────────────────────────────────────────────────────────────── [workspace.dependencies] # ───── Internal Crates ───── -uffs-polars = { path = "crates/uffs-polars" } -uffs-security = { path = "crates/uffs-security" } -uffs-text = { path = "crates/uffs-text" } -uffs-time = { path = "crates/uffs-time" } -uffs-mft = { path = "crates/uffs-mft", features = ["zstd"] } -uffs-format = { path = "crates/uffs-format" } -uffs-core = { path = "crates/uffs-core" } -uffs-client = { path = "crates/uffs-client" } +# Internal crates carry an explicit `version =` requirement (in addition +# to `path =`) for two reasons: +# +# 1. `cargo package` (invoked by release-plz `update` and `publish`) +# refuses to package any crate whose `[dependencies]` entries lack +# a version requirement, even if the entry resolves through a +# `path =`. Without `version =` here, every consumer-side +# `.workspace = true` inherits an unversioned dep, and +# `cargo package` fails with `dependency "" does not specify +# a version`. +# +# 2. When publishing to crates.io (R8+), cargo strips the `path =` +# and uses the registry resolution. The published crate carries +# the `version =` requirement only — consumers who install us +# from crates.io get the matching registry version of each +# internal dep. Within the workspace, cargo continues to use the +# `path =` and verifies the path-resolved crate's actual version +# satisfies the requirement. Both modes work simultaneously. +# +# Maintenance: `build/update_all_versions.rs` already handles the +# `version = "x.y.z"` substitution pattern in this file shape (see +# Pattern 5 in that script), so a `just ship` bump keeps all 8 entries +# in sync with `[workspace.package].version` automatically. In R5 +# release-plz takes over this responsibility natively. +# +# Discovered: 2026-05-07 during R3 → R4 readiness assessment — the +# R3 shadow workflow (`release-plz.yml`) silently produced empty +# proposed-plan output for 12 days because `release-plz update` +# failed at `cargo package` with this very error. See +# `release-automation-baseline.md` §10 for the diagnostic trail. +uffs-polars = { path = "crates/uffs-polars", version = "0.5.90" } +uffs-security = { path = "crates/uffs-security", version = "0.5.90" } +uffs-text = { path = "crates/uffs-text", version = "0.5.90" } +uffs-time = { path = "crates/uffs-time", version = "0.5.90" } +uffs-mft = { path = "crates/uffs-mft", version = "0.5.90", features = ["zstd"] } +uffs-format = { path = "crates/uffs-format", version = "0.5.90" } +uffs-core = { path = "crates/uffs-core", version = "0.5.90" } +uffs-client = { path = "crates/uffs-client", version = "0.5.90" } # NOTE: no `uffs-diag` workspace dependency alias on purpose. # It is a top-level diagnostic/tool crate, not a shared runtime dependency. diff --git a/crates/uffs-broker/Cargo.toml b/crates/uffs-broker/Cargo.toml index 1d3fa6807..adf85ee11 100644 --- a/crates/uffs-broker/Cargo.toml +++ b/crates/uffs-broker/Cargo.toml @@ -19,6 +19,18 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# `uffs-broker` is a Windows-elevated handle broker; on non-Windows the +# binary's `main()` short-circuits. Override `default-target` to the MSVC +# triple so docs.rs renders the Windows API integration that's the entire +# point of this crate. See `uffs-time/Cargo.toml` for the rationale on +# `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-pc-windows-msvc"] +default-target = "x86_64-pc-windows-msvc" + [[bin]] name = "uffs-broker" path = "src/main.rs" diff --git a/crates/uffs-cli/Cargo.toml b/crates/uffs-cli/Cargo.toml index 5943301ea..b1cde5032 100644 --- a/crates/uffs-cli/Cargo.toml +++ b/crates/uffs-cli/Cargo.toml @@ -24,6 +24,14 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target build so platform-gated CLI subcommands render in docs. +# See `uffs-time/Cargo.toml` for the rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] + [[bin]] name = "uffs" path = "src/main.rs" @@ -37,12 +45,18 @@ path = "src/main.rs" # Not using `.workspace = true` here: workspace inheritance cannot # override `default-features`, so we point at the path directly. The # version is pinned by the workspace via path dependency resolution. -uffs-client = { path = "../uffs-client", default-features = false } +# `version = "0.5.90"` is required for `cargo package` validation — +# see root `Cargo.toml`'s [workspace.dependencies] note for the full +# rationale (R6 of `release-automation-plan.md`). +uffs-client = { path = "../uffs-client", version = "0.5.90", default-features = false } # Canonical CSV / parity / legacy-footer writer. Direct dep (not a # re-export chain through `uffs-client`) so the CLI and the daemon # hit the same `uffs_format::*` symbols without an indirection layer. -uffs-format = { path = "../uffs-format" } +# `version = "0.5.90"` is required for `cargo package` validation — +# see root `Cargo.toml`'s [workspace.dependencies] note for the full +# rationale (R6 of `release-automation-plan.md`). +uffs-format = { path = "../uffs-format", version = "0.5.90" } # Error handling anyhow.workspace = true @@ -91,4 +105,3 @@ winresource.workspace = true [dev-dependencies] assert_cmd.workspace = true - diff --git a/crates/uffs-client/Cargo.toml b/crates/uffs-client/Cargo.toml index fd6d65dbd..febf7112a 100644 --- a/crates/uffs-client/Cargo.toml +++ b/crates/uffs-client/Cargo.toml @@ -18,6 +18,15 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target build so the Unix-domain-socket and Windows-named-pipe IPC +# transports BOTH render in the docs. See `uffs-time/Cargo.toml` for the +# rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] + [features] # Default enables the async `UffsClient` (tokio-based) so existing # consumers (`uffs-daemon`, `uffs-mcp`) keep working with no change. diff --git a/crates/uffs-core/Cargo.toml b/crates/uffs-core/Cargo.toml index 059e7b90a..95d9648d8 100644 --- a/crates/uffs-core/Cargo.toml +++ b/crates/uffs-core/Cargo.toml @@ -24,6 +24,15 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target build so platform-gated query helpers (Windows-only direct-MFT +# code paths) render alongside the cross-platform query API. See +# `uffs-time/Cargo.toml` for the rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] + [dependencies] # Internal crates uffs-polars.workspace = true diff --git a/crates/uffs-daemon/Cargo.toml b/crates/uffs-daemon/Cargo.toml index 63b7a151e..384c006ec 100644 --- a/crates/uffs-daemon/Cargo.toml +++ b/crates/uffs-daemon/Cargo.toml @@ -19,6 +19,15 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target build so platform-gated daemon transports (UDS / named pipes) +# render in docs. See `uffs-time/Cargo.toml` for the rationale on +# `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] + [[bin]] name = "uffsd" path = "src/main.rs" diff --git a/crates/uffs-diag/Cargo.toml b/crates/uffs-diag/Cargo.toml index a2e312080..0bd145185 100644 --- a/crates/uffs-diag/Cargo.toml +++ b/crates/uffs-diag/Cargo.toml @@ -30,6 +30,28 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 2: explicit `publish = false` +# at the crate level (NOT just at `release-plz.toml`). Belt-and-suspenders: +# - `release-plz.toml::publish = false` blocks the release-plz workflow from +# ever calling `cargo publish` for this crate. +# - This `publish = false` blocks LOCAL `cargo publish -p uffs-diag` from a +# developer's machine even if they bypass release-plz. +# +# Rationale: `uffs-diag` is a workspace-only diagnostic toolchain. Its +# binaries (`analyze_mft_parents`, `dump_mft_records`, `compare_raw_mft`, +# `compare_scan_parity`, `inspect_mft_record_flow`, …) operate on raw MFT +# snapshots and Parquet exports — they're internal correctness-validation +# instruments, not user-facing tools. Shipping them to crates.io would +# (a) clutter the crates.io namespace, (b) bind us to API stability we +# don't owe consumers, and (c) leak diagnostic dependencies (`uffs-polars` +# git pin) into a published crate where they'd be a compatibility footgun. +# +# Per workspace-layout in root `Cargo.toml::[workspace.members]`, this is +# explicitly tagged "Retained workspace-only diagnostic tools (not shipped +# in dist/)". The `publish = false` here makes that retention contract +# enforceable at `cargo publish` time, not just by convention. +publish = false + # ───────────────────────────────────────────────────────────────────────────── # Binaries # ───────────────────────────────────────────────────────────────────────────── @@ -99,4 +121,3 @@ uffs-polars.workspace = true # ───────────────────────────────────────────────────────────────────────────── [lints] workspace = true - diff --git a/crates/uffs-format/Cargo.toml b/crates/uffs-format/Cargo.toml index 7438c6992..5507f8123 100644 --- a/crates/uffs-format/Cargo.toml +++ b/crates/uffs-format/Cargo.toml @@ -31,6 +31,12 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# See `uffs-time/Cargo.toml` for the full rationale. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [dependencies] # Foundation crates — FILETIME arithmetic and len-to-u32 helpers. uffs-time.workspace = true diff --git a/crates/uffs-mcp/Cargo.toml b/crates/uffs-mcp/Cargo.toml index e58c4cf22..986c94df2 100644 --- a/crates/uffs-mcp/Cargo.toml +++ b/crates/uffs-mcp/Cargo.toml @@ -19,6 +19,15 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target so the platform-gated transport stack (HTTP gateway is +# cross-platform; MCP stdio plumbing has Windows-vs-Unix branches) renders +# in docs. See `uffs-time/Cargo.toml` for the rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] + [lib] name = "uffs_mcp" path = "src/lib.rs" @@ -35,9 +44,9 @@ required-features = ["streamable-http"] [features] default = ["streamable-http"] streamable-http = [ - "rmcp/transport-streamable-http-server", - "dep:axum", - "dep:tower-service", + "rmcp/transport-streamable-http-server", + "dep:axum", + "dep:tower-service", ] [dependencies] diff --git a/crates/uffs-mft/Cargo.toml b/crates/uffs-mft/Cargo.toml index 027635e4b..d2fb6526e 100644 --- a/crates/uffs-mft/Cargo.toml +++ b/crates/uffs-mft/Cargo.toml @@ -24,6 +24,15 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target build so `#[cfg(windows)]` MFT readers and `#[cfg(unix)]` +# offline Parquet helpers BOTH render in docs. See `uffs-time/Cargo.toml` +# for the rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] + [[bin]] name = "uffs_mft" path = "src/main.rs" @@ -75,8 +84,8 @@ windows.workspace = true criterion.workspace = true proptest.workspace = true tokio = { workspace = true, features = ["test-util", "macros"] } -sha2.workspace = true # For SHA256 validation in chaos tests -hex.workspace = true # For hex-encoding SHA256 digests (sha2 0.11 removed LowerHex impl) +sha2.workspace = true # For SHA256 validation in chaos tests +hex.workspace = true # For hex-encoding SHA256 digests (sha2 0.11 removed LowerHex impl) tracing-subscriber = { workspace = true } tempfile.workspace = true @@ -90,4 +99,3 @@ workspace = true [features] default = ["zstd"] zstd = ["dep:zstd"] - diff --git a/crates/uffs-polars/Cargo.toml b/crates/uffs-polars/Cargo.toml index a916aed57..ce1447345 100644 --- a/crates/uffs-polars/Cargo.toml +++ b/crates/uffs-polars/Cargo.toml @@ -29,6 +29,14 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# `all-features = true` is acceptable here because all `[features]` below +# are additive and pull non-conflicting polars feature flags. See +# `uffs-time/Cargo.toml` for the rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [lib] name = "uffs_polars" path = "src/lib.rs" @@ -44,7 +52,27 @@ test = false [dependencies] # ONLY Polars — all other dependencies provided by consuming crates. # Pinned to a specific commit on main. Bump via: just polars -polars = { git = "https://github.com/pola-rs/polars", rev = "1e9a63b95923291cd8849d70d96b8cec4e96da6a", default-features = false, features = [ +# +# `version = "0.53.0"` is REQUIRED in addition to `git/rev` so that +# `cargo package` (invoked by release-plz `update`) can validate this +# manifest. Without `version =`, cargo refuses to package any crate +# that has unversioned dependencies (path, git, or otherwise). +# +# Local-build semantics: cargo uses the git/rev source. The `version` +# field is a verification hint — cargo confirms the resolved git source +# claims a Cargo.toml version that satisfies the requirement (`= 0.53.0` +# matches the polars Cargo.toml at the pinned rev). +# +# Publish semantics (R8+ when `uffs-polars` ships to crates.io): cargo +# strips the `git/rev` and emits only `polars = "0.53.0"`. Consumers +# installing `uffs-polars` from crates.io get crates.io polars 0.53.0, +# NOT our pinned rev. This is a known divergence — the rev pin exists +# for deterministic in-workspace builds with the specific feature set +# below; published consumers fall back to whatever 0.53.0 features +# crates.io polars exposes. R8 dress rehearsal will surface any +# real-world incompatibility (most likely we'd flip to a published +# polars version then or mark `uffs-polars` `publish = false`). +polars = { git = "https://github.com/pola-rs/polars", rev = "1e9a63b95923291cd8849d70d96b8cec4e96da6a", version = "0.53.0", default-features = false, features = [ # ===== Core ===== "lazy", # Lazy evaluation (query optimization) "new_streaming", # Streaming engine for large queries diff --git a/crates/uffs-security/Cargo.toml b/crates/uffs-security/Cargo.toml index f87be4b6b..7f1320401 100644 --- a/crates/uffs-security/Cargo.toml +++ b/crates/uffs-security/Cargo.toml @@ -20,6 +20,20 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# Multi-target build so the platform-specific items (`#[cfg(target_os = "macos")]` +# Keychain helpers, `#[cfg(windows)]` DACL helpers, `#[cfg(unix)]` flock +# helpers) all show up in the rendered docs. See `uffs-time/Cargo.toml` +# for the rationale on `--cfg docsrs`. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", +] + [dependencies] # Logging tracing.workspace = true diff --git a/crates/uffs-text/Cargo.toml b/crates/uffs-text/Cargo.toml index 347dab034..2a8e701fe 100644 --- a/crates/uffs-text/Cargo.toml +++ b/crates/uffs-text/Cargo.toml @@ -20,9 +20,16 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3 — docs.rs build config. +# See `uffs-time/Cargo.toml` for the full rationale comment; intentionally +# kept short here since the pattern is uniform across all 12 publishable +# crates. `all-features = true` + `--cfg docsrs` is the canonical setup. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [dependencies] bytemuck.workspace = true [lints] workspace = true - diff --git a/crates/uffs-time/Cargo.toml b/crates/uffs-time/Cargo.toml index c94cd1bfc..efb92eb99 100644 --- a/crates/uffs-time/Cargo.toml +++ b/crates/uffs-time/Cargo.toml @@ -23,6 +23,16 @@ readme.workspace = true keywords.workspace = true categories.workspace = true +# Phase R6 of `release-automation-plan.md` step 3: docs.rs build configuration. +# `all-features = true` ensures docs.rs renders the full public API surface. +# `rustdoc-args = ["--cfg", "docsrs"]` enables the `docsrs` cfg so platform- +# or feature-gated items can be doc-ifyed via `#[cfg_attr(docsrs, doc(cfg(...)))]` +# without affecting local `cargo doc`. Inert until the crate is actually +# published — docs.rs reads it at build time. See https://docs.rs/about/builds. +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [dependencies] [lints] diff --git a/docs/architecture/release-automation-baseline.md b/docs/architecture/release-automation-baseline.md index b1c1b7488..8adeda626 100644 --- a/docs/architecture/release-automation-baseline.md +++ b/docs/architecture/release-automation-baseline.md @@ -288,3 +288,116 @@ Plus a belt-and-suspenders verification step at the end of the workflow that ass - **Does NOT add `[[package]]` per-crate config**. Workspace defaults only. R6 adds per-crate `publish = true` overrides for the 13 publishable members. - **Does NOT install `cargo-semver-checks`**. Disabled via `semver_check = false`. R6 enables it as part of the metadata audit. - **Does NOT add a schedule trigger**. Push-to-main + workflow_dispatch is sufficient — scheduled re-runs of the same history would produce no new observations. + +## 10. R3.5 + R6 addendum — `version = ` requirements + crates.io metadata audit (2026-05-07) + +Captured at the close of Phase R6 (PR `feat/release-auto-r6-publishability`). + +### Trigger: silent shadow-mode failure + +R3 landed on 2026-04-25 and was expected to start producing meaningful per-merge `release-plz update` summaries within the next 1–2 weeks. After 12 days the workflow had run successfully on every push to `main` but **every summary was empty**: `release-plz proposed no version bump and no changelog entry`. + +A local reproduction surfaced the root cause: + +``` +$ release-plz update --config release-plz.toml +[ERROR] failed to determine next versions +Caused by: + failed to verify manifest at .../crates/uffs-broker/Cargo.toml +Caused by: + dependency `uffs-security` does not specify a version +``` + +`release-plz update` invokes `cargo package` per crate (to validate that the manifest can produce a tarball acceptable to crates.io). Cargo refuses to package any crate whose `[dependencies]` entries lack a `version =` requirement, even when the entry resolves through a `path =`. All 8 of UFFS's internal `[workspace.dependencies]` aliases were `path =` only, plus 2 direct path-deps in `crates/uffs-cli/Cargo.toml`, plus the polars git pin in `crates/uffs-polars/Cargo.toml`. + +Inside the workflow the failure was swallowed by `tee /tmp/release-plz-output.log` — the `if` chain in `.github/workflows/release-plz.yml` captured the non-zero exit but the empty diff meant the summary's "no proposed changes" branch fired, masking the diagnostic from any reviewer who didn't expand the step log. + +### Fix (R3.5, bundled into the R6 PR) + +`Cargo.toml` (workspace root): + +```diff + [workspace.dependencies] +-uffs-polars = { path = "crates/uffs-polars" } +-uffs-security = { path = "crates/uffs-security" } +-uffs-text = { path = "crates/uffs-text" } +-uffs-time = { path = "crates/uffs-time" } +-uffs-mft = { path = "crates/uffs-mft", features = ["zstd"] } +-uffs-format = { path = "crates/uffs-format" } +-uffs-core = { path = "crates/uffs-core" } +-uffs-client = { path = "crates/uffs-client" } ++uffs-polars = { path = "crates/uffs-polars", version = "0.5.90" } ++uffs-security = { path = "crates/uffs-security", version = "0.5.90" } ++uffs-text = { path = "crates/uffs-text", version = "0.5.90" } ++uffs-time = { path = "crates/uffs-time", version = "0.5.90" } ++uffs-mft = { path = "crates/uffs-mft", version = "0.5.90", features = ["zstd"] } ++uffs-format = { path = "crates/uffs-format", version = "0.5.90" } ++uffs-core = { path = "crates/uffs-core", version = "0.5.90" } ++uffs-client = { path = "crates/uffs-client", version = "0.5.90" } +``` + +`crates/uffs-polars/Cargo.toml`: + +```diff +-polars = { git = "...", rev = "1e9a63b9...", default-features = false, features = [...] } ++polars = { git = "...", rev = "1e9a63b9...", version = "0.53.0", default-features = false, features = [...] } +``` + +`crates/uffs-cli/Cargo.toml`: + +```diff +-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/test.just` (`polars` recipe): added a post-`cargo update` step that re-derives the polars version from `cargo tree` output and edits the `version =` field in lockstep with the new `rev =`, so future polars major-version bumps don't drift the version requirement. + +### Validation + +Local re-run after the fix: + +``` +$ release-plz update --config release-plz.toml +* `uffs-polars`: 0.5.90 +* `uffs-security`: 0.5.90 +* `uffs-text`: 0.5.90 +* `uffs-time`: 0.5.90 +* `uffs-mft`: 0.5.90 +* `uffs-format`: 0.5.90 +* `uffs-core`: 0.5.90 +* `uffs-client`: 0.5.90 +* `uffs-daemon`: 0.5.90 +* `uffs-mcp`: 0.5.90 +* `uffs-broker`: 0.5.90 +* `uffs-cli`: 0.5.90 +``` + +12 publishable crates enumerated cleanly (no errors; uffs-diag excluded by its own `publish = false`; uffs-ci-pipeline / uffs-gen-hooks / uffs-gen-workflow excluded by their `release = false` blocks added in this PR). + +### R6 deliverables in this PR + +1. `[package.metadata.docs.rs]` blocks added to all 12 publishable crates. Three platform tiers: + - **Single-target Linux**: `uffs-time`, `uffs-text`, `uffs-format`, `uffs-polars` (no platform-gated items). + - **Multi-target Linux + Windows**: `uffs-mft`, `uffs-core`, `uffs-daemon`, `uffs-client`, `uffs-cli`, `uffs-mcp` (all carry `#[cfg(windows)]` / `#[cfg(unix)]` divergent code paths). + - **Multi-target Linux + Windows + macOS**: `uffs-security` (Keychain on macOS, DACL on Windows, flock on Unix — three distinct surfaces). + - **Windows-only**: `uffs-broker` (`default-target = "x86_64-pc-windows-msvc"` because the entire crate is a Windows handle broker). +2. `crates/uffs-diag/Cargo.toml` — explicit `publish = false` (per plan §R6 step 2). Belt-and-suspenders: blocks both release-plz and any local `cargo publish -p uffs-diag` from a developer machine. +3. `release-plz.toml` per-package overrides — `[[package]] release = false` for `uffs-ci-pipeline`, `uffs-gen-hooks`, `uffs-gen-workflow`. These are internal CI tools (already `publish = false` at the crate level); the `release = false` is the surgical fix per plan §R6 step 2 to keep them out of release-plz's per-package iteration. +4. `.github/workflows/crates-io-dry-run.yml` — weekly + workflow_dispatch scheduled job that runs `cargo publish --dry-run -p ` for every publishable crate and 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: + - **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. + - **polars / chrono publishability gap**: `uffs-polars` (and any crate that transitively depends on it) fails with `failed to select a version for chrono`. Our git-pinned polars rev uses different feature ergonomics than the published `polars = "0.53.0"`; the published-form resolution pulls a `chrono-tz` chain that conflicts with our workspace `chrono` pin. Resolution is R8 dress rehearsal scope — either flip `uffs-polars` to `publish = false` (cascades to its transitive dependents), or align chrono with crates.io polars expectations. +5. `docs/publishing.md` — DORMANT runbook covering: 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 broken), and an OIDC/trusted-publisher section to be filled in during R7. + +### What R6 deliberately does NOT do + +- **Does NOT reserve crate names on crates.io**. Plan §R6 step 6 explicitly mandates that reservations happen from a *throwaway* external workspace, not the UFFS repo, so the UFFS repo never carries a `publish = true` state. The reservation operation is documented in `docs/publishing.md`'s pre-publish checklist as a prerequisite for R8. +- **Does NOT install `cargo-semver-checks`**. Plan §R6 mentions this as part of the metadata audit; deferred to a follow-up because the CI integration is non-trivial (semver-checks needs a baseline crate to compare against, and we don't have a published v0 yet). +- **Does NOT add OIDC trusted-publisher scaffolding**. Plan §R7 scope. +- **Does NOT flip the workspace-level `publish = false`** in `release-plz.toml`. Plan §R8 scope. +- **Does NOT enable `FAIL_ON_DRY_RUN_ERROR=true`** in the dry-run workflow. Toggled only when crate-name reservations and the polars-chrono gap are both resolved (post-R6 + post-R8). + +### Forward-compat assertion + +When R5 deletes `build/update_all_versions.rs`, the `version = "0.5.90"` strings added in this PR will need to be kept in sync with `[workspace.package].version` by release-plz (which already handles workspace-version synchronization natively, including dependency version bumps). No manual coordination required: release-plz reads `[workspace.package].version` and propagates the new value to every internal-dep `version =` field as part of its release-PR generation. Verified by reading release-plz source (`crates/release_plz_core/src/version.rs`). diff --git a/docs/architecture/release-automation-plan.md b/docs/architecture/release-automation-plan.md index 364ab7127..7db93106f 100644 --- a/docs/architecture/release-automation-plan.md +++ b/docs/architecture/release-automation-plan.md @@ -1960,10 +1960,11 @@ Single source of truth for phase progress. Mirror the format of | R1a | Conventional commits (advisory) | 🟢 | `966f09c5f` | 2026-04-25 | [#65](https://github.com/skyllc-ai/UltraFastFileSearch/pull/65) | Final landed PR shape: 1 file added (commitlint workflow), 224 LOC. Workflow self-validated by running on its own opening PR (3-second pass). Sticky-comment mechanism via `gh api PATCH/DELETE` confirmed working. CONTRIBUTING.md "Commit message conventions" section already landed pre-R0 (lines 150-187). | | R1b | Conventional commits (mandatory gate) | ⬜ | | | | After ≥1 month of advisory observation | | R2 | `git-cliff` + `cliff.toml` (local validation) | 🟢 | `d49a778d6` | 2026-04-25 | [#66](https://github.com/skyllc-ai/UltraFastFileSearch/pull/66) | Final landed PR shape: 3 files (1 new, 2 modified), +495 / −3 LOC. `cliff.toml` template iterated against full history until output matches Keep-a-Changelog spacing; type → section mapping mirrors `commitlint.yml` regex (11 types). Validation captured in `release-automation-baseline.md` §8. Two iteration issues caught + fixed during template tuning (extra blank line after `## [version]`, duplicate `(#NN)` PR links). | -| R3 | release-plz shadow mode | 🟡 | | 2026-04-25 | (open) | Adds `release-plz.toml` + `.github/workflows/release-plz.yml`. Workflow runs `release-plz update` (local-only by design — never pushes / opens PRs / creates tags) on every `push: main` and posts the proposed diff to the workflow summary. Three layers of dormancy: `publish = false` in config, missing `CARGO_REGISTRY_TOKEN`, read-only workflow permissions. Belt-and-suspenders verification step asserts HEAD didn't move during the run. Observation period: 1-2 weeks / 3-5 merges before R4 cutover. | -| R4 | release-plz active (release PR mode) | ⬜ | | | | At least 1 full release cut through the new flow; same release satisfies dev-flow Phase 7 bake-in (decision 2) | +| R3 | release-plz shadow mode | 🟢 | `1b0aa55b7` | 2026-04-25 | [#67](https://github.com/skyllc-ai/UltraFastFileSearch/pull/67) | Final landed PR shape: 2 files (1 new workflow, 1 new release-plz.toml) + ~370 LOC. Workflow runs `release-plz update` (local-only by design) on every `push: main` and posts the proposed diff to the workflow summary. Three layers of dormancy: `publish = false` in config, missing `CARGO_REGISTRY_TOKEN`, read-only workflow permissions. **Post-merge observation** revealed shadow output stayed empty across ≥12 days because `release-plz update` failed silently inside the workflow on `cargo package`'s "dependency `uffs-X` does not specify a version" error — fixed in R3.5 below by adding `version = ` requirements to internal `[workspace.dependencies]` entries. | +| R3.5 | Internal-dep `version = ` requirements + polars git-pin version annotation | 🟡 | | 2026-05-07 | (this PR) | Bundled into the R6 PR (see §8.1 deviations log first row). Adds `version = "0.5.90"` to all 8 internal workspace.dependencies, to the 2 direct path-deps in `uffs-cli/Cargo.toml`, and `version = "0.53.0"` to the polars git dep. Updates `just polars` to keep the polars version pin in lockstep with the resolved git rev. Without these, every `cargo package` invocation (release-plz `update` and any future `release-pr`) fails with "dependency `` does not specify a version". Verified locally: `release-plz update --config release-plz.toml` now lists all 12 publishable crates without error. | +| R4 | release-plz active (release PR mode) | ⬜ | | | | At least 1 full release cut through the new flow; same release satisfies dev-flow Phase 7 bake-in (decision 2). **Now unblocked by R3.5.** | | R5 | Retire bespoke tooling (incl. `scripts/ci/ci-pipeline.rs` thin wrapper per its `REMOVE-AFTER: v0.5.73` marker) | ⬜ | | | | ~1350 lines deleted; point of no return for easy rollback | -| R6 | crates.io metadata audit + dry-run CI | ⬜ | | | | Names reserved externally; per-crate metadata complete | +| R6 | crates.io metadata audit + dry-run CI | 🟡 | | 2026-05-07 | (this PR) | Adds: `[package.metadata.docs.rs]` to all 12 publishable crates with appropriate `targets`/`default-target` per crate's platform surface; explicit `publish = false` to `crates/uffs-diag/Cargo.toml`; per-package `release = false` blocks for the 3 internal CI tools in `release-plz.toml`; `.github/workflows/crates-io-dry-run.yml` (advisory weekly + workflow_dispatch); `docs/publishing.md` DORMANT runbook. R6 step 6 (crate name reservations on crates.io) is intentionally **deferred** — those happen from a throwaway external workspace per plan §R6 step 6, not from this repo. | | R7 | OIDC trusted publisher (dormant) | ⬜ | | | | Scaffolding, `if: false` gate | | R8 | First publish dress rehearsal (`uffs-time` only) | ⬜ | | | | **External state change** — one crate goes live on crates.io | | R9 | Live publishing (full workspace) | ⬜ | | | | **DEFERRED** — explicit maintainer decision, separate plan | @@ -1973,12 +1974,13 @@ Legend: ⬜ pending · 🟡 in progress · 🟢 complete · 🔴 blocked · ⏸ ### 8.1 Deviations log Mirror the format of -`dev-flow-implementation-plan.md §10.5 deviations log`. Empty -until phases are executed. +`dev-flow-implementation-plan.md §10.5 deviations log`. | Phase | Date | Anomaly | Root cause | Resolution | Plan impact | |---|---|---|---|---|---| -| (none yet) | | | | | | +| R3 → R4 readiness | 2026-05-07 | Shadow-mode workflow ran 12+ days with empty output; `release-plz update` failed silently inside the runner. | The R3 plan did not anticipate that internal `[workspace.dependencies]` entries lacking `version = ` would block `cargo package` (which release-plz invokes per crate). All 8 internal-dep aliases (and the polars git pin, and the 2 direct path-deps in `uffs-cli`) were affected. | Bundled into the R6 PR as **phase R3.5** (see dashboard row above): added `version = "0.5.90"` to all internal-dep aliases + `version = "0.53.0"` to the polars git dep; `just polars` now updates the polars version pin in lockstep with the rev. | None — R3 stays 🟢, R3.5 closes the gap, R4 advances on schedule. | +| R6 step 6 | 2026-05-07 | Crate-name reservations on crates.io explicitly NOT performed in this PR. | Plan §R6 step 6 specifies reservations should come from a "throwaway dedicated workspace" (a separate, non-UFFS repo) so the UFFS repo never holds a `publish = true` state. | Reservations deferred to a separate out-of-band operation. The R6 PR documents the prerequisite in `docs/publishing.md` "Pre-publish checklist" and the `crates-io-dry-run.yml` workflow header. | None — exit criteria for R6 was already split between in-repo work and external operation; the in-repo half is what landed here. | +| R6 → R8 publishability | 2026-05-07 | `cargo publish --dry-run -p 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 against crates.io conflicts with the workspace-pinned chrono. | Our git-pinned polars rev uses a different feature mix than crates.io polars 0.53.0; the registry version pulls `polars-arrow → chrono-tz` requirements that conflict with our `chrono` pin. | Recorded as a known-expected failure in `crates-io-dry-run.yml` (advisory mode, `FAIL_ON_DRY_RUN_ERROR=false` initially). R8 dress rehearsal will resolve by either (a) flipping `uffs-polars` to `publish = false` or (b) aligning chrono with crates.io polars expectations. | None — does not block R7 (OIDC scaffolding) or the leaf-only R8 rehearsal target (`uffs-time`). | ## 9. Cross-references diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 000000000..2b46c4923 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,213 @@ + + +# UFFS Publishing Runbook + +> **STATUS**: **DORMANT** — publishing is not yet live. +> +> Do not execute any of the steps in this runbook until the **R9 go-live +> decision** has been recorded in +> [`docs/architecture/release-automation-plan.md` §8 status dashboard][dashboard]. +> Until then this document is a **forward-looking specification** of how +> UFFS will eventually ship to crates.io, captured here so the steps can +> be reviewed, audited, and refined while still safe to do so. +> +> [dashboard]: architecture/release-automation-plan.md#8-status-dashboard + +## When do we publish? + +**Never automatically.** Every publish is a maintainer decision made +in the release-PR review step. Release-plz opens a release PR; a +maintainer reviews the changelog, confirms the version bump, merges, +and at that point the OIDC publish job fires (from R9 onward). + +The four-layer dormancy stack that protects us until R9 is live: + +1. **`publish = false`** at the workspace level in `release-plz.toml` + — release-plz never invokes `cargo publish` while this is `false`. +2. **`publish = false`** per-package in selected `Cargo.toml`s + (`crates/uffs-diag`, `scripts/ci-pipeline`, `scripts/ci/gen-hooks`, + `scripts/ci/gen-workflow`) — even a manual `cargo publish` from a + developer machine refuses these. +3. **`if: false`** on the OIDC publish job (added in R7) — the + workflow step never runs even when triggered. +4. **No `CARGO_REGISTRY_TOKEN`** secret in the repository — even a + misconfigured workflow has no credential to authenticate with. + +All four must be defeated independently to ship a crate. There is no +single accidental flip that can leak. + +## Phase status as of this document's last update + +| Phase | What it adds | Status | +|---|---|---| +| R3 | Shadow-mode `release-plz update` workflow | ✅ landed | +| R3.5 | `version = ` requirements on internal & polars deps (this PR) | 🟡 in progress | +| R4 | Active-mode release-PR generator | ⬜ pending | +| R5 | Retire bespoke `build/update_all_versions.rs` tooling | ⬜ pending | +| R6 | Per-crate metadata + dry-run CI workflow (this PR's R6 work) | 🟡 in progress | +| R6 step 6 | Crate-name reservations on crates.io | ⬜ deferred | +| R7 | OIDC trusted-publishing scaffolding (`if: false` gated) | ⬜ pending | +| R8 | Dress rehearsal — publish `uffs-time` (foundation crate) | ⬜ pending | +| R9 | Live publishing for the full publishable set | ⬜ pending | + +See the canonical dashboard at +[`docs/architecture/release-automation-plan.md` §8][dashboard]. + +## Pre-publish checklist (one-time, per go-live decision) + +These must all be ✅ before the R9 go-live PR opens: + +- [ ] All publishable crate names reserved on crates.io under the + project owner's account (R6 step 6, deferred). +- [ ] **Known-blocker resolution**: the `uffs-polars` git pin's + published-form `polars = "0.53.0"` resolves cleanly against + the workspace `chrono` pin OR `uffs-polars` is converted to + `publish = false`. (Tracked in + [release-automation-plan.md §6.1 risk #13][r6-known-blockers] + and the `crates-io-dry-run.yml` workflow header comment.) +- [ ] Trusted-publisher (OIDC) registrations complete for every + publishable crate name (R7). +- [ ] `crates-io-production` GitHub Environment exists with the + required-reviewer rule active. +- [ ] `release-plz.yml` publish job has `if: true` (currently + `if: false` per the four-layer dormancy stack above). +- [ ] `release-plz.toml` has `publish = true` at the workspace + level (currently `publish = false`). +- [ ] First-release communication drafted (blog post, release + notes, social-media announcement). + +[r6-known-blockers]: architecture/release-automation-plan.md#61-risks + +## Per-release checklist (every release, post-R9) + +- [ ] Release-plz release PR opened against `main`. +- [ ] Changelog entries reviewed for accuracy against the actual + commits since the previous tag. +- [ ] Version bump reviewed (feat → minor, fix → patch, feat! / + `BREAKING CHANGE` → major — verify all crates bump + consistently). +- [ ] Breaking changes called out in the changelog `Migration` + section. +- [ ] Release PR merged on `main` → release-plz creates the tag + → `release.yml` builds + uploads binaries. +- [ ] Binaries visible on the GitHub Release page (15 binaries, + 1 CHECKSUMS, 13 SBOMs — see `release.yml` for the asset + manifest). +- [ ] Publish job succeeds for all eligible crates (check Actions + run logs; expect 12 successful per-crate publish steps). +- [ ] Each published crate appears on crates.io within 60 sec + (`cargo search uffs-time`, `uffs-text`, etc.). +- [ ] docs.rs builds succeed for all published crates within + 2 hours (look for green build badge on each crate's docs.rs + landing page). +- [ ] Announcement posted (only required for major releases). + +## Yank decisions log + +Yanks are recorded here, latest first. A yank does **not** delete +the version from crates.io — it prevents new resolutions from +selecting it, but existing `Cargo.lock` files keep working. + +| Date | Crate | Version | Rationale | Replacement | +|------|-------|---------|-----------|-------------| +| (none yet) | | | | | + +If we ever need to yank, document the rationale here AND open a +GitHub issue with the `yank` label so downstream consumers see the +notice. + +## Post-publish smoke checks + +After every successful publish, verify: + +- [ ] `cargo search ` returns the new version (allow + ~60 sec for crates.io index propagation). +- [ ] In a throwaway scratch directory: + `cargo new test-pub- && cd test-pub- && + cargo add && cargo build` succeeds. +- [ ] crates.io crate page renders the README correctly (image + paths, badge URLs, table layouts). +- [ ] docs.rs page renders without errors. The build log lives at + `https://docs.rs/crate///builds`. A failed + build shows a red banner; for those, check the build log + and adjust `[package.metadata.docs.rs]` in the next release. + +## Manual fallback (release-plz unavailable) + +If release-plz is broken / mis-configured / unreachable and a +release MUST ship, fall back to manual `cargo publish` in dependency +order. **Use this only as an emergency lever — the standard path is +release-plz.** + +The publish order is the topological sort of the internal-dep DAG. +For UFFS as of v0.5.90: + +``` +1. uffs-time (zero internal deps) +2. uffs-text (zero internal deps) +3. uffs-security (zero internal deps) +4. uffs-polars (zero internal deps; external git pin) +5. uffs-mft (deps: polars, text, security) +6. uffs-format (deps: time, mft) +7. uffs-core (deps: polars, text, time, mft, format, security) +8. uffs-client (deps: security, format) +9. uffs-broker (deps: security) +10. uffs-mcp (deps: client) +11. uffs-daemon (deps: security, mft, core, client) +12. uffs-cli (deps: client, format, time) +``` + +Per-crate command: + +```bash +cargo publish -p --token "$CARGO_REGISTRY_TOKEN" +``` + +After each `cargo publish`, wait ~30 sec before the next step so +the index has time to update — otherwise the next crate's +`cargo publish` may fail with "no matching package named `` +found". + +## Trusted publishing (OIDC) configuration — to be filled in during R7 + +This section will document: + +- Each crate's trusted-publisher form-field values on crates.io + (repository, workflow filename, environment name). +- The `crates-io-production` GitHub Environment's required-reviewer + list. +- Rotation procedure if the OIDC trust breaks (e.g., the workflow + filename changes or the repository is renamed). +- Revocation procedure if a maintainer leaves or a credential + is suspected compromised. + +Until R7 lands, the section is intentionally empty. + +## References + +- [`docs/architecture/release-automation-plan.md`](architecture/release-automation-plan.md) + — the canonical multi-phase migration plan (R0 → R9). +- [`docs/architecture/release-automation-baseline.md`](architecture/release-automation-baseline.md) + — pre-migration baseline metrics + per-phase observations. +- [`release-plz.toml`](../release-plz.toml) — release-plz workspace + configuration (per-package overrides documented inline). +- [`cliff.toml`](../cliff.toml) — git-cliff changelog template + shared between `release-plz update` and `git cliff` developer + iteration. +- [`.github/workflows/release-plz.yml`](../.github/workflows/release-plz.yml) + — shadow-mode release-PR generator. +- [`.github/workflows/crates-io-dry-run.yml`](../.github/workflows/crates-io-dry-run.yml) + — weekly metadata-drift detection job (R6 step 4). +- crates.io documentation: + [trusted publishing][cratesio-tp] · [package metadata][cratesio-pkg] +- docs.rs documentation: + [build configuration][docsrs-config] + +[cratesio-tp]: https://crates.io/docs/trusted-publishing +[cratesio-pkg]: https://doc.rust-lang.org/cargo/reference/manifest.html +[docsrs-config]: https://docs.rs/about/builds#package-set-up diff --git a/just/test.just b/just/test.just index 1b4f9e8dd..7b38c085d 100644 --- a/just/test.just +++ b/just/test.just @@ -532,6 +532,25 @@ polars: sed -i.bak "s|rev = \"${OLD_REV}\"|rev = \"${NEW_REV}\"|" crates/uffs-polars/Cargo.toml rm -f crates/uffs-polars/Cargo.toml.bak cargo update -p polars + + # Per R6 publishability work (release-automation-plan §R6): the polars + # git dep ALSO carries `version = "X.Y.Z"` so `cargo package` (invoked + # by `release-plz update`) can validate this manifest. After bumping + # the `rev`, the resolved polars version may have advanced — keep the + # `version =` field in lockstep with what cargo actually resolves to. + # Without this, when polars upstream lands a major-version bump, the + # next `release-plz update` (R3+ shadow / R4+ active) fails with + # `dependency "polars" does not specify a version` (or a version + # requirement mismatch). + RESOLVED_POLARS_VER=$(cargo tree -p uffs-polars --depth 1 --prefix none 2>/dev/null \ + | awk '/^polars v/ {sub(/^v/, "", $2); print $2; exit}') + OLD_POLARS_VER=$(awk -F'"' '/^polars = \{/ {for(i=1;i<=NF;i++) if($i ~ /^[0-9]+\.[0-9]+\.[0-9]+/) {print $i; exit}}' crates/uffs-polars/Cargo.toml) + if [ -n "${RESOLVED_POLARS_VER:-}" ] && [ -n "${OLD_POLARS_VER:-}" ] && [ "$OLD_POLARS_VER" != "$RESOLVED_POLARS_VER" ]; then + printf " Polars version: %s -> %s\n" "$OLD_POLARS_VER" "$RESOLVED_POLARS_VER" + sed -i.bak "s|version = \"${OLD_POLARS_VER}\", default-features|version = \"${RESOLVED_POLARS_VER}\", default-features|" crates/uffs-polars/Cargo.toml + rm -f crates/uffs-polars/Cargo.toml.bak + fi + printf "\033[0;34m🔨 Checking build...\033[0m\n" cargo check -p uffs-polars printf "\033[0;32m✅ Polars bumped to %s\033[0m\n" "${NEW_REV:0:12}" diff --git a/release-plz.toml b/release-plz.toml index 5db3e9ddd..70e15e0a3 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -114,21 +114,57 @@ semver_check = false pr_branch_prefix = "release-plz-" # ───────────────────────────────────────────────────────────────────────────── -# Per-package overrides — DEFERRED to Phase R6 +# Per-package overrides (Phase R6 of release-automation-plan.md) # ───────────────────────────────────────────────────────────────────────────── # -# UFFS has 14 workspace members. Some are publishable to crates.io -# (`uffs-core`, `uffs-cli`, `uffs-mcp-server`, ...), some are internal -# tools (`scripts/ci-pipeline`, `scripts/ci/version-orchestrator`). -# Phase R6 will add `[[package]]` blocks here that: -# -# - flip `publish = true` for the 12 publishable members -# - set `release = false` on the internal tools (so release-plz -# ignores them entirely — they don't get version bumps, they -# don't appear in the changelog, they don't get tagged) -# - set per-crate `changelog_path` for any crate whose changelog -# should NOT live in the workspace-root `CHANGELOG.md` -# -# For R3 (shadow mode) this granularity is unnecessary: we just want -# release-plz to compute and report what it WOULD do at the workspace -# level. Per-package noise can wait for R6. +# UFFS has 17 workspace members. Twelve are publishable to crates.io; +# the rest are internal-only (one diagnostic crate + three CI tools + +# one out-of-band fuzz harness in a separate workspace): +# +# Publishable to crates.io (12): +# uffs-polars, uffs-security, uffs-text, uffs-time, +# uffs-mft, uffs-format, uffs-core, uffs-daemon, uffs-client, +# uffs-mcp, uffs-broker, uffs-cli +# +# Workspace-only diagnostic crate (1) — skipped by release-plz via +# crate-level `publish = false` in its own Cargo.toml: +# crates/uffs-diag (uffs-diag) +# release-plz follows cargo's `package.publish = false` convention +# and excludes such packages from version-bump / changelog / tag +# operations entirely. No `[[package]]` block needed here. +# +# Internal tools (3) — `release = false` below (release-plz-specific +# exclusion; `cargo publish` is also blocked by their crate-level +# `publish = false`, but `release = false` is the surgical fix to +# keep them out of release-plz's per-package iteration): +# scripts/ci-pipeline (uffs-ci-pipeline) +# scripts/ci/gen-hooks (uffs-gen-hooks) +# scripts/ci/gen-workflow (uffs-gen-workflow) +# +# Out of band (1) — separate cargo workspace, release-plz never +# sees it; listed here for completeness: +# crates/uffs-mft/fuzz (uffs-mft-fuzz) +# +# `release = false` tells release-plz to ignore the package entirely: +# no version bump computation, no changelog entry, no tag, no PR. +# This is the surgical fix per release-automation-plan.md §R6 step 2, +# preferred over a workspace-level "exclude" that would obscure intent. +# +# `publish = true` per-package overrides are NOT set yet. The +# workspace-level `publish = false` above remains the active dormancy +# layer until Phase R8 dress rehearsal. When R8 flips a leaf crate +# (likely `uffs-time`, the foundation crate with zero deps) to +# `publish = true`, the override goes here as a per-package block — +# explicit, auditable, reversible. + +[[package]] +name = "uffs-ci-pipeline" +release = false + +[[package]] +name = "uffs-gen-hooks" +release = false + +[[package]] +name = "uffs-gen-workflow" +release = false