From 4d41cbc4889854166c40433ac36098d669a9cae0 Mon Sep 17 00:00:00 2001 From: sksizer <56994+sksizer@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:23:49 -0500 Subject: [PATCH] ci: automate releases with release-plz Replace the local cargo-release flow (release.toml, just release recipes) with release-plz running in CI, mirroring the setup proven in rust-ontogen and rust-path-opener: a bot opens a 'chore: release' PR on pushes to main; merging it tags, publishes to crates.io, and creates a GitHub Release. Changelog generation stays on the existing cliff.toml. README documents the two one-time steps each templated repo needs: the CARGO_REGISTRY_TOKEN secret and enabling 'Allow GitHub Actions to create and approve pull requests'. RELEASE.md covers the full flow, the tracked-and-gitignored gotcha, and the manual escape hatch. --- .github/workflows/release-plz.yml | 27 ++++++ README.md | 29 +++++++ RELEASE.md | 131 ++++++++++++++++++++++++++++++ justfile | 12 +-- release-plz.toml | 14 ++++ release.toml | 14 ---- 6 files changed, 204 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/release-plz.yml create mode 100644 RELEASE.md create mode 100644 release-plz.toml delete mode 100644 release.toml diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..bf2bb72 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,27 @@ +name: "release-plz" + +on: + push: + branches: ["main"] + +permissions: + pull-requests: write + contents: write + +jobs: + release-plz: + runs-on: "ubuntu-latest" + concurrency: + group: "release-plz-${{ github.ref }}" + cancel-in-progress: false + steps: + - uses: "actions/checkout@v4" + with: + fetch-depth: 0 + - uses: "actions-rust-lang/setup-rust-toolchain@v1" + with: + toolchain: "stable" + - uses: "MarcoIeni/release-plz-action@v0.5" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" diff --git a/README.md b/README.md index a55e1c9..87ea037 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,35 @@ request made to the `main` branch. You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). +### Automated Releases + +Releases are automated by [release-plz](https://release-plz.dev/): on every push to `main`, a bot opens (or updates) a +`chore: release` PR with the version bump and changelog. Merging that PR tags the release, publishes to crates.io, and +creates a GitHub Release. See [RELEASE.md](./RELEASE.md) for the full flow. + +Two one-time setup steps are required in each new repository created from this template: + +1. **Set the crates.io token** so the publish step can run. Create an API token at + [crates.io/settings/tokens](https://crates.io/settings/tokens) (scope: `publish-new` + `publish-update`), then: + + ```sh + gh secret set CARGO_REGISTRY_TOKEN + ``` + + (Paste the token when prompted, or pipe it in. Without this secret, the release PR still gets opened — only the + crates.io publish on merge will fail.) + +2. **Allow GitHub Actions to create pull requests** — GitHub disables this by default, and release-plz can't open the + release PR without it. Enable it under Settings → Actions → General → Workflow permissions, or via CLI: + + ```sh + gh api -X PUT repos///actions/permissions/workflow \ + -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true + ``` + +If the project should not be published to crates.io, set `publish = false` in +[release-plz.toml](./release-plz.toml) — you keep the automated versioning, changelog, tags, and GitHub Releases. + ## Usage See [The Rust Book](https://doc.rust-lang.org/book/) and [The Cargo Book](https://doc.rust-lang.org/cargo/index.html). diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..b401187 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,131 @@ +# Releasing + +Releases are **fully automated** by [`release-plz`](https://release-plz.dev/) +running in CI. You do **not** run any release command locally — you merge a PR +that a bot prepares for you. + +## The flow, end to end + +``` + merge feature PRs to main + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ release-plz CI (on every push to main) │ + │ opens/updates a "chore: release" PR: │ + │ • bumps the crate version │ + │ • regenerates CHANGELOG.md (git-cliff) │ + └───────────────────────────────────────────────┘ + │ + ▼ + you review + merge the "chore: release" PR + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ release-plz CI (on that merge): │ + │ • git tag (v{version}) │ + │ • publish to crates.io │ + │ • create a GitHub Release │ + └───────────────────────────────────────────────┘ +``` + +So the only human steps are: **merge feature work**, then **merge the release +PR** when you're ready to cut a release. Everything else is the bot. + +## Where it's configured + +| File | What it controls | +|------|------------------| +| [`.github/workflows/release-plz.yml`](./.github/workflows/release-plz.yml) | The CI job: triggers on push to `main`, runs the `release-plz` action. | +| [`release-plz.toml`](./release-plz.toml) | Release behavior: publish on/off, semver gate, GitHub releases. | +| [`cliff.toml`](./cliff.toml) | Changelog format and how commits are grouped. | + +Key `release-plz.toml` settings: + +- `publish = true` — publishes to crates.io when the release PR merges. +- `git_release_enable = true` — creates a GitHub Release per release. +- `semver_check = true` — runs `cargo-semver-checks` before publishing. +- `changelog_config = "cliff.toml"` — reuse the git-cliff config for changelogs. +- `pr_name = "chore: release"` — the title of the automated release PR. + +If the project should **not** be published to crates.io, set `publish = false` +in `release-plz.toml` (and `publish = false` in `Cargo.toml` to match) — you +keep the automated version bumps, changelog, tags, and GitHub Releases. + +## How the version is decided + +release-plz reads **Conventional Commits** since the last release tag and picks +the bump automatically. Pre-1.0 crates follow 0.x SemVer — the **minor** slot is +the breaking tier: + +| Commit type | Bump (0.x) | +|-------------|-----------| +| `fix:` | patch | +| `feat:` | minor | +| `feat!:` / `fix!:` / `BREAKING CHANGE:` | minor (the 0.x breaking tier) | + +You influence the release by how you write commit messages — there is no manual +version bump. `cargo-semver-checks` runs as a gate before publishing and will +block a bump that's too small for the API delta. + +> Merge commits (`Merge pull request #NNN …`) are non-conventional and are +> filtered out of the changelog by `cliff.toml`; the underlying `feat:`/`fix:` +> commits on the branches are what appear. + +## Prerequisites (one-time setup per project) + +1. **`CARGO_REGISTRY_TOKEN` repo secret** — a crates.io API token with publish + rights. Needed only at the *publish* step (when a release PR merges), not to + open the PR. + - Set via: `gh secret set CARGO_REGISTRY_TOKEN` (paste a crates.io token), or + - Prefer **crates.io Trusted Publishing (OIDC)** — no long-lived token; see + the release-plz docs. +2. **"Allow GitHub Actions to create and approve pull requests"** must be enabled + (Settings → Actions → General → Workflow permissions), or via CLI: + + ```sh + gh api -X PUT repos///actions/permissions/workflow \ + -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true + ``` + + Without it, release-plz cannot open the release PR. The workflow already + grants the job `contents: write` and `pull-requests: write`. + +> Optional but recommended: have release-plz open the PR with a **PAT or GitHub +> App token** instead of the default `GITHUB_TOKEN`. PRs opened by the default +> token do **not** trigger other workflows (like `ci.yml`), so the release PR +> won't get CI runs otherwise. + +## Common gotcha: "working directory has uncommitted changes" + +release-plz refuses to run if any file is **both git-tracked and matched by a +committed `.gitignore`** — it sees them as uncommitted noise. Symptom in the CI +log: + +``` +ERROR failed to update packages + 1: the working directory of this project has uncommitted changes. If these + files are both committed and in .gitignore, either delete them or remove + them from .gitignore. +``` + +Find offenders and untrack them (they stay on disk): + +```sh +git ls-files --cached --ignored --exclude-standard +git rm --cached +``` + +## Manual escape hatch + +There is no local release recipe — release-plz owns releases. If you ever must +publish by hand (registry outage, emergency), do it deliberately and outside +this pipeline: + +```sh +cargo semver-checks --baseline-rev "$(git describe --tags --abbrev=0)" # just semver-check +cargo publish # then a real publish +``` + +...and reconcile the tag/changelog afterward so release-plz's next run agrees +with reality. diff --git a/justfile b/justfile index 6900bad..efd1e69 100644 --- a/justfile +++ b/justfile @@ -77,8 +77,10 @@ install-tools: # ---------------------------------------------------------------------------- # # RELEASE # # ---------------------------------------------------------------------------- # +# Releases are automated by release-plz in CI — merge the "chore: release" PR +# it opens on main. See RELEASE.md. The recipes below are local previews only. -# Generate changelog from conventional commits +# Generate changelog from conventional commits (local preview; CI regenerates it) changelog: git-cliff --output CHANGELOG.md @@ -86,14 +88,6 @@ changelog: semver-check: cargo semver-checks --baseline-rev "$(git describe --tags --abbrev=0)" -# Dry-run a release (default: patch bump) -release-dry-run level="patch": - cargo release {{level}} --no-confirm - -# Perform a release (patch, minor, or major) -release level="patch": - cargo release {{level}} --execute - # ---------------------------------------------------------------------------- # # QUALITY CHECK # # ---------------------------------------------------------------------------- # diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..acf1350 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,14 @@ +# Releases are fully automated by release-plz running in CI — see RELEASE.md. +[workspace] +# Use the existing git-cliff config for changelogs +changelog_config = "cliff.toml" +pr_name = "chore: release" +# Run semver-checks before publishing +semver_check = true +# Create a GitHub release when the release PR merges +git_release_enable = true +# Publish to crates.io when the release PR is merged. Requires the +# CARGO_REGISTRY_TOKEN repo secret — see README / RELEASE.md. +publish = true +# Verify the package builds from a clean tree before publishing +publish_allow_dirty = false diff --git a/release.toml b/release.toml deleted file mode 100644 index bd5a0a5..0000000 --- a/release.toml +++ /dev/null @@ -1,14 +0,0 @@ -allow-branch = ["main"] -pre-release-commit-message = "chore: release v{{version}}" -publish = false -push = true -sign-commit = false -sign-tag = false -tag = true -tag-message = "Release {{tag_name}}" -tag-name = "v{{version}}" -tag-prefix = "" - -# 1. Generate changelog for the new version and stage it for the release commit -# 2. Run semver-checks against the latest tag (skipped if no prior tags exist) -pre-release-hook = ["bash", "-c", "git-cliff --tag \"v${NEW_VERSION}\" --output CHANGELOG.md && git add CHANGELOG.md && if git describe --tags --abbrev=0 >/dev/null 2>&1; then cargo semver-checks --baseline-rev \"$(git describe --tags --abbrev=0)\"; else echo 'semver-checks: no previous tag found, skipping'; fi"]