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
27 changes: 27 additions & 0 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<owner>/<repo>/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).
Expand Down
131 changes: 131 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -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/<owner>/<repo>/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 <each file>
```

## 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.
12 changes: 3 additions & 9 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,17 @@ 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

# Check for semver violations against the latest git tag
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 #
# ---------------------------------------------------------------------------- #
Expand Down
14 changes: 14 additions & 0 deletions release-plz.toml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 0 additions & 14 deletions release.toml

This file was deleted.

Loading