Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -34,17 +39,34 @@ 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

- 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:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
32 changes: 18 additions & 14 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -15,22 +18,17 @@ 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:

```sh
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.
Expand Down Expand Up @@ -64,18 +62,21 @@ 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

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-<platform>-<arch>` naming scheme
- Generates SHA256 checksum files
Expand All @@ -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`
14 changes: 11 additions & 3 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
11 changes: 11 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions scripts/check_release_tag_sync.sh
Original file line number Diff line number Diff line change
@@ -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 <vX.Y.Z>"
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
Loading