diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2dba3f858..6c5f47c4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,21 @@ env: CARGO_TERM_COLOR: always permissions: - contents: write + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: - license-index: - name: Verify Embedded License Index + verify-release: + name: Verify Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: + fetch-depth: 0 submodules: recursive lfs: false @@ -34,7 +39,7 @@ jobs: - name: Set up Rust cache uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae with: - key: embedded-license-index + key: release-verify - name: Verify embedded license index uses: ./.github/actions/verify-embedded-license-index @@ -42,9 +47,26 @@ jobs: - name: Verify ScanCode output format version sync uses: ./.github/actions/verify-scancode-output-format-version + - name: Verify release version sync + run: ./scripts/check_release_version_sync.sh + + - name: Verify release tag matches crate version + run: ./scripts/check_release_tag_sync.sh + + - name: Verify tagged commit is reachable from origin/main + run: | + git fetch origin main --depth=1 + git merge-base --is-ancestor "$GITHUB_SHA" origin/main + + - name: Check packaged crate size + run: ./scripts/check_crate_size.sh + + - name: Verify crates.io package publish dry-run + run: cargo publish --locked --dry-run -p provenant-cli + build: name: Build ${{ matrix.target }} - needs: license-index + needs: verify-release strategy: fail-fast: false matrix: @@ -137,10 +159,50 @@ jobs: target/${{ matrix.target }}/release/${{ matrix.asset_name }}.${{ matrix.archive_format }}.sha256 if-no-files-found: error + publish-crate: + name: Publish crate + needs: build + if: github.repository == 'mstykow/provenant' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + submodules: false + lfs: false + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 + with: + cache: false + rustflags: "" + + - name: Set up Rust cache + uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae + with: + key: publish-crate + + - name: Authenticate to crates.io via trusted publishing + id: crates-io-auth + uses: rust-lang/crates-io-auth-action@1d2b8f3552d69b407d7790fa3ec7c39041305a61 + + - name: Publish provenant-cli to crates.io + run: cargo publish --locked -p provenant-cli + env: + CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} + create-release: name: Create Release - needs: build + if: github.repository == 'mstykow/provenant' + needs: + - build + - publish-crate runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Download all artifacts uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 diff --git a/Cargo.toml b/Cargo.toml index c339c6b5e..278bee4cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,8 +53,10 @@ pre-release-replacements = [ pre-release-hook = ["cargo", "generate-lockfile"] # Tag message template tag-message = "Release v{{version}}" -# Publish to crates.io automatically -publish = true +# Release tags must use the v-prefixed GitHub workflow contract. +tag-name = "v{{version}}" +# Crates.io publish is handled by the tag-triggered GitHub Actions workflow. +publish = false [workspace] resolver = "3" diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 0036f182a..25971cf05 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -4,7 +4,10 @@ This guide documents the maintainer release flow for `provenant`. ## Overview -Releases are driven locally with `release.sh`, which wraps `cargo release`, refreshes the embedded license data, and checks for ScanCode output-format drift before publishing. +Releases are split into two phases: + +1. local release preparation with `release.sh`, which refreshes the embedded license data, runs the release-time sync checks, prepares the release commit, and pushes the release tag +2. tag-triggered GitHub Actions publication, which publishes `provenant-cli` to crates.io via trusted publishing and creates the GitHub Release assets The published crate name is `provenant-cli`, while the installed binary and product name remain `provenant` / Provenant. @@ -15,9 +18,10 @@ Before cutting a release, make sure you have: - A clean working tree - The `reference/scancode-toolkit/` submodule initialized via `./setup.sh` - `cargo-release` installed locally -- A valid crates.io login in your Cargo credentials - GPG signing configured for git tags -- A green `CI` workflow run for the exact commit you plan to release +- A green `CI` workflow run on `main` before you start release prep + +For the normal release path, you do **not** need `cargo login` on your local machine. crates.io authentication is handled by the tag-triggered GitHub Actions trusted publishing flow, which verifies that the tagged commit is reachable from `origin/main` before it can mint the short-lived crates.io token. Install `cargo-release` if needed: @@ -25,12 +29,6 @@ Install `cargo-release` if needed: cargo install cargo-release ``` -Authenticate with crates.io if needed: - -```sh -cargo login -``` - ## Preflight Checks The primary pre-release quality gate is the GitHub `CI` workflow in `.github/workflows/check.yml`. Start from a commit where that workflow is already green. @@ -64,10 +62,11 @@ On every release attempt, the script: 1. verifies a clean working tree and initialized ScanCode reference submodule 2. updates the pinned ScanCode checkout from `origin/develop` and regenerates the embedded license index artifact 3. checks ScanCode output-format version sync before continuing -4. in `--execute` mode, commits any license-data refresh with `git commit -s` -5. runs the `cargo release` flow for versioning, publishing, tagging, and pushing +4. runs the release version sync check after `cargo release` updates versioned files +5. in `--execute` mode, commits any license-data refresh with `git commit -s` +6. runs the local `cargo release` flow for versioning, tagging, and pushing -The exact `cargo release` behavior comes from `[package.metadata.release]` in `Cargo.toml`, including the `CITATION.cff` version replacement, `Cargo.lock` regeneration, signed tag creation, and publish/push behavior. The release commit written by `release.sh` stays versionless (`chore: release`) and DCO-signed. +The exact `cargo release` behavior comes from `[package.metadata.release]` in `Cargo.toml`, including the `CITATION.cff` version replacement, `Cargo.lock` regeneration, signed tag creation, and push behavior. The release commit written by `release.sh` stays versionless (`chore: release`) and DCO-signed. ## GitHub Release Automation @@ -75,7 +74,9 @@ Pushing the `vX.Y.Z` tag triggers `.github/workflows/release.yml`. That workflow: +- verifies release invariants on the tagged commit, including version/tag alignment and crates.io dry-run packaging - Builds release binaries for Linux, macOS (Intel and Apple Silicon), and Windows +- publishes `provenant-cli` to crates.io via trusted publishing - Re-runs embedded license index verification as a final release-time safeguard before building artifacts - Packages each build under the `provenant--` naming scheme - Generates SHA256 checksum files @@ -89,13 +90,16 @@ Monitor the [GitHub Actions release workflow](https://github.com/mstykow/provena Verify: -- The crates.io publish step succeeded +- The crates.io publish job in the GitHub Actions release workflow succeeded - The tag and release commit are present on the remote - The GitHub Release contains all expected Linux, macOS (Intel and Apple Silicon), and Windows archives and checksum files +If the GitHub Release asset step fails after crates.io publish has already succeeded, rerun only the failed downstream jobs. Do not rerun the successful crates.io publish job for the same version. + ## Common Failure Points - Missing submodule setup: run `./setup.sh` -- Missing crates.io credentials: run `cargo login` - Missing GPG configuration: `cargo release` cannot create the signed tag - Dirty working tree: clean up local changes before retrying +- Release tag is not reachable from `main` +- Release tag does not match the crate version in `Cargo.toml` diff --git a/release.sh b/release.sh index 155aaed2b..3172ef8f1 100755 --- a/release.sh +++ b/release.sh @@ -111,6 +111,9 @@ run_release_step version "$RELEASE_TYPE" run_release_step replace run_release_step hook +echo "🔎 Verifying release version sync..." +./scripts/check_release_version_sync.sh + if [ -n "$EXECUTE_FLAG" ]; then echo "📝 Creating DCO-signed release commit..." git add -u @@ -123,9 +126,14 @@ if [ -n "$EXECUTE_FLAG" ]; then git commit -s -m "chore: release" fi -echo "🚀 Running cargo-release publish, tag, and push steps..." -run_release_step publish +echo "đŸˇī¸ Running cargo-release tag and push steps..." run_release_step tag run_release_step push -echo "✅ Release completed successfully!" +if [ -n "$EXECUTE_FLAG" ]; then + echo "✅ Release prep completed successfully!" + echo "â„šī¸ The pushed release tag now triggers GitHub Actions to publish provenant-cli to crates.io and create GitHub release assets." +else + echo "✅ Dry-run release prep completed successfully!" + echo "â„šī¸ In execute mode, the pushed release tag will trigger GitHub Actions to publish provenant-cli to crates.io and create GitHub release assets." +fi diff --git a/scripts/README.md b/scripts/README.md index a08939421..b56265dd8 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -77,6 +77,17 @@ Example: ./scripts/check_release_version_sync.sh ``` +## `check_release_tag_sync.sh` + +Verify that a release tag matches the root crate version in `Cargo.toml`. + +Examples: + +```bash +./scripts/check_release_tag_sync.sh v0.1.1 +GITHUB_REF_NAME=v0.1.1 ./scripts/check_release_tag_sync.sh +``` + ## `check_scancode_output_format_sync.sh` Verify that Provenant's `OUTPUT_FORMAT_VERSION` stays aligned with the pinned diff --git a/scripts/check_release_tag_sync.sh b/scripts/check_release_tag_sync.sh new file mode 100755 index 000000000..1251d33fe --- /dev/null +++ b/scripts/check_release_tag_sync.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Provenant contributors +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +ROOT_MANIFEST="Cargo.toml" +TAG_NAME="${1:-${GITHUB_REF_NAME:-}}" + +if [ -z "$TAG_NAME" ]; then + echo "Usage: ./scripts/check_release_tag_sync.sh " + echo "Or set GITHUB_REF_NAME in the environment." + exit 1 +fi + +python3 - "$ROOT_MANIFEST" "$TAG_NAME" <<'PY' +import pathlib +import re +import sys + +root_manifest = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") +tag_name = sys.argv[2] + +if tag_name.startswith("refs/tags/"): + tag_name = tag_name.removeprefix("refs/tags/") + +root_version_match = re.search(r'^version = "([^"]+)"$', root_manifest, re.MULTILINE) +if root_version_match is None: + raise SystemExit("Could not determine root crate version from Cargo.toml") + +root_version = root_version_match.group(1) +expected_tag = f"v{root_version}" + +if tag_name != expected_tag: + raise SystemExit( + "Release tag is out of sync with Cargo.toml: " + f"expected {expected_tag}, got {tag_name}.\n" + "Create a tag that exactly matches the crate version." + ) +PY