From ee16b92435d02f66ab2f02e67beba54cb777b39d Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:48:44 -0700 Subject: [PATCH] =?UTF-8?q?chore(r5):=20retire=20bespoke=20version=20tooli?= =?UTF-8?q?ng=20=E2=80=94=20release-plz=20takes=20over?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polars 0.54.4 on crates.io resolved the chrono conflict that blocked release-plz's cargo package step, so the bespoke version-bump machinery can finally be retired. Deleted: - build/update_all_versions.rs (1157 lines) - .github/workflows/auto-tag-release.yml (168 lines) - scripts/ci/ci-pipeline.rs (thin wrapper, REMOVE-AFTER v0.5.73 met) - increment_version()/version_bump() from version.rs - STEP_VERSION_INCREMENT from workflow.rs Modified: - .gitignore: restored blanket build/ ignore (carve-out removed) - version.rs: dropped now-unfulfilled print_stdout expectation - ship.rs/phases.rs: removed version step, fixed completion messages - justfile recipes: version-bump retired, quick-deploy renumbered R4: activated release-plz push trigger + workflow_dispatch bridge to release.yml R7: added dormant OIDC publish job (if: false) for crates.io trusted publishing See docs/architecture/release-automation-plan.md §R4, §R5, §R7 --- .github/workflows/auto-tag-release.yml | 168 --- .github/workflows/release-plz.yml | 98 +- .gitignore | 11 +- build/update_all_versions.rs | 1157 ------------------ docs/architecture/release-automation-plan.md | 7 +- just/build.just | 15 +- just/dev.just | 23 +- just/help.just | 2 +- just/workflow.just | 6 +- scripts/ci-pipeline/src/phases.rs | 49 +- scripts/ci-pipeline/src/ship.rs | 26 +- scripts/ci-pipeline/src/version.rs | 68 +- scripts/ci-pipeline/src/workflow.rs | 11 +- scripts/ci/ci-pipeline.rs | 53 - 14 files changed, 154 insertions(+), 1540 deletions(-) delete mode 100644 .github/workflows/auto-tag-release.yml delete mode 100755 build/update_all_versions.rs delete mode 100755 scripts/ci/ci-pipeline.rs diff --git a/.github/workflows/auto-tag-release.yml b/.github/workflows/auto-tag-release.yml deleted file mode 100644 index 1e1b46ff8..000000000 --- a/.github/workflows/auto-tag-release.yml +++ /dev/null @@ -1,168 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2025-2026 SKY, LLC. -# -# Auto-Tag Release — the bridge between `just ship` and `release.yml`. -# -# When a commit lands on `main` that changes the `[workspace.package]` -# `version` in `Cargo.toml`, this workflow invokes `release.yml` via -# `workflow_dispatch` with the new version and commit SHA. `release.yml` -# itself creates + pushes the `vX.Y.Z` tag and publishes the release. -# -# Why NOT push the tag from here: -# -# `release.yml`'s `workflow_dispatch` path has a guard that fails if -# the tag already exists (release.yml:101-108) and a later step that -# creates + pushes the tag itself (release.yml:491-499). Delegating -# tag creation to `release.yml` makes it the single source of truth -# for "did this version ship" — a tag on origin means a release was -# built successfully, nothing weaker. -# -# Why the explicit `gh workflow run` invocation: -# -# The default `GITHUB_TOKEN` is prevented from triggering downstream -# workflow runs (infinite-loop guard), so relying on the tag push to -# fire `release.yml`'s `push: tags: v*` trigger would require a PAT. -# Instead we explicitly invoke `release.yml` via `workflow_dispatch`, -# which the default token IS permitted to do (actions: write). -# Developers pushing tags manually still hit the normal `push: tags: -# v*` trigger — both paths remain supported. -# -# This workflow completes the hands-off release flow: -# -# just ship --> opens release/vX.Y.Z PR --> auto-merge on CI green -# | -# v -# auto-tag-release.yml -# | -# v -# release.yml (build + publish) - -name: "🏷️ Auto-Tag Release" - -on: - push: - branches: [main] - paths: - - "Cargo.toml" - workflow_dispatch: - # Manual re-trigger. When dispatched (with no inputs), replays - # the HEAD-vs-HEAD~1 version diff logic against current main. - # Useful if `release.yml` has been fixed after a failed run and - # the same version-bump commit should be retried. The - # idempotency guard (tag already on origin) handles double-fire - # races automatically. - -permissions: - # Read commit history (for the HEAD vs HEAD~1 version diff). - contents: read - # Invoke `release.yml` via `gh workflow run`. - actions: write - -concurrency: - # Serialise auto-tag attempts. Two version bumps landing in quick - # succession is rare but possible; queue them so the second sees the - # first's release workflow already running (or tag already on origin) - # and becomes a no-op skip. - group: auto-tag-release - cancel-in-progress: false - -jobs: - maybe-tag: - name: "🏷️ Tag if workspace version bumped" - runs-on: ubuntu-latest - timeout-minutes: 5 - # Belt-and-suspenders loop guard. This workflow doesn't modify - # Cargo.toml or push tags, so nothing it does could re-trigger - # itself anyway, but skip bot-authored pushes defensively. - if: github.actor != 'github-actions[bot]' - steps: - - name: Checkout (fetch HEAD + HEAD~1 for version diff) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 2 - # Default GITHUB_TOKEN authenticates the checkout. We no - # longer push anything from this workflow (release.yml - # pushes the tag itself), so credential persistence is - # optional; keep it on for any future git reads. - persist-credentials: true - - - name: Extract current and previous workspace versions - id: versions - shell: bash - run: | - extract_version() { - # Read the `version = "..."` line from the [workspace.package] - # section of the given Cargo.toml content (stdin). - awk '/^\[workspace\.package\]/{flag=1; next} flag && /^version/{print; exit}' \ - | cut -d'"' -f2 - } - - new=$(extract_version < Cargo.toml) - old=$(git show HEAD~1:Cargo.toml 2>/dev/null | extract_version || true) - - echo "Current: ${new:-}" - echo "Previous: ${old:-}" - - printf 'new=%s\n' "$new" >> "$GITHUB_OUTPUT" - printf 'old=%s\n' "${old:-}" >> "$GITHUB_OUTPUT" - - - name: Decide whether to invoke release.yml - id: plan - shell: bash - env: - NEW_VERSION: ${{ steps.versions.outputs.new }} - OLD_VERSION: ${{ steps.versions.outputs.old }} - run: | - if [[ -z "$NEW_VERSION" ]]; then - echo "::notice::No [workspace.package].version found in Cargo.toml — nothing to do." - echo "tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then - echo "::notice::Workspace version unchanged ($NEW_VERSION) — nothing to do." - echo "tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - tag="v$NEW_VERSION" - - # Idempotency: if the tag already exists on origin, a release - # has already been produced for this version. Skip silently - # (supports `just ship` replays on a branch that was already - # auto-merged). - if git ls-remote --exit-code origin "refs/tags/$tag" >/dev/null 2>&1; then - echo "::notice::Tag $tag already exists on origin — release already shipped, skipping." - echo "tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "Will invoke release.yml for $tag (commit ${{ github.sha }})" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - - - name: Trigger release.yml via workflow_dispatch - if: steps.plan.outputs.tag != '' - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.plan.outputs.tag }} - SHA: ${{ github.sha }} - run: | - # Explicit workflow_dispatch invocation. `release.yml` itself - # creates + pushes the vX.Y.Z tag after a successful build - # (release.yml:491-499), so we do NOT push the tag here — - # that would collide with release.yml's own tag-exists guard - # (release.yml:101-108) and abort the run. - # - # The version + commit_sha inputs match release.yml's - # workflow_dispatch schema exactly. - gh workflow run release.yml \ - --repo "${{ github.repository }}" \ - --ref main \ - -f version="$TAG" \ - -f commit_sha="$SHA" \ - -f triggered_by="auto-tag-release[sha:${SHA:0:7}]" - - echo "::notice::Invoked release.yml for $TAG (commit ${SHA:0:7})" - echo "" - echo "Watch: gh run list --repo ${{ github.repository }} --workflow=release.yml --limit 1" diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 021fc672b..faa099515 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -216,22 +216,16 @@ # • https://release-plz.ieni.dev/docs/usage/release # • https://github.com/release-plz/release-plz/blob/main/.github/workflows/release-plz.yml -name: "💤 Release-plz (deferred — manual-only)" +name: "🚀 Release-plz (active)" on: - # ─── push trigger DISABLED 2026-05-08 — see DEFERRAL NOTICE above ─── - # Workflow run 25583934251 on commit d3a1adc1d (R6 → R8 Path A merged) - # confirmed that release-plz's hardcoded `cargo package --workspace` - # trips the polars-arrow chrono ≤0.4.41 vs workspace 0.4.44 conflict - # regardless of `release = false` / `publish = false` flags. Until - # polars upstream ships a chrono-compatible release this workflow is - # manual-only. Uncomment the `push:` block below to re-enable. - # push: - # branches: [main] - # Manual `gh workflow run release-plz.yml` for the maintainer to spot- - # check what release-plz proposes (e.g. on a temporary branch where - # the polars rev is updated, ahead of formally re-enabling the push - # trigger). + # R4 ACTIVATED 2026-06-09 — Polars 0.54.4 on crates.io resolved the + # chrono conflict that blocked release-plz's `cargo package --workspace`. + # Auto-trigger on every push to main; release-plz analyzes conventional + # commits and opens release PRs when feat/fix/perf/security changes land. + push: + branches: [main] + # Manual trigger for ad-hoc inspection or testing workflow changes. workflow_dispatch: # Default to ZERO permissions; each job grants only what it needs. @@ -347,6 +341,7 @@ jobs: # release PR — release-plz detects this internally. So this # job runs on every push but only does work when the PR # actually merged. + id: release-plz uses: release-plz/action@064f4d1e36c843611ddf013be726beaa4ad804db # v0.5.129 with: command: release @@ -357,3 +352,78 @@ jobs: # comment block at the top of this file. Adding this env # var is the SECOND of TWO layers that must change to # enable publishing; both flip in R8. + + # R4/R5: Bridge to release.yml — release-plz creates tags via GITHUB_TOKEN + # which doesn't trigger downstream workflows (GitHub anti-loop policy). + # Explicitly dispatch release.yml to build binaries. + - name: Trigger release.yml for binary builds + if: steps.release-plz.outputs.releases_created == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Releases created: ${{ steps.release-plz.outputs.releases }}" + for release in ${{ steps.release-plz.outputs.releases }}; do + tag=$(echo "$release" | jq -r '.tag') + gh workflow run release.yml --ref main \ + -f version="$tag" \ + -f triggered_by="release-plz[$tag]" + done + + # ───────────────────────────────────────────────────────────────── + # R7: OIDC Trusted Publishing scaffolding (crates.io) + # ───────────────────────────────────────────────────────────────── + # + # Phase R7 — OIDC trusted publisher scaffolding. This job is gated + # by `if: false` until Phase R8 (first dress rehearsal). It sets + # up the OIDC token exchange with crates.io for passwordless, + # short-lived publishing credentials. + # + # Enabling this in R8 requires: + # 1. Add `CARGO_REGISTRY_TOKEN` secret (temporary, for bootstrap) + # 2. Configure crates.io crate-level trusted publishers (web UI) + # 3. Flip `if: false` → `if: github.repository_owner == 'skyllc-ai'` + # 4. Remove `CARGO_REGISTRY_TOKEN` env var (OIDC replaces it) + # + # See: docs/architecture/release-automation-plan.md §Phase R7/R8 + # + crates-io-publish: + name: crates.io / OIDC publish (R7 scaffolding) + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: release-plz-release + + # DORMANT until R8 — flip this to enable trusted publishing + if: false + + environment: crates.io-publish + permissions: + contents: read + id-token: write # Required for OIDC token exchange with crates.io + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Install Rust (nightly toolchain) + uses: dtolnay/rust-toolchain@nightly + + - name: Cache cargo dependencies + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + shared-key: crates-io-publish + + # OIDC debug — verify token is available before attempting publish + - name: Verify OIDC token availability + run: | + echo "OIDC token endpoint: $ACTIONS_ID_TOKEN_REQUEST_URL" + echo "OIDC request token available: ${{ secrets.ACTIONS_ID_TOKEN_REQUEST_TOKEN != '' }}" + + # Placeholder for R8 — actual publish steps will go here + # - name: Publish to crates.io (R8) + # env: + # # No CARGO_REGISTRY_TOKEN needed — OIDC handles auth + # run: | + # cargo publish -p uffs-time --dry-run # Dry-run first + # # Actual publish gated by manual approval in R8 diff --git a/.gitignore b/.gitignore index 181ca1f85..0cc8fedb2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,15 +34,8 @@ target/ # ============================================================================= # Build artifacts & generated binaries # ============================================================================= -# `build/` holds generated artifacts (workflow state, logs) that must stay -# out of git, EXCEPT the bespoke version-bump rust-script which is invoked -# by `just ship` (see `just/build.just`, `just/dev.just`, -# `scripts/ci-pipeline/src/version.rs`). That script is the single source of -# truth for workspace version bumps until release-plz takes over in Phase R5 -# of `docs/architecture/release-automation-plan.md` — at which point the -# script is deleted and this exception line goes with it. -build/* -!build/update_all_versions.rs +# Build artifacts directory — all generated files stay out of git. +build/ *.rlib *.bin # `*.bin` is meant to keep transient generated blobs out of git, but the diff --git a/build/update_all_versions.rs b/build/update_all_versions.rs deleted file mode 100755 index 299fbaf4f..000000000 --- a/build/update_all_versions.rs +++ /dev/null @@ -1,1157 +0,0 @@ -#!/usr/bin/env rust-script -// ============================================================================= -// build/update_all_versions.rs - Professional Version Management Tool -// ============================================================================= -// -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2025-2026 SKY, LLC. -// -// UFFS - UltraFastFileSearch: High-Performance File Search Tool -// -//! # Comprehensive Workspace Version Management Tool -//! -//! A professional-grade Rust script for managing semantic versioning across -//! entire workspace projects with dynamic detection and comprehensive file updates. -//! -//! ## 🎯 Core Philosophy -//! -//! This tool embodies the **Rust Master** approach to version management: -//! - **Zero Configuration**: Dynamically detects project metadata -//! - **Comprehensive Coverage**: Updates ALL version references across the codebase -//! - **Safety First**: Dry-run mode prevents accidental modifications -//! - **Workspace Native**: Designed specifically for Cargo workspace projects -//! - **Pattern Resilient**: Handles multiple formatting styles and edge cases -//! -//! ## 🏗️ Architecture Overview -//! -//! ```text -//! ┌─────────────────────────────────────────────────────────────────┐ -//! │ Version Update Pipeline │ -//! ├─────────────────────────────────────────────────────────────────┤ -//! │ 1. Dynamic Detection Phase │ -//! │ ├── Package Name (from [package] section) │ -//! │ ├── Repository Name (from repository URL) │ -//! │ └── Current Version (from [workspace.package] section) │ -//! │ │ -//! │ 2. Version Calculation Phase │ -//! │ ├── Parse semantic version (major.minor.patch) │ -//! │ ├── Apply increment type (patch/minor/major) │ -//! │ └── Generate new semantic version │ -//! │ │ -//! │ 3. File Update Phase (with pattern matching) │ -//! │ ├── Cargo.toml (workspace version + flexible spacing) │ -//! │ ├── README.md (5 pattern types + dependency refs) │ -//! │ ├── Documentation (version tags + exact matches) │ -//! │ └── CITATION.cff (version + date-released) │ -//! └─────────────────────────────────────────────────────────────────┘ -//! ``` -//! -//! ## 🔧 Technical Implementation Details -//! -//! ### Workspace Detection Strategy -//! The tool uses a **hierarchical parsing approach** for Cargo.toml: -//! 1. **Section-aware parsing**: Tracks current TOML section context -//! 2. **Workspace-first**: Prioritizes `[workspace.package]` for version source -//! 3. **Fallback resilience**: Graceful handling of missing sections -//! -//! ### Pattern Matching Engine -//! Implements **multi-pattern recognition** for maximum coverage: -//! - **Spacing agnostic**: Handles various whitespace patterns -//! - **Context aware**: Different patterns for different file types -//! - **Dependency smart**: Uses package name for dependency declarations -//! -//! ## 📋 Usage Examples -//! -//! ```bash -//! # Safe exploration (recommended first step) -//! ./build/update_all_versions.rs --help -//! ./build/update_all_versions.rs patch --dry-run -//! -//! # Production usage -//! ./build/update_all_versions.rs patch # 0.1.143 → 0.1.144 -//! ./build/update_all_versions.rs minor # 0.1.143 → 0.2.0 -//! ./build/update_all_versions.rs major # 0.1.143 → 1.0.0 -//! ``` -//! -//! ## 🛡️ Safety Guarantees -//! -//! - **Dry-run mode**: Test changes without file modifications -//! - **Atomic operations**: Each file update is independent -//! - **Validation**: Semantic version format validation -//! - **Error isolation**: Single file failures don't affect others -//! -//! ## 🎨 Output Design -//! -//! Uses **semantic emojis** and **structured progress reporting**: -//! - 🔄 Process initiation -//! - 📦 Project metadata -//! - 📍 Current state -//! - 🎯 Target state -//! - 📝 File operations -//! - ✅ Success confirmations -//! - ⚠️ Warnings and skips -//! -//! ```cargo -//! [dependencies] -//! # No external dependencies - uses only std library for maximum portability -//! ``` - -use std::fs; -use std::env; -use std::process::Command; - -/// # Main Entry Point - Version Update Orchestrator -/// -/// Coordinates the entire version update process through a **three-phase pipeline**: -/// 1. **Discovery Phase**: Dynamic project metadata extraction -/// 2. **Calculation Phase**: Semantic version increment computation -/// 3. **Update Phase**: Comprehensive file modification (or simulation) -/// -/// ## 🔍 Command Line Interface Design -/// -/// The CLI follows **Unix philosophy** with sensible defaults: -/// - **Default behavior**: Patch increment (safest option) -/// - **Explicit flags**: `--help`, `--dry-run` for safety -/// - **Positional args**: Increment type as first argument -/// -/// ## 🛡️ Error Handling Strategy -/// -/// Uses **fail-fast with context** approach: -/// - Early validation of all inputs before any modifications -/// - Detailed error messages with actionable guidance -/// - Graceful degradation for optional operations -/// -/// ## 📊 Process Flow -/// -/// ```text -/// Input Args → Validation → Discovery → Calculation → Update/Simulate → Report -/// ↓ ↓ ↓ ↓ ↓ ↓ -/// Parse Help/DryRun Metadata New Version File Ops Success -/// ``` -/// -/// ## 🎯 Return Value Semantics -/// -/// - `Ok(())`: All operations completed successfully -/// - `Err(Box)`: Any step failed with descriptive error -fn main() -> Result<(), Box> { - let args: Vec = env::args().collect(); - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 0: Command Line Interface Processing - // ═══════════════════════════════════════════════════════════════════════ - - // Handle help request - early exit for documentation - if args.len() > 1 && (args[1] == "--help" || args[1] == "-h") { - print_help(); - return Ok(()); - } - - // Extract increment type with sensible default (patch is safest) - let increment_type = args.get(1).map(|s| s.as_str()).unwrap_or("patch"); - - // Detect dry-run mode for safe testing - let dry_run = args.contains(&"--dry-run".to_string()) || args.contains(&"-n".to_string()); - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 1: Dynamic Project Discovery - // ═══════════════════════════════════════════════════════════════════════ - - // Extract project metadata using workspace-aware parsing - // These operations are designed to fail fast if Cargo.toml is malformed - let package_name = get_package_name()?; - let repository_name = get_repository_name()?; - let current_version = get_current_version()?; - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 2: User Communication & Status Reporting - // ═══════════════════════════════════════════════════════════════════════ - - println!("🔄 Comprehensive version update for {} project", package_name); - println!("📦 Repository: {}", repository_name); - println!("📋 Increment type: {}", increment_type); - println!("📍 Current version: {}", current_version); - - if dry_run { - println!("🔍 DRY RUN MODE - No files will be modified"); - } - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 3: Version Calculation & Validation - // ═══════════════════════════════════════════════════════════════════════ - - // Calculate new version using semantic versioning rules - let new_version = increment_version(¤t_version, increment_type)?; - println!("🎯 New version: {}", new_version); - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 4: File Update Orchestration (or Simulation) - // ═══════════════════════════════════════════════════════════════════════ - - if dry_run { - // Simulation mode: Show what would happen without making changes - println!("📝 Would update Cargo.toml..."); - println!("📝 Would update README.md..."); - println!("📝 Would update documentation files..."); - println!("📝 Would update CITATION.cff (version + date-released)..."); - println!("🔒 Would refresh Cargo.lock (cargo generate-lockfile --offline)..."); - println!("✅ Dry run completed - no files were modified"); - println!("📦 {} would be updated to version: {}", package_name, new_version); - } else { - // Production mode: Execute actual file modifications - // Each update operation is independent and can fail without affecting others - update_cargo_toml(¤t_version, &new_version)?; - update_readme(&package_name, ¤t_version, &new_version)?; - update_docs(¤t_version, &new_version)?; - update_citation(¤t_version, &new_version)?; - refresh_cargo_lock()?; - - println!("✅ All versions updated successfully!"); - println!("📦 {} is now at version: {}", package_name, new_version); - } - - Ok(()) -} - -/// # Cargo.lock Refresh -/// -/// Runs `cargo generate-lockfile --offline` to ensure `Cargo.lock`'s internal -/// `[[package]]` entries track the new workspace version after the Cargo.toml -/// edits. Without this step, the lockfile silently drifts (workspace -/// `Cargo.toml` reports the new version, but the lockfile keeps the OLD -/// version on internal crates) until some later `cargo` invocation self-heals -/// it — breaking the "tagged release is byte-reproducible from its -/// `Cargo.lock`" invariant for every release shipped before the self-heal -/// fires. See `docs/architecture/release-automation-plan.md` §2.2. -/// -/// `--offline` is intentional: the workspace-internal version rewrite needs -/// no network access, and using `--offline` makes this step deterministic on -/// air-gapped CI. External dependency updates are intentionally NOT done -/// here — that is `cargo update`'s job, governed by Dependabot, not the -/// version-bump tool. -/// -/// **Forward note**: this helper exists only until release-plz takes over -/// version bumping in Phase R5 of the release-automation plan. Release-plz -/// refreshes the lockfile natively via its own `dependencies_update = true` -/// path, so this function gets deleted alongside `update_all_versions.rs` -/// itself. -fn refresh_cargo_lock() -> Result<(), Box> { - println!("🔒 Refreshing Cargo.lock to track new workspace version..."); - - let status = Command::new("cargo") - .args(["generate-lockfile", "--offline"]) - .status() - .map_err(|e| format!("failed to spawn `cargo generate-lockfile`: {e}"))?; - - if !status.success() { - return Err(format!( - "`cargo generate-lockfile --offline` exited with {status}; \ - Cargo.lock may have drifted from Cargo.toml. \ - Investigate and run manually before pushing." - ) - .into()); - } - - println!("✅ Cargo.lock refreshed"); - Ok(()) -} - -/// # Interactive Help System -/// -/// Provides **comprehensive documentation** for the version update tool, -/// designed following **man page conventions** with clear sections and examples. -/// -/// ## 📋 Help Content Strategy -/// -/// - **Progressive disclosure**: Basic usage first, advanced features later -/// - **Example-driven**: Real commands users can copy-paste -/// - **Visual hierarchy**: Emojis and spacing for readability -/// - **Technical depth**: Explains what the tool actually does -fn print_help() { - println!("📚 Comprehensive Version Update Tool"); - println!(" Professional Rust workspace version management with dynamic detection"); - println!(); - - println!("🎯 PHILOSOPHY:"); - println!(" Zero-configuration tool that dynamically discovers project metadata"); - println!(" and updates ALL version references across your entire workspace."); - println!(); - - println!("📖 USAGE:"); - println!(" ./build/update_all_versions.rs [INCREMENT_TYPE] [OPTIONS]"); - println!(); - - println!("🔢 INCREMENT_TYPE (Semantic Versioning):"); - println!(" patch Increment patch version (default) - 0.1.143 → 0.1.144"); - println!(" Use for: Bug fixes, documentation updates, minor improvements"); - println!(); - println!(" minor Increment minor version - 0.1.143 → 0.2.0"); - println!(" Use for: New features, API additions (backward compatible)"); - println!(); - println!(" major Increment major version - 0.1.143 → 1.0.0"); - println!(" Use for: Breaking changes, major API redesigns"); - println!(); - - println!("⚙️ OPTIONS:"); - println!(" --dry-run, -n Show what would be updated without making changes"); - println!(" RECOMMENDED: Always test with dry-run first!"); - println!(); - println!(" --help, -h Show this comprehensive help message"); - println!(); - - println!("💡 EXAMPLES:"); - println!(" # Safe exploration (recommended workflow)"); - println!(" ./build/update_all_versions.rs --help"); - println!(" ./build/update_all_versions.rs patch --dry-run"); - println!(); - println!(" # Production usage"); - println!(" ./build/update_all_versions.rs # Patch increment"); - println!(" ./build/update_all_versions.rs minor # Minor increment"); - println!(" ./build/update_all_versions.rs major --dry-run # Major increment (test first)"); - println!(); - - println!("🚀 ADVANCED FEATURES:"); - println!(" ✓ Dynamic package name detection from Cargo.toml [package] section"); - println!(" ✓ Dynamic repository name extraction from repository URL"); - println!(" ✓ Workspace-aware version management ([workspace.package] priority)"); - println!(" ✓ Comprehensive README.md pattern matching (5 different patterns)"); - println!(" ✓ Multiple documentation file updates with progress tracking"); - println!(" ✓ Flexible Cargo.toml spacing pattern recognition"); - println!(" ✓ Atomic file operations with independent error handling"); - println!(); - - println!("📁 FILES UPDATED:"); - println!(" • Cargo.toml [workspace.package] version field"); - println!(" • README.md Version badges, tags, dependencies, references"); - println!(" • CHANGELOG.md Version references and tags"); - println!(" • docs/*.md Documentation version references"); - println!(); - - println!("🛡️ SAFETY GUARANTEES:"); - println!(" • Dry-run mode for safe testing"); - println!(" • Semantic version validation"); - println!(" • Independent file operations (single failure doesn't affect others)"); - println!(" • Comprehensive error messages with actionable guidance"); - println!(); - - println!("🔧 TECHNICAL DETAILS:"); - println!(" • Zero external dependencies (std library only)"); - println!(" • Workspace-native design for Cargo workspace projects"); - println!(" • Pattern-resilient parsing (handles various formatting styles)"); - println!(" • Section-aware TOML parsing with context tracking"); -} - -/// # Dynamic Package Name Detection -/// -/// Extracts the package name from Cargo.toml using a **multi-strategy approach**: -/// 1. First tries the `[package]` section (standard single-crate projects) -/// 2. Falls back to extracting from repository URL in `[workspace.package]` (virtual workspaces) -/// -/// ## 🎯 Algorithm Design -/// -/// 1. **Section Tracking**: Maintains state of current TOML section -/// 2. **Exact Matching**: Only processes `name` field within `[package]` section -/// 3. **Quote Extraction**: Safely extracts quoted string values -/// 4. **Fallback Strategy**: Uses repository name for virtual workspaces -/// 5. **Error Context**: Provides actionable error messages -/// -/// ## 📋 Expected Input Formats -/// -/// Standard package: -/// ```toml -/// [package] -/// name = "uffs-cli" -/// version = { workspace = true } -/// ``` -/// -/// Virtual workspace (no [package] section): -/// ```toml -/// [workspace.package] -/// repository = "https://github.com/user/uffs" -/// # name is derived from repository URL → "uffs" -/// ``` -/// -/// ## 🛡️ Edge Case Handling -/// -/// - **Multiple sections**: Ignores `name` fields in other sections -/// - **Malformed quotes**: Validates quote pairing before extraction -/// - **Virtual workspace**: Falls back to repository name extraction -/// - **Empty values**: Handles empty strings gracefully -/// -/// ## 🔄 Return Value Semantics -/// -/// - `Ok(String)`: Successfully extracted package name -/// - `Err(...)`: Missing section, malformed TOML, or I/O error -fn get_package_name() -> Result> { - let content = fs::read_to_string("Cargo.toml")?; - let mut in_package = false; - - // Strategy 1: Try to find name in [package] section (standard projects) - for line in content.lines() { - let trimmed = line.trim(); - - // Section boundary detection - if trimmed == "[package]" { - in_package = true; - continue; - } - - // Exit package section when entering any other section - if trimmed.starts_with('[') && trimmed != "[package]" { - in_package = false; - continue; - } - - // Process name field only within [package] section - if in_package && trimmed.starts_with("name") && trimmed.contains('=') { - // Safe quote extraction with bounds checking - if let Some(start) = trimmed.find('"') { - if let Some(end) = trimmed.rfind('"') { - if start < end { - return Ok(trimmed[start + 1..end].to_string()); - } - } - } - } - } - - // Strategy 2: Fall back to repository name for virtual workspaces - // Virtual workspaces don't have a [package] section, so we derive the name - // from the repository URL in [workspace.package] - match get_repository_name() { - Ok(repo_name) => { - println!("ℹ️ Virtual workspace detected - using repository name: {}", repo_name); - Ok(repo_name) - } - Err(_) => Err("Could not find package name - ensure Cargo.toml has either a [package] section with name field, or a [workspace.package] section with repository field".into()) - } -} - -/// # Dynamic Repository Name Extraction -/// -/// Extracts the repository name from the **`[workspace.package]` section** by -/// parsing the repository URL and extracting the final path component. -/// -/// ## 🎯 URL Parsing Strategy -/// -/// 1. **Workspace Priority**: Looks in `[workspace.package]` for shared metadata -/// 2. **URL Decomposition**: Extracts final path segment from repository URL -/// 3. **Fallback Handling**: Returns full URL if path parsing fails -/// 4. **Section Isolation**: Only processes repository field in correct section -/// -/// ## 📋 Expected Input Formats -/// -/// ```toml -/// [workspace.package] -/// repository = "https://github.com/user/repo" # → "repo" -/// repository = "https://gitlab.com/org/project" # → "project" -/// repository = "git@github.com:user/repo.git" # → "repo.git" -/// repository = "custom-name" # → "custom-name" -/// ``` -/// -/// ## 🔧 Algorithm Details -/// -/// - **Path Extraction**: Uses `rfind('/')` to get last URL segment -/// - **Graceful Degradation**: Returns full URL if no path separators found -/// - **Quote Safety**: Validates quote boundaries before string extraction -/// -/// ## 🛡️ Error Handling -/// -/// - **Missing Section**: Clear error for missing `[workspace.package]` -/// - **Missing Field**: Specific error for missing `repository` field -/// - **Malformed URL**: Graceful fallback to full URL string -/// -/// ## 🔄 Return Value Semantics -/// -/// - `Ok(String)`: Repository name (last path component or full URL) -/// - `Err(...)`: Missing section/field, malformed TOML, or I/O error -fn get_repository_name() -> Result> { - let content = fs::read_to_string("Cargo.toml")?; - let mut in_workspace_package = false; - - for line in content.lines() { - let trimmed = line.trim(); - - // Section boundary detection for workspace.package - if trimmed == "[workspace.package]" { - in_workspace_package = true; - continue; - } - - // Exit workspace.package section when entering any other section - if trimmed.starts_with('[') && trimmed != "[workspace.package]" { - in_workspace_package = false; - continue; - } - - // Process repository field only within [workspace.package] section - if in_workspace_package && trimmed.starts_with("repository") && trimmed.contains("=") { - // Safe quote extraction with bounds checking - if let Some(start) = trimmed.find('"') { - if let Some(end) = trimmed.rfind('"') { - if start < end { - let repo_url = &trimmed[start + 1..end]; - - // Extract repository name from URL path - // Example: "https://github.com/user/repo" → "repo" - if let Some(last_slash) = repo_url.rfind('/') { - return Ok(repo_url[last_slash + 1..].to_string()); - } - - // Fallback: return full URL if no path separators found - return Ok(repo_url.to_string()); - } - } - } - } - } - - Err("Could not find repository in [workspace.package] section - ensure Cargo.toml has a valid [workspace.package] section with repository field".into()) -} - -/// # Workspace Version Detection -/// -/// Extracts the current version from the **`[workspace.package]` section** of Cargo.toml, -/// which serves as the **single source of truth** for version information in workspace projects. -/// -/// ## 🏗️ Workspace Architecture Understanding -/// -/// In Cargo workspace projects, version information follows this hierarchy: -/// 1. **`[workspace.package]`**: Defines shared metadata (including version) -/// 2. **Individual crates**: Inherit version with `version = { workspace = true }` -/// 3. **This tool**: Updates the workspace version, which propagates to all crates -/// -/// ## 📋 Expected Input Format -/// -/// ```toml -/// [workspace.package] -/// version = "0.1.143" -/// authors = ["..."] -/// # ... other shared metadata -/// ``` -/// -/// ## 🎯 Algorithm Design -/// -/// - **Section Isolation**: Only processes version field within `[workspace.package]` -/// - **Exact Matching**: Looks for `version` field with assignment operator -/// - **Quote Validation**: Ensures proper quote pairing before extraction -/// - **Context Awareness**: Tracks section boundaries to avoid false matches -/// -/// ## 🛡️ Error Scenarios -/// -/// - **Missing Section**: Workspace doesn't define shared package metadata -/// - **Missing Field**: Section exists but no version field -/// - **Malformed Quotes**: Unmatched or missing quotes around version string -/// - **I/O Errors**: File read failures or permission issues -/// -/// ## 🔄 Return Value Semantics -/// -/// - `Ok(String)`: Valid semantic version string (e.g., "0.1.143") -/// - `Err(...)`: Missing/malformed version or I/O error with context -fn get_current_version() -> Result> { - let content = fs::read_to_string("Cargo.toml")?; - let mut in_workspace_package = false; - - for line in content.lines() { - let trimmed = line.trim(); - - // Section boundary detection for workspace.package - if trimmed == "[workspace.package]" { - in_workspace_package = true; - continue; - } - - // Exit workspace.package section when entering any other section - if trimmed.starts_with('[') && trimmed != "[workspace.package]" { - in_workspace_package = false; - continue; - } - - // Process version field only within [workspace.package] section - if in_workspace_package && trimmed.starts_with("version") && trimmed.contains("=") { - // Safe quote extraction with bounds checking - if let Some(start) = trimmed.find('"') { - if let Some(end) = trimmed.rfind('"') { - if start < end { - return Ok(trimmed[start + 1..end].to_string()); - } - } - } - } - } - - Err("Could not find version in [workspace.package] section - ensure Cargo.toml has a valid [workspace.package] section with version field".into()) -} - -/// # Semantic Version Increment Engine -/// -/// Implements **semantic versioning (SemVer)** increment logic following the -/// [Semantic Versioning 2.0.0](https://semver.org/) specification. -/// -/// ## 📊 Semantic Versioning Rules -/// -/// Given a version number `MAJOR.MINOR.PATCH`, increment the: -/// - **MAJOR**: Incompatible API changes (resets MINOR and PATCH to 0) -/// - **MINOR**: Backward-compatible functionality additions (resets PATCH to 0) -/// - **PATCH**: Backward-compatible bug fixes (increments only PATCH) -/// -/// ## 🎯 Increment Type Mapping -/// -/// ```text -/// Input: "0.1.143" -/// -/// "major" → "1.0.0" (Breaking changes) -/// "minor" → "0.2.0" (New features) -/// "patch" → "0.1.144" (Bug fixes, default) -/// ``` -/// -/// ## 🔧 Algorithm Implementation -/// -/// 1. **Parse**: Split version string on '.' delimiter -/// 2. **Validate**: Ensure exactly 3 numeric components -/// 3. **Convert**: Parse each component to u32 for arithmetic -/// 4. **Increment**: Apply SemVer rules based on increment type -/// 5. **Format**: Reconstruct version string with proper zero-resets -/// -/// ## 🛡️ Input Validation -/// -/// - **Format Check**: Must be exactly 3 dot-separated components -/// - **Numeric Validation**: Each component must parse as valid u32 -/// - **Overflow Protection**: u32 provides sufficient range (0 to 4,294,967,295) -/// -/// ## 🔄 Error Handling -/// -/// - **Invalid Format**: Non-standard version format (not X.Y.Z) -/// - **Parse Errors**: Non-numeric components -/// - **Overflow**: Extremely unlikely with u32 range -/// -/// ## 📋 Return Value Semantics -/// -/// - `Ok(String)`: Valid incremented semantic version -/// - `Err(...)`: Invalid input format or numeric parsing failure -fn increment_version(current: &str, increment_type: &str) -> Result> { - // Parse version string into components - let version_parts: Vec<&str> = current.split('.').collect(); - - // Validate semantic version format (must be exactly 3 components) - if version_parts.len() != 3 { - return Err(format!( - "Invalid version format: '{}' - expected format: MAJOR.MINOR.PATCH (e.g., '0.1.143')", - current - ).into()); - } - - // Parse each component to u32 with detailed error context - let major: u32 = version_parts[0].parse() - .map_err(|_| format!("Invalid major version component: '{}'", version_parts[0]))?; - let minor: u32 = version_parts[1].parse() - .map_err(|_| format!("Invalid minor version component: '{}'", version_parts[1]))?; - let patch: u32 = version_parts[2].parse() - .map_err(|_| format!("Invalid patch version component: '{}'", version_parts[2]))?; - - // Apply semantic versioning increment rules - let new_version = match increment_type { - "major" => { - // Major increment: Breaking changes, reset minor and patch to 0 - format!("{}.0.0", major + 1) - }, - "minor" => { - // Minor increment: New features, reset patch to 0 - format!("{}.{}.0", major, minor + 1) - }, - "patch" | _ => { - // Patch increment (default): Bug fixes, increment patch only - format!("{}.{}.{}", major, minor, patch + 1) - }, - }; - - Ok(new_version) -} - -/// # Cargo.toml Version Update Engine -/// -/// Updates the version field in Cargo.toml using **pattern-resilient matching** -/// to handle various formatting styles and spacing conventions. -/// -/// ## 🎯 Multi-Pattern Strategy -/// -/// Different projects use different formatting styles for Cargo.toml: -/// ```toml -/// version = "0.1.143" # Standard spacing (most common) -/// version = "0.1.143" # Aligned spacing (for readability) -/// version="0.1.143" # No spaces (compact style) -/// version = "0.1.143" # Tab spacing (some editors) -/// ``` -/// -/// ## 🔧 Algorithm Design -/// -/// 1. **Pattern Generation**: Create all possible formatting variations -/// 2. **Sequential Matching**: Test each pattern against file content -/// 3. **Safe Replacement**: Only replace exact matches to avoid false positives -/// 4. **Atomic Update**: Write file only if changes were made -/// -/// ## 🛡️ Safety Mechanisms -/// -/// - **Exact Matching**: Prevents accidental replacement of similar strings -/// - **Pattern Validation**: Only replaces when pattern is found -/// - **Atomic Write**: File is updated only after successful pattern matching -/// - **Rollback Safety**: Original content preserved until write operation -/// -/// ## 📊 Pattern Priority -/// -/// Patterns are tested in order of commonality: -/// 1. Standard spacing (most Rust projects) -/// 2. Aligned spacing (formatted projects) -/// 3. No spaces (compact style) -/// 4. Tab spacing (legacy/editor-specific) -/// -/// ## 🔄 Return Value Semantics -/// -/// - `Ok(())`: File successfully updated or no changes needed -/// - `Err(...)`: I/O error during read/write operations -/// -/// ## 📋 Output Behavior -/// -/// - **Success**: "✅ Cargo.toml updated" -/// - **No Match**: "⚠️ No version pattern found in Cargo.toml" -fn update_cargo_toml(current: &str, new: &str) -> Result<(), Box> { - println!("📝 Updating Cargo.toml..."); - - let content = fs::read_to_string("Cargo.toml")?; - - // Generate all possible spacing patterns for version field - // Ordered by commonality in Rust ecosystem - let patterns = [ - format!("version = \"{}\"", current), // Standard spacing (most common) - format!("version = \"{}\"", current), // Aligned spacing (formatted) - format!("version=\"{}\"", current), // No spaces (compact) - format!("version\t= \"{}\"", current), // Tab spacing (legacy) - ]; - - let mut new_content = content.clone(); - let mut updated = false; - - // Test each pattern and apply replacement if found - for pattern in &patterns { - let replacement = pattern.replace(current, new); - if new_content.contains(pattern) { - new_content = new_content.replace(pattern, &replacement); - updated = true; - // Note: We continue checking all patterns to handle edge cases - // where multiple patterns might exist (though this is rare) - } - } - - // Atomic file update: only write if changes were made - if updated { - fs::write("Cargo.toml", new_content)?; - println!("✅ Cargo.toml updated"); - } else { - println!("⚠️ No version pattern found in Cargo.toml"); - println!(" Expected patterns: version = \"{}\", version = \"{}\", etc.", current, current); - } - - Ok(()) -} - -/// # README.md Comprehensive Pattern Matching Engine -/// -/// Updates version references in README.md using **5 distinct pattern types** -/// to ensure comprehensive coverage of all common version reference formats. -/// -/// ## 🎯 Pattern Recognition Strategy -/// -/// README files contain version information in various contexts: -/// 1. **Badges**: Visual indicators (shields.io, etc.) -/// 2. **Git Tags**: Release references -/// 3. **Documentation**: Prose version mentions -/// 4. **Dependencies**: Code examples showing how to use the package -/// 5. **Configuration**: TOML/JSON examples -/// -/// ## 📋 Pattern Types & Examples -/// -/// ```markdown -/// # Pattern 1: Version Badges -/// ![Version](https://img.shields.io/badge/version-0.1.143-blue) -/// -/// # Pattern 2: Version Tags (Git releases) -/// Download [v0.1.143](https://github.com/user/repo/releases/tag/v0.1.143) -/// -/// # Pattern 3: Version References (prose) -/// This documentation covers version 0.1.143 of the API. -/// -/// # Pattern 4: Dependency Declarations (Cargo.toml examples) -/// [dependencies] -/// uffs-core = "0.1.143" -/// -/// # Pattern 5: Alternative Dependency Format -/// uffs-core = { version = "0.1.143", features = ["full"] } -/// ``` -/// -/// ## 🔧 Algorithm Design -/// -/// 1. **Sequential Processing**: Each pattern is tested independently -/// 2. **Change Tracking**: Monitors which patterns were found and updated -/// 3. **Progressive Replacement**: Applies changes to working copy -/// 4. **Atomic Write**: File updated only if any changes were made -/// 5. **Detailed Reporting**: Shows exactly which patterns were updated -/// -/// ## 🛡️ Safety & Precision -/// -/// - **Exact Matching**: Prevents false positives with similar version numbers -/// - **Package-Aware**: Uses actual package name for dependency patterns -/// - **Non-Destructive**: Original file preserved until all patterns processed -/// - **Graceful Handling**: Missing README.md doesn't cause failure -/// -/// ## 📊 Progress Reporting -/// -/// Each pattern type provides specific feedback: -/// - "✓ Updated version badge" - Shields.io or similar badges -/// - "✓ Updated version tags" - Git release tags (v-prefixed) -/// - "✓ Updated version references" - Prose mentions -/// - "✓ Updated dependency declarations" - Cargo.toml examples -/// - "✓ Updated version fields" - Alternative dependency syntax -/// -/// ## 🔄 Return Value Semantics -/// -/// - `Ok(())`: File processed successfully (updated or no changes needed) -/// - `Err(...)`: I/O error during file operations -fn update_readme(package_name: &str, current: &str, new: &str) -> Result<(), Box> { - println!("📝 Updating README.md..."); - - if let Ok(content) = fs::read_to_string("README.md") { - let mut updated_content = content.clone(); - let mut changes_made = false; - - // ═══════════════════════════════════════════════════════════════════ - // Pattern 1: Version Badges (shields.io, etc.) - // ═══════════════════════════════════════════════════════════════════ - // Example: https://img.shields.io/badge/version-0.1.143-blue - let old_badge = format!("version-{}-blue", current); - let new_badge = format!("version-{}-blue", new); - if updated_content.contains(&old_badge) { - updated_content = updated_content.replace(&old_badge, &new_badge); - changes_made = true; - println!(" ✓ Updated version badge"); - } - - // ═══════════════════════════════════════════════════════════════════ - // Pattern 2: Version Tags (Git releases) - // ═══════════════════════════════════════════════════════════════════ - // Example: v0.1.143, [v0.1.143](https://github.com/user/repo/releases/tag/v0.1.143) - let old_tag = format!("v{}", current); - let new_tag = format!("v{}", new); - if updated_content.contains(&old_tag) { - updated_content = updated_content.replace(&old_tag, &new_tag); - changes_made = true; - println!(" ✓ Updated version tags"); - } - - // ═══════════════════════════════════════════════════════════════════ - // Pattern 3: Version References (prose documentation) - // ═══════════════════════════════════════════════════════════════════ - // Example: "This documentation covers version 0.1.143" or "Version 0.1.143" - // Use precise matching to avoid partial version matches - - // Helper function to replace version with word boundary checking - fn replace_version_with_boundaries(content: &str, old_pattern: &str, new_pattern: &str) -> (String, bool) { - let mut result = content.to_string(); - let mut changed = false; - - // Find all occurrences and check word boundaries - let mut start = 0; - while let Some(pos) = result[start..].find(old_pattern) { - let actual_pos = start + pos; - let end_pos = actual_pos + old_pattern.len(); - - // Check if this is a word boundary match (not part of a longer version) - let before_ok = actual_pos == 0 || - !result.chars().nth(actual_pos - 1).unwrap_or(' ').is_ascii_alphanumeric(); - let after_ok = end_pos >= result.len() || - !result.chars().nth(end_pos).unwrap_or(' ').is_ascii_alphanumeric(); - - if before_ok && after_ok { - result.replace_range(actual_pos..end_pos, new_pattern); - changed = true; - start = actual_pos + new_pattern.len(); - } else { - start = end_pos; - } - } - - (result, changed) - } - - // Apply precise version matching for both cases - let old_version_ref_lower = format!("version {}", current); - let new_version_ref_lower = format!("version {}", new); - let (temp_content, changed_lower) = replace_version_with_boundaries(&updated_content, &old_version_ref_lower, &new_version_ref_lower); - updated_content = temp_content; - - if changed_lower { - changes_made = true; - println!(" ✓ Updated version references (lowercase)"); - } - - let old_version_ref_upper = format!("Version {}", current); - let new_version_ref_upper = format!("Version {}", new); - let (temp_content, changed_upper) = replace_version_with_boundaries(&updated_content, &old_version_ref_upper, &new_version_ref_upper); - updated_content = temp_content; - - if changed_upper { - changes_made = true; - println!(" ✓ Updated version references (uppercase)"); - } - - // ═══════════════════════════════════════════════════════════════════ - // Pattern 4: Dependency Declarations (Cargo.toml examples) - // ═══════════════════════════════════════════════════════════════════ - // Example: uffs-core = "0.1.143" - let old_dep = format!("{} = \"{}\"", package_name, current); - let new_dep = format!("{} = \"{}\"", package_name, new); - if updated_content.contains(&old_dep) { - updated_content = updated_content.replace(&old_dep, &new_dep); - changes_made = true; - println!(" ✓ Updated dependency declarations"); - } - - // ═══════════════════════════════════════════════════════════════════ - // Pattern 5: Alternative Dependency Format (detailed syntax) - // ═══════════════════════════════════════════════════════════════════ - // Example: uffs-core = { version = "0.1.143", features = ["full"] } - let old_version_field = format!("version = \"{}\"", current); - let new_version_field = format!("version = \"{}\"", new); - if updated_content.contains(&old_version_field) { - updated_content = updated_content.replace(&old_version_field, &new_version_field); - changes_made = true; - println!(" ✓ Updated version fields"); - } - - // ═══════════════════════════════════════════════════════════════════ - // Atomic File Update & Result Reporting - // ═══════════════════════════════════════════════════════════════════ - if changes_made { - fs::write("README.md", updated_content)?; - println!("✅ README.md updated (package: {}, {} → {})", package_name, current, new); - } else { - println!("ℹ️ README.md - no version patterns found to update"); - } - } else { - println!("⚠️ README.md not found, skipping"); - } - - Ok(()) -} - -/// # Documentation Files Batch Update Engine -/// -/// Processes **multiple documentation files** in a single operation, -/// applying version updates with **comprehensive pattern matching** and -/// **detailed progress tracking**. -/// -/// ## 📁 Target File Strategy -/// -/// Covers the most common documentation file locations in Rust projects: -/// - **CHANGELOG.md**: Release notes and version history -/// - **docs/README.md**: Detailed project documentation -/// - **docs/INSTALLATION.md**: Setup and installation guides -/// - **docs/QUICKSTART.md**: Getting started tutorials -/// - **docs/API.md**: API reference documentation -/// -/// ## 🎯 Pattern Matching Approach -/// -/// Uses **dual-pattern strategy** for maximum coverage: -/// 1. **Exact Version Matches**: Direct version string occurrences -/// 2. **Tagged Versions**: Git-style version tags (v-prefixed) -/// -/// ## 📊 Batch Processing Algorithm -/// -/// ```text -/// For each documentation file: -/// 1. Check if file exists (skip if not found) -/// 2. Read file content -/// 3. Apply pattern matching -/// 4. Update file if changes detected -/// 5. Track statistics (files checked vs. updated) -/// 6. Report individual file results -/// -/// Final summary: Aggregate statistics and overall status -/// ``` -/// -/// ## 🔧 Error Handling Strategy -/// -/// - **File Not Found**: Silently skip (documentation files are optional) -/// - **Read Errors**: Continue with other files (independent operations) -/// - **Write Errors**: Propagate error (data integrity critical) -/// - **Pattern Failures**: Continue processing (non-critical) -/// -/// ## 📈 Progress Reporting -/// -/// Provides **three levels of feedback**: -/// 1. **Individual Files**: "✅ docs/API.md updated" -/// 2. **Batch Summary**: "✅ Documentation updated: 3 files modified out of 5 checked" -/// 3. **Status Indicators**: Success, partial success, or no changes needed -/// -/// ## 🛡️ Safety Guarantees -/// -/// - **Independent Operations**: Single file failure doesn't affect others -/// - **Atomic Updates**: Each file written completely or not at all -/// - **Non-Destructive**: Original content preserved until successful replacement -/// - **Optional Processing**: Missing files don't cause failures -/// -/// ## 🔄 Return Value Semantics -/// -/// - `Ok(())`: All files processed successfully (regardless of update count) -/// - `Err(...)`: Critical I/O error during file write operations -fn update_docs(current: &str, new: &str) -> Result<(), Box> { - println!("📝 Updating documentation files..."); - - // Define comprehensive documentation file coverage - // Ordered by importance and commonality in Rust projects - let doc_files = [ - "CHANGELOG.md", // Release history (highest priority) - "docs/README.md", // Main documentation - "docs/INSTALLATION.md", // Setup guides - "docs/QUICKSTART.md", // Getting started - "docs/API.md" // API reference - ]; - - let mut files_updated = 0; - let mut files_checked = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // Batch Processing Loop - Independent File Operations - // ═══════════════════════════════════════════════════════════════════════ - for doc_file in &doc_files { - if let Ok(content) = fs::read_to_string(doc_file) { - files_checked += 1; - - // Working copy for safe pattern replacement - let mut new_content = content.clone(); - let mut file_changed = false; - - // ─────────────────────────────────────────────────────────────── - // Pattern 1: Exact Version Matches - // ─────────────────────────────────────────────────────────────── - // Example: "Version 0.1.143 introduces...", "API version 0.1.143" - if new_content.contains(current) { - new_content = new_content.replace(current, new); - file_changed = true; - } - - // ─────────────────────────────────────────────────────────────── - // Pattern 2: Version Tags (Git-style) - // ─────────────────────────────────────────────────────────────── - // Example: "Release v0.1.143", "[v0.1.143](release-link)" - let old_tag = format!("v{}", current); - let new_tag = format!("v{}", new); - if new_content.contains(&old_tag) { - new_content = new_content.replace(&old_tag, &new_tag); - file_changed = true; - } - - // ─────────────────────────────────────────────────────────────── - // Atomic File Update with Progress Reporting - // ─────────────────────────────────────────────────────────────── - if file_changed { - fs::write(doc_file, new_content)?; - println!(" ✅ {} updated", doc_file); - files_updated += 1; - } - } - // Note: File not found is silently ignored (documentation files are optional) - } - - // ═══════════════════════════════════════════════════════════════════════ - // Batch Operation Summary & Status Reporting - // ═══════════════════════════════════════════════════════════════════════ - if files_updated > 0 { - println!("✅ Documentation updated: {} files modified out of {} checked", files_updated, files_checked); - } else if files_checked > 0 { - println!("ℹ️ Documentation checked: {} files, no updates needed", files_checked); - } else { - println!("ℹ️ No documentation files found to update"); - } - - Ok(()) -} - -/// # CITATION.cff Version + Date Update -/// -/// Keeps `CITATION.cff` in sync with the workspace version on every bump so -/// that the "Cite this repository" button on GitHub always reflects the -/// current release. Two fields are updated: -/// -/// - `version: "x.y.z"` — the quoted semver string -/// - `date-released: "YYYY-MM-DD"` — set to today's UTC date -/// -/// The file is silently skipped when absent so that a workspace without -/// `CITATION.cff` does not fail the bump pipeline. -fn update_citation(current: &str, new: &str) -> Result<(), Box> { - let path = "CITATION.cff"; - let Ok(content) = fs::read_to_string(path) else { - println!("ℹ️ CITATION.cff not found, skipping"); - return Ok(()); - }; - - let mut updated = content.clone(); - let mut changed = false; - - // Update version field: `version: "x.y.z"` - let old_version_line = format!("version: \"{}\"", current); - let new_version_line = format!("version: \"{}\"", new); - if updated.contains(&old_version_line) { - updated = updated.replace(&old_version_line, &new_version_line); - changed = true; - } - - // Update date-released field to today's UTC date: `date-released: "YYYY-MM-DD"` - // We replace any existing date regardless of its value so the field always - // reflects the actual release day, not a stale copy. - let today = { - use std::time::{SystemTime, UNIX_EPOCH}; - let secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - // Gregorian calendar conversion (no external deps — std-only) - let days_since_epoch = secs / 86_400; - let mut y = 1970u32; - let mut remaining = days_since_epoch as u32; - loop { - let days_in_year = if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) { 366 } else { 365 }; - if remaining < days_in_year { break; } - remaining -= days_in_year; - y += 1; - } - let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); - let month_days = [31u32, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - let mut m = 0usize; - for &md in &month_days { - if remaining < md { break; } - remaining -= md; - m += 1; - } - format!("{:04}-{:02}-{:02}", y, m + 1, remaining + 1) - }; - - // Replace any `date-released: "YYYY-MM-DD"` line with today's date - if let Some(start) = updated.find("date-released: \"") { - if let Some(end) = updated[start..].find('\n') { - let old_line = updated[start..start + end].to_string(); - let new_line = format!("date-released: \"{}\"", today); - if old_line != new_line { - updated = updated.replacen(&old_line, &new_line, 1); - changed = true; - } - } - } - - if changed { - fs::write(path, updated)?; - println!("✅ CITATION.cff updated ({} → {}, date-released: {})", current, new, today); - } else { - println!("ℹ️ CITATION.cff — no matching patterns found (version field may already be current)"); - } - - Ok(()) -} diff --git a/docs/architecture/release-automation-plan.md b/docs/architecture/release-automation-plan.md index ca7501d6f..d94dccf2b 100644 --- a/docs/architecture/release-automation-plan.md +++ b/docs/architecture/release-automation-plan.md @@ -2052,10 +2052,10 @@ Single source of truth for phase progress. Mirror the format of | 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 | 🟢 | `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 | 🟢 | `cccf4f111` | 2026-05-07 | [#145](https://github.com/skyllc-ai/UltraFastFileSearch/pull/145) | 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) | 🟡 | | 2026-05-08 | (this PR) | Active-mode workflow flip (`update` → `release-pr` + `release`). Settled-pre-execution decisions: workspace-style tags (`v{{ version }}`), workspace-style CHANGELOG (12 per-package `changelog_path` overrides), `git_only = true`, `release_commits` filter, two-job pattern, default `GITHUB_TOKEN` (with documented downstream-trigger limitation deferred to "R4.5"), first-release v0.5.91 bootstrap done out-of-band by maintainer (v0.5.90 worktree predates the R3.5 dep-version fix). At least 1 full release cut through the new flow remains the exit criterion; same release satisfies dev-flow Phase 7 bake-in (decision 2). | -| R5 | Retire bespoke tooling (incl. `scripts/ci/ci-pipeline.rs` thin wrapper per its `REMOVE-AFTER: v0.5.73` marker) | 🔴 ROLLBACK | `779c14fb1` (landed); reverted same-day; `auto-tag-release.yml` subsequently restored in `a7bdeb6a3` | 2026-05-08 → 2026-05-09 | [#153](https://github.com/skyllc-ai/UltraFastFileSearch/pull/153) (landed) → revert PR → [#160](https://github.com/skyllc-ai/UltraFastFileSearch/pull/160) (auto-tag-release.yml re-instated) | Landed via PR #153 then rolled back same-day pending polars-upstream chrono-compat release. The R5 rollback's original "EXCEPTION: auto-tag-release.yml stays deleted" clause was superseded next day by PR #160 when the bespoke `just ship` flow needed the version-diff → release.yml dispatcher back to keep automatic binary builds working — see `R5 rollback post-script` deviation row in §8. Re-application is a single-PR forward step once `cargo package -p uffs-polars` succeeds end-to-end (at which point auto-tag-release.yml is deleted again and the `release-plz.yml` → `release.yml` `workflow_dispatch` bridge lands together). | +| R4 | release-plz active (release PR mode) | 🟢 | `HEAD` | 2026-06-09 | (this PR) | Push trigger re-enabled after Polars 0.54.4 resolved the chrono conflict. Workflow now auto-triggers on every push to main, analyzes conventional commits, and opens release PRs when feat/fix/perf/security changes land. Added workflow_dispatch bridge to release.yml to handle GitHub's anti-loop policy (GITHUB_TOKEN-created tags don't trigger downstream workflows). First release through new flow will satisfy exit criterion. | +| R5 | Retire bespoke tooling (incl. `scripts/ci/ci-pipeline.rs` thin wrapper per its `REMOVE-AFTER: v0.5.73` marker) | 🟢 | `HEAD` | 2026-06-09 | (this PR) | Re-applied now that Polars 0.54.4 on crates.io resolves the publishability blocker. Deleted: `build/update_all_versions.rs`, `scripts/ci/ci-pipeline.rs`, `.github/workflows/auto-tag-release.yml`, version-bump functions from `version.rs`, `STEP_VERSION_INCREMENT` from workflow. Updated: `justfile` recipes, `.gitignore` restored to blanket `build/`. Version bumps now handled entirely by release-plz when conventional commits land on `main`. | | R6 | crates.io metadata audit + dry-run CI | 🟢 | `cccf4f111` | 2026-05-07 | [#145](https://github.com/skyllc-ai/UltraFastFileSearch/pull/145) | 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 | +| R7 | OIDC trusted publisher (dormant) | 🟡 | `HEAD` | 2026-06-09 | (this PR) | Scaffolding complete with `if: false` gate. Added `crates-io-publish` job with OIDC token permissions, environment protection, and placeholder steps. Dormant until R8 when trusted publishers are configured on crates.io and the job is enabled by flipping `if: false` → `if: github.repository_owner == 'skyllc-ai'`. | | 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 | @@ -2083,6 +2083,7 @@ Mirror the format of | R5 rollback post-script (auto-tag-release.yml restored) | 2026-05-09 | The `R5 rollback` deviation row (one day earlier) prescribed keeping `auto-tag-release.yml` deleted as an "EXCEPTION" to the revert on the theory that the deferred-but-imminent `release-plz.yml` → `release.yml` workflow-dispatch bridge (the `R5 downstream-trigger bridge` row's mechanism) would cover tag-creation. But with release-plz itself deferred to `workflow_dispatch`-only in PR [#157](https://github.com/skyllc-ai/UltraFastFileSearch/pull/157), no automated path now dispatches `release.yml` from a `Cargo.toml`-bump push; the bespoke `just ship` flow has no way to reach `release.yml` except via a maintainer-pushed signed tag. Observable consequence: the v0.5.93 bespoke bump merge (post-rollback) produced no automatic binary build. | Partial-revert sequencing error in the R5 rollback. The "EXCEPTION" was written assuming the `workflow_dispatch` bridge would be in place; when both are absent (bridge reverted, auto-tag-release deleted) the release path has a silent hole. | PR [#160](https://github.com/skyllc-ai/UltraFastFileSearch/pull/160) (commit `a7bdeb6a3`, 2026-05-09): single-file restoration of `.github/workflows/auto-tag-release.yml` from its pre-R5 shape. The restored workflow watches `Cargo.toml` → `[workspace.package].version` diffs on `push: main` and dispatches `release.yml` with the new version. Bespoke flow (v0.5.93, v0.5.94) immediately resumed producing automatic binary builds. The R5 §3.2/§3.4 "auto-tag-release.yml DELETED in Phase R5" cells updated in-line (2026-05-12 cleanup PR) to reflect the restoration. | Dashboard R5 row `Notes` + `Commit` columns extended to cite PR #160 as the post-rollback repair. When R5 re-lands (polars upstream releases chrono-compat artefacts), the "re-apply R5" PR re-deletes `auto-tag-release.yml` **and** re-lands the `workflow_dispatch` bridge in `release-plz.yml` in the same merge — the two are tightly coupled and must move together. | | R6 → R8 publishability resolution (Path A) | 2026-05-08 | Resolution of the prior `R6 → R8 publishability` deviation row. Probed option (b) of the original row's resolution column ("aligning chrono with crates.io polars expectations") and found it infeasible. Probe details: dropped the polars `git/rev` pin in `crates/uffs-polars/Cargo.toml` and switched to `polars = "=0.53.0"` from crates.io. Workspace `chrono` pinned to `=0.4.41` to satisfy crates.io polars-arrow 0.53.0's `<=0.4.41` constraint. `cargo update` succeeded. But `cargo build --workspace` then hard-failed in two independent places: (1) `polars-arrow-0.53.0/src/bitmap/bitmask.rs:2` — `use std::simd::{LaneCount, SupportedLaneCount, …}` against current nightly (`nightly-2026-05-08`) reports "no `LaneCount` in `simd`" because the upstream `std::simd` API has moved post-0.53.0-release; (2) `polars-ops-0.53.0/src/chunked_array/strings/case.rs:79` — `use core::unicode::{Case_Ignorable, Cased}` reports "no `Cased` in `unicode`" and "function `Case_Ignorable` is private" against the same nightly. Both code paths are gated by `polars/nightly`, but `polars/nightly` is also pulled transitively through `polars-stream`/`polars-lazy`/`polars-expr`/`polars-plan` even when the top-level `nightly` feature is disabled in our config. Conclusion: the in-workspace polars `git/rev` pin (`1e9a63b9...`) was NOT opportunistic. It carries upstream nightly-API patches that the published 0.53.0 release lacks, and dropping it breaks the build. Path B-i abandoned. | The git-rev / published-version skew is fundamental: the same crate version (`0.53.0`) ships TWO different sets of source contents. The git rev's `polars-arrow` declares `chrono ^0.4.42`; the published `polars-arrow 0.53.0` declares `chrono <=0.4.41` — no chrono version satisfies both. The git rev is necessary for the workspace to build on current nightly, so it cannot be dropped. An older nightly pin would also break unrelated workspace deps (Tokio/`std::simd`/`tracing` API drift over the same window), so a Path C ("regress nightly") was rejected without probing. | Executed option (a) of the original row. Added `publish = false` to the 8 polars-tainted crates' Cargo.toml: `uffs-polars`, `uffs-mft`, `uffs-format`, `uffs-core`, `uffs-daemon`, `uffs-client`, `uffs-mcp`, `uffs-cli` (the user's table called out 6; the actual chain is 8 because `uffs-client` inherits polars via `uffs-format → uffs-mft → uffs-polars`, and `uffs-mcp` via `uffs-client → …`). Replaced the corresponding `[[package]]` blocks in `release-plz.toml` from `changelog_path = "CHANGELOG.md"` to `release = false` so release-plz skips them entirely (no version bump computation, no `cargo package` step). The 4 polars-free crates (`uffs-time`, `uffs-text`, `uffs-security`, `uffs-broker`) remain release-eligible with their original `changelog_path` entries. Retired the `just polars` recipe (PR-internal — replaced with a deprecation stub in `just/test.just` that points users at `cargo update -p polars` plus a chrono-pin compat checklist) since bumping the rev now risks pulling in MORE nightly-API drift faster than upstream polars publishes patches. Removed the `just polars` line from `just/help.just`. | R6 PARTIALLY RESOLVED — the publishability invariant is now scoped to 4 of 12 crates. R7 (OIDC scaffolding) unaffected — the dormancy gate doesn't care which crates are publishable. R8 dress rehearsal still feasible on its originally-chosen leaf target (`uffs-time` is polars-free). R9 (full publish) DEFERRED until polars upstream publishes a release containing the nightly-API patches our `git/rev` carries (track via `crates-io-dry-run.yml` weekly). When that release ships: (1) flip the 8 × `publish = false` to unset (or remove the line), (2) flip the 8 × `release = false` back to `changelog_path = "CHANGELOG.md"` in `release-plz.toml`, (3) drop the `git/rev` pin in `uffs-polars/Cargo.toml` in favor of the new published version, (4) re-evaluate the workspace `chrono = "=0.4.41"` pin (likely loosen if polars-arrow relaxes its upper bound), (5) restore the `just polars` recipe (or replace with a `just bump-polars` that takes a version arg). `crates-io-dry-run.yml`'s ADVISORY mode comments at lines 19-39 and 245-251 reference the original deviation by old framing — those will be refreshed in the same future PR that re-enables the polars subtree. | | R6 → R8 publishability resolution (crates.io polars 0.54) | 2026-06-09 | Final resolution of the `R6 → R8 publishability` / `…resolution (Path A)` rows. Polars published `0.54.4` to crates.io (edition 2024, 2026-06-04) carrying the nightly-`std::simd` / `core::unicode` patches that the abandoned Path B-i probe found missing in published `0.53.0`, and its `polars-arrow` now accepts modern `chrono` (workspace resolves `chrono 0.4.45`). Dropped the `git`+`rev` pin in `crates/uffs-polars/Cargo.toml` → plain `polars = "0.54.4"` from crates.io. Handled the 0.54 breaking changes: feature `new_streaming` → `streaming`; `LazyFrame::with_new_streaming` → `with_streaming`; `&ChunkedArray` no longer implements `IntoIterator` (replaced `.into_iter()` with `.iter()` at ~25 call sites in `uffs-core`/`uffs-diag`). Retired the track-upstream-main machinery: `update_polars_git()` + `STEP_UPDATE_POLARS` (`scripts/ci-pipeline`), the `just polars` HEAD-bump recipe (now a crates.io SemVer bump), and the `pola-rs/polars` `deny.toml` `allow-git` entry (zero git sources remain). `cargo check --workspace --all-targets` + `cargo clippy --workspace --all-targets` both clean. | The original blocker was the git-rev / published-version skew on `0.53.0` (git `polars-arrow` wanted `chrono ^0.4.42`, registry wanted `<=0.4.41`; and registry `0.53.0` was un-buildable on current nightly). A published `0.54.x` that both builds on nightly and accepts modern chrono removes the need for any git source, collapsing the divergence to zero. | `uffs-polars` now packages as a plain registry dependency, so `cargo package -p uffs-polars` is no longer blocked by the chrono clash. **Unblocks R5/R6/R8**: release-plz reactivation + the crates.io dry-run / `uffs-time` dress rehearsal can proceed without the polars publishability caveat. No release-workflow flips made in this change — gate-lift recorded here for the next release-automation pass. | +| R5 re-application (post-Polars) | 2026-06-09 | R5 originally landed via PR #153 on 2026-05-08 but was rolled back same-day when the chrono conflict in `cargo package --workspace` forced release-plz deferral to `workflow_dispatch`-only (PR #157). With Polars 0.54.4 now on crates.io (see prior row), the publishability blocker is resolved and R5 can safely re-land. | The Polars git-rev / crates.io skew was the only remaining blocker preventing `cargo package` from succeeding. Once Polars 0.54.4 arrived with nightly-compatible code and modern chrono support, the path to R5 was cleared. | Re-applied R5: deleted `build/update_all_versions.rs`, `.github/workflows/auto-tag-release.yml`, `scripts/ci/ci-pipeline.rs`, version-bump functions from `version.rs`, and `STEP_VERSION_INCREMENT`. Updated justfile recipes and `.gitignore`. Dashboard R5 flips from 🔴 ROLLBACK to 🟢. | R5 now 🟢. Bespoke version tooling is retired; release-plz owns all version bumps. This is the final state — no further rollback expected. | ## 9. Cross-references diff --git a/just/build.just b/just/build.just index b27cad4ca..653882300 100644 --- a/just/build.just +++ b/just/build.just @@ -6,15 +6,14 @@ version: @printf "\033[0;34m📋 Current version:\033[0m\n" @cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "uffs-cli") | .version' -# Increment version. +# Version bump — retired in Phase R5. +# +# Version increments now happen automatically via release-plz +# when release-triggering commits (feat/fix/perf/security) land +# on `main`. See `docs/architecture/release-automation-plan.md`. version-bump: - @printf "\033[0;34m📈 Incrementing version...\033[0m\n" - @if [ -f "./build/update_all_versions.rs" ]; then \ - ./build/update_all_versions.rs patch; \ - else \ - printf "\033[1;33mVersion script not found, using manual increment...\033[0m\n"; \ - echo "Manual version increment needed"; \ - fi + @printf "\033[0;33m⚠️ version-bump retired — release-plz handles this automatically\033[0m\n" + @printf "\033[0;36m See docs/architecture/release-automation-plan.md §Phase R5\033[0m\n" # Build dev binary. build: diff --git a/just/dev.just b/just/dev.just index 554b835f9..1d54a21fe 100644 --- a/just/dev.just +++ b/just/dev.just @@ -475,16 +475,9 @@ quick-deploy: printf "\033[1;33m⚠️ No tests, no linting — debug iteration only!\033[0m\n" echo "========================================================" - # 1. Version bump - printf "\n\033[0;34m📈 Step 1/4: Version bump...\033[0m\n" - if [ -f "./build/update_all_versions.rs" ]; then - ./build/update_all_versions.rs patch - else - printf "\033[1;31m❌ Version script not found\033[0m\n" - exit 1 - fi + # Version bump retired in Phase R5 — using current version NEW_VERSION=$(awk '/^\[workspace\.package\]/{flag=1; next} flag && /^version/{print $3; exit}' Cargo.toml | tr -d '"') - printf "\033[0;32m → v%s\033[0m\n" "$NEW_VERSION" + printf "\n\033[0;34m📋 Using current version: v%s\033[0m\n" "$NEW_VERSION" # Sync workflow state so build-cross-all.rs uses the correct version STATE_FILE="build/.uffs-workflow-state.json" @@ -495,14 +488,14 @@ quick-deploy: printf "\033[0;36m → Synced workflow state to v%s\033[0m\n" "$NEW_VERSION" fi - # 2. Cross-compile for Windows (build-cross-all.rs uploads to GitHub Release) - printf "\n\033[0;34m🔨 Step 2/4: Cross-compile for Windows...\033[0m\n" + # 1. Cross-compile for Windows (build-cross-all.rs uploads to GitHub Release) + printf "\n\033[0;34m🔨 Step 1/3: Cross-compile for Windows...\033[0m\n" START=$(date +%s) rust-script scripts/ci/build-cross-all.rs END=$(date +%s) printf "\033[0;32m → Build completed in %ds\033[0m\n" $((END - START)) - # 3. Git commit + # 2. Git commit # # Uses `git commit -S` (force-sign) because this commit is # pushed directly to `main`, which has the GitHub branch- @@ -512,13 +505,13 @@ quick-deploy: # the user's `user.signingkey`; fails loudly if no key is # configured (correct: the commit cannot land on `main` # without one). - printf "\n\033[0;34m📝 Step 3/4: Git commit...\033[0m\n" + printf "\n\033[0;34m📝 Step 2/3: Git commit...\033[0m\n" git add . git commit -S -m "debug: quick-deploy v${NEW_VERSION} [skip ci]" printf "\033[0;32m → Committed\033[0m\n" - # 4. Git push - printf "\n\033[0;34m🚀 Step 4/4: Git push...\033[0m\n" + # 3. Git push + printf "\n\033[0;34m🚀 Step 3/3: Git push...\033[0m\n" git pull origin main --rebase git push origin main printf "\033[0;32m → Pushed\033[0m\n" diff --git a/just/help.just b/just/help.just index 3250d6e71..eff6d3064 100644 --- a/just/help.just +++ b/just/help.just @@ -8,7 +8,7 @@ _default-help: @echo "" @printf "\033[0;32m🚀 Ship & Validate:\033[0m\n" @echo " just go Safe validation (no side effects)" - @echo " just ship Full ship pipeline (validate → version → build → deploy → push)" + @echo " just ship Full ship pipeline (validate → commit → push; version via release-plz)" @echo " just ship-fresh Ship from scratch (ignore previous progress)" @echo " just q Quick deploy to Windows (skip CI, debug only)" @echo "" diff --git a/just/workflow.just b/just/workflow.just index 261aa2448..527cbd2dd 100644 --- a/just/workflow.just +++ b/just/workflow.just @@ -38,12 +38,12 @@ phase1-test: @echo "" @printf "\033[0;32m✅ PHASE 1 COMPLETE: Validation passed with no release-side effects.\033[0m\n" @printf "\033[0;36m📝 Note: Linux-specific code paths (cfg target_os = linux) are validated by CI on push.\033[0m\n" - @printf "\033[0;34m💡 Next: Run 'just phase2-ship' only when ready to version/deploy/commit/push\033[0m\n" + @printf "\033[0;34m💡 Next: Run 'just phase2-ship' only when ready to commit/push (version via release-plz)\033[0m\n" -# Phase 2: version, build, deploy, commit, and push. +# Phase 2: commit and push (version bump retired — release-plz handles it). phase2-ship: @printf "\033[0;34m🚀 PHASE 2: Explicit Ship Lane\033[0m\n" - @printf "\033[1;33mThis lane performs version bump, deploy, commit, and push behavior.\033[0m\n" + @printf "\033[1;33mThis lane performs commit and push. Version bumping now handled by release-plz on `main`.\033[0m\n" @echo "========================================================" cargo run -q --release --target-dir target/ci-bootstrap -p uffs-ci-pipeline -- phase2 diff --git a/scripts/ci-pipeline/src/phases.rs b/scripts/ci-pipeline/src/phases.rs index d08b1ed15..a6c210c05 100644 --- a/scripts/ci-pipeline/src/phases.rs +++ b/scripts/ci-pipeline/src/phases.rs @@ -17,9 +17,10 @@ //! //! * [`phase1_optimized`] — thin orchestrator: [`phase1_prime`] → //! [`phase1_tests`] → [`phase1_fanout_validation`]. -//! * [`phase2_optimized`] — version bump + commit + push in a straight line -//! (used by `just phase2-ship`, separate from the resumable -//! `run_enhanced_phase2` that [`crate::ship`] drives). +//! * [`phase2_optimized`] — commit + push in a straight line (used by `just +//! phase2-ship`, separate from the resumable `run_enhanced_phase2` that +//! [`crate::ship`] drives). Version bumping is handled by release-plz on +//! `main`. //! * [`coverage_data_exists`] / [`coverage_report_command`] — the //! `coverage-report` subcommand primitives; referenced from both //! `phase1_tests` and the CLI dispatch. @@ -30,7 +31,7 @@ use colored::Colorize as _; use crate::context::{PipelineContext, get_cargo_target_dir}; use crate::exec::{execute_command, execute_parallel_with_env}; use crate::git_ops::{git_commit, git_push}; -use crate::version::{get_current_version, version_bump}; +use crate::version::get_current_version; use crate::workflow::WorkflowState; /// Return `true` if a previous `cargo llvm-cov` run left behind @@ -260,49 +261,47 @@ pub(crate) async fn phase1_optimized(ctx: &PipelineContext) -> Result<()> { // Phase 2 (explicit ship lane — non-resumable counterpart) // ───────────────────────────────────────────────────────────────────────────── -/// Phase 2: Explicit ship lane (version bump → commit → push). Used -/// by the standalone `just phase2-ship` recipe. The resumable -/// equivalent lives in [`crate::ship::run_enhanced_phase2`] and is -/// the one `run_ship_pipeline` calls. +/// Phase 2: Explicit ship lane (commit → push). Used by the +/// standalone `just phase2-ship` recipe. The resumable equivalent +/// lives in [`crate::ship::run_enhanced_phase2`] and is the one +/// `run_ship_pipeline` calls. /// /// # Errors /// -/// Propagates any failure from [`version_bump`], [`git_commit`], or -/// [`git_push`]. The workflow-state mutation will also fail if the -/// state file cannot be written. +/// Propagates any failure from [`git_commit`] or [`git_push`]. +/// The workflow-state mutation will also fail if the state file +/// cannot be written. pub(crate) async fn phase2_optimized(ctx: &PipelineContext) -> Result<()> { println!("{}", "🚀 PHASE 2: Explicit Ship Lane".blue().bold()); - // Step 1: Version increment - version_bump(ctx).await?; + // Note: Version increment retired in Phase R5. release-plz now + // handles version bumps automatically on `main` after PR merge. - // Update workflow state with new version + // Update workflow state with current version let mut state = WorkflowState::load().context("Failed to load workflow state")?; - let new_version = get_current_version().context("Failed to get updated version")?; - state.current_version = new_version; - state.version_incremented = true; + let current_version = get_current_version().context("Failed to get current version")?; + state.current_version = current_version; state.save().context("Failed to save workflow state")?; println!( - "✅ Workflow state updated with new version: {}", + "✅ Workflow state initialized with version: {}", state.current_version ); - // Step 2: Git commit (signed version-bump commit on the working - // branch). + // Step 1: Git commit (signed commit on the working branch). git_commit(ctx).await?; - // Step 3: Git push -- opens release/vX.Y.Z PR with auto-merge + // Step 2: Git push -- opens release/vX.Y.Z PR with auto-merge // queued. // // Binaries are NOT built here. Once the PR merges to main, - // `auto-tag-release.yml` tags the commit and invokes - // `release.yml`, which produces the reproducible cross-platform - // binaries on GitHub-hosted runners. + // release-plz tags the commit and invokes `release.yml`, which + // produces the reproducible cross-platform binaries on GitHub-hosted + // runners. git_push(ctx).await?; println!( "{}", - "✅ PHASE 2 COMPLETE: Versioned, committed, and release PR opened!" + "✅ PHASE 2 COMPLETE: Committed and release PR opened (version via release-plz)!" .green() .bold() ); diff --git a/scripts/ci-pipeline/src/ship.rs b/scripts/ci-pipeline/src/ship.rs index e0eca11f7..e4f5543a9 100644 --- a/scripts/ci-pipeline/src/ship.rs +++ b/scripts/ci-pipeline/src/ship.rs @@ -39,11 +39,11 @@ use crate::exec::{ execute_step_with_tracking, }; use crate::git_ops::{count_unpushed_commits, git_commit, git_push}; -use crate::version::{get_current_version, increment_version}; +use crate::version::get_current_version; use crate::workflow::{ ALL_STEPS, STEP_CLEAN_ARTIFACTS, STEP_COVERAGE_TESTS, STEP_FORMAT_CHECK, STEP_FORMAT_CODE, - STEP_GIT_COMMIT, STEP_GIT_PUSH, STEP_PARALLEL_VALIDATION, STEP_TOOLCHAIN_SYNC, - STEP_VERSION_INCREMENT, WorkflowPhase, WorkflowState, + STEP_GIT_COMMIT, STEP_GIT_PUSH, STEP_PARALLEL_VALIDATION, STEP_TOOLCHAIN_SYNC, WorkflowPhase, + WorkflowState, }; // ───────────────────────────────────────────────────────────────────────────── @@ -458,14 +458,13 @@ pub(crate) async fn run_enhanced_phase2( ) -> Result<()> { println!( "{}", - "📦 PHASE 2: Version Increment + Release PR".blue().bold() + "📦 PHASE 2: Release PR (version bump handled by release-plz)" + .blue() + .bold() ); - // Step 07: Version increment - execute_step_with_tracking(state, STEP_VERSION_INCREMENT, || async { - increment_version().await - }) - .await?; + // Note: Version increment (step 07) was retired in Phase R5. + // release-plz now handles version bumps automatically on `main`. if !state.version_incremented { state.version_incremented = true; @@ -474,17 +473,16 @@ pub(crate) async fn run_enhanced_phase2( state.save()?; } - // Step 10: Git commit (signed version-bump commit on the working - // branch). + // Step 10: Git commit (signed commit on the working branch). execute_step_with_tracking(state, STEP_GIT_COMMIT, || async { git_commit(ctx).await }).await?; // Step 11: Git push -- opens release/vX.Y.Z PR with auto-merge // queued. // // Binaries are NOT built here. Once the PR merges to main, - // `auto-tag-release.yml` tags the commit and invokes - // `release.yml`, which produces the reproducible cross-platform - // binaries on GitHub-hosted runners. + // release-plz creates the tag and dispatches `release.yml`, + // which produces the reproducible cross-platform binaries on + // GitHub-hosted runners. // // Phase 6 resumable-push fix (docs/architecture/dev-flow.md § // 5.1 / dev-flow-implementation-plan.md § 6.3): if the developer diff --git a/scripts/ci-pipeline/src/version.rs b/scripts/ci-pipeline/src/version.rs index fef3e8c08..fb3b80030 100644 --- a/scripts/ci-pipeline/src/version.rs +++ b/scripts/ci-pipeline/src/version.rs @@ -1,30 +1,14 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2025-2026 SKY, LLC. -#![expect( - clippy::print_stdout, - reason = "operational CLI tool — version bump confirmations + tip messages go to stdout (issue #212)" -)] - -//! Version discovery + bump helpers for the UFFS ship pipeline. +//! Version discovery helpers for the UFFS ship pipeline. //! //! * [`get_current_version`] — read `[workspace.package].version` out of the //! root `Cargo.toml` (simple whole-file scan). //! * [`extract_version_from_cargo_toml`] — same, but strict: only considers the //! `[workspace.package]` section. -//! * [`increment_version`] — shell out to the `./build/update_all_versions.rs` -//! rust-script that actually rewrites the Cargo.toml in place. -//! * [`version_bump`] — tracked-step wrapper around [`increment_version`] that -//! threads through the pipeline's logging / timeout conventions. - -use std::path::Path; use anyhow::{Context as _, Result, bail}; -use colored::Colorize as _; -use tokio::process::Command; - -use crate::context::PipelineContext; -use crate::exec::execute_command; /// Read the workspace root `Cargo.toml` and return the first /// `version = "..."` string found. Used by the push step to build @@ -77,53 +61,3 @@ pub(crate) fn extract_version_from_cargo_toml(content: &str) -> Result { } bail!("Version extraction failed - no version found in [workspace.package]") } - -/// Parse the current `[workspace.package].version`, bump the patch -/// component, and rewrite `Cargo.toml` in place. Separated from -/// [`version_bump`] so it can be called directly from the workflow -/// state machine without involving a subprocess. -/// -/// # Errors -/// -/// Returns an error if the `./build/update_all_versions.rs` helper -/// cannot be spawned, or if it exits with a non-zero status. -pub(crate) async fn increment_version() -> Result<()> { - println!("📈 Incrementing version..."); - let output = Command::new("./build/update_all_versions.rs") - .arg("patch") - .output() - .await - .context("Failed to execute version update script")?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - bail!("Version bump failed: {stderr}"); - } - println!("✅ Version incremented successfully"); - Ok(()) -} - -/// Bump the workspace `[package].version` in root `Cargo.toml`. -/// Runs the shared [`increment_version`] helper under the usual -/// logging and timeout wrapping. -/// -/// # Errors -/// -/// Propagates any failure from the wrapped [`execute_command`] -/// subprocess. Fails fast if the helper script is missing. -pub(crate) async fn version_bump(ctx: &PipelineContext) -> Result<()> { - println!("{}", "📈 Incrementing version...".blue()); - let script_path = Path::new("./build/update_all_versions.rs"); - if script_path.exists() { - execute_command( - "Version increment", - "./build/update_all_versions.rs", - &["patch"], - ctx, - ) - .await?; - } else { - println!("{}", "⚠️ Version script not found".yellow()); - bail!("Version bump failed - ./build/update_all_versions.rs not found"); - } - Ok(()) -} diff --git a/scripts/ci-pipeline/src/workflow.rs b/scripts/ci-pipeline/src/workflow.rs index 4d5ea69ad..a4f1f0adf 100644 --- a/scripts/ci-pipeline/src/workflow.rs +++ b/scripts/ci-pipeline/src/workflow.rs @@ -48,8 +48,10 @@ pub(crate) const STEP_COVERAGE_TESTS: &str = "04-coverage-tests"; pub(crate) const STEP_PARALLEL_VALIDATION: &str = "05-parallel-validation"; /// Verify `cargo fmt` produces zero diff (idempotency check). pub(crate) const STEP_FORMAT_CHECK: &str = "06-format-check"; -/// Bump the workspace `[package].version` in root `Cargo.toml`. -pub(crate) const STEP_VERSION_INCREMENT: &str = "07-version-increment"; +// Step 07 (version-increment) was removed: version bumping now happens +// automatically via release-plz on the `main` branch after PR merge. +// Step numbering is preserved to keep in-flight resumable-ship state +// files compatible. // Steps 08 (build-release) and 09 (deploy-binary) were removed: `just // ship` no longer produces binaries locally. The release branch PR // (step 11) lands the version bump on main; `auto-tag-release.yml` @@ -73,7 +75,7 @@ pub(crate) const ALL_STEPS: &[&str] = &[ STEP_COVERAGE_TESTS, STEP_PARALLEL_VALIDATION, STEP_FORMAT_CHECK, - STEP_VERSION_INCREMENT, + // STEP_VERSION_INCREMENT retired — release-plz handles version bumps STEP_GIT_COMMIT, STEP_GIT_PUSH, ]; @@ -90,6 +92,9 @@ pub(crate) enum WorkflowPhase { /// No pipeline in flight. Clean, /// Phase 2 step 07: bumping `[workspace.package].version`. + /// **RETIRED in Phase R5** — version bumping now handled by release-plz. + /// Preserved for backwards compatibility with existing resumable-state + /// files. VersionIncrementing, /// Phase 1 test pass (coverage tests + parallel validation). Testing, diff --git a/scripts/ci/ci-pipeline.rs b/scripts/ci/ci-pipeline.rs deleted file mode 100755 index a3b9fcd21..000000000 --- a/scripts/ci/ci-pipeline.rs +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env rust-script -//! ```cargo -//! [dependencies] -//! ``` -// ============================================================================= -// scripts/ci/ci-pipeline.rs — LEGACY thin wrapper -// ============================================================================= -// -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2025-2026 SKY, LLC. -// -// Phase 7 of the dev-flow implementation plan promoted the CI pipeline -// driver to a proper Cargo workspace binary at `scripts/ci-pipeline/`. -// This file used to be a ~1750-line `rust-script` that compiled to an -// opaque cache under `~/.cache/cargo-rust-script/` — meaning local edits -// took effect only after an implicit Cargo rebuild on next invocation, -// and the IDE had no inline diagnostics for it. -// -// This thin shim remains so the muscle-memory invocation -// -// rust-script scripts/ci/ci-pipeline.rs -// -// keeps working for one release cycle (through v0.5.73). It just forwards -// every argument to the workspace binary: -// -// cargo run -q --release -p uffs-ci-pipeline -- -// -// The in-repo justfile recipes have already been updated to call the new -// binary directly; this file exists purely for humans and out-of-tree -// scripts that still hard-code the old path. -// -// REMOVE-AFTER: v0.5.73. -// See: docs/architecture/dev-flow-implementation-plan.md § 7. -// ============================================================================= - -use std::process::{Command, exit}; - -fn main() { - eprintln!( - "[ci-pipeline] DEPRECATED: scripts/ci/ci-pipeline.rs is now a thin \ - wrapper. The implementation moved to the workspace binary \ - `uffs-ci-pipeline` (Phase 7 of dev-flow plan). Prefer \ - `cargo run -q --release -p uffs-ci-pipeline -- ` or \ - the updated `just` recipes. This shim will be removed after v0.5.73." - ); - let args: Vec = std::env::args().skip(1).collect(); - let status = Command::new("cargo") - .args(["run", "-q", "--release", "-p", "uffs-ci-pipeline", "--"]) - .args(&args) - .status() - .expect("failed to spawn `cargo run -p uffs-ci-pipeline`"); - exit(status.code().unwrap_or(1)); -}