Skip to content
Open
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
53 changes: 53 additions & 0 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release-plz
# Keeps one release PR open on main. See docs/adr/0001-release-plz-release-pr.md.
on:
push:
branches:
- main

permissions:
contents: read

# Never cancel a run in progress. Today this only avoids a half-updated release
# PR; once this workflow also tags and releases, a run cancelled halfway could
# leave a tag without a GitHub release.
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false

jobs:
release-pr:
name: Release PR
runs-on: ubuntu-latest
timeout-minutes: 30
# The App credentials live in the `release` environment, which only main can use.
environment: release
steps:
# A GitHub App token, not GITHUB_TOKEN: a PR opened with GITHUB_TOKEN does
# not trigger other workflows, so test.yml would never run on the release PR
# and the required "Build (Rust MSRV)" check would never report.
- name: Create GitHub App token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ secrets.RELEASE_PLZ_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0 # release-plz reads tags and history since the last release
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false

# Opens or updates the release PR. On GitHub, release-plz creates the branch
# and commit through the API (no git push, so no persisted credentials),
# and GitHub marks the commit Verified.
- name: Run release-plz
uses: release-plz/action@8b5bf233ecb5779f63508db7cd050e948ee77cc2 # v0.5.144
with:
command: release-pr
version: 0.3.174
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
16 changes: 15 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,21 @@ version and notes, then **tagging** that merged commit.

### 1. Prepare-release PR

Open a PR against `main` that:
On `main`, [release-plz](https://release-plz.dev) opens this PR for you and keeps it up to date.
After every push to `main`, the `Release-plz` workflow opens or updates one PR, titled like `chore: release v3.1.0`, on a `release-plz-*` branch.
It bumps the workspace version (and `Cargo.lock`) from the conventional commits since the last `v*` tag.
To use it:

1. Check the proposed version.
If it's wrong, for example because a breaking change wasn't marked `feat!:`, edit the version in the PR.
2. In `CHANGELOG.md`, rename `## [Unreleased]` to `## [X.Y.Z] - <release date>`, and add a new empty `## [Unreleased]` above it.
Make your edits (this one and any version change) the last commits before merging.
If `main` moves after a person has committed to the PR, release-plz closes it and opens a fresh one without those edits, so check the version again before approving.
3. Approve and merge in one sitting.
release-plz rebuilds the PR whenever `main` moves, and `main` keeps approvals, so an older approval can cover changes you haven't seen.

release-plz never generates changelog entries for Proxy, and the configuration is explained in [ADR 0001](docs/adr/0001-release-plz-release-pr.md).
To prepare a release by hand instead (always the case on `release/2.2`), open a PR that:

1. Bumps `version` under `[workspace.package]` in the root `Cargo.toml` (and runs
`cargo update --workspace` so `Cargo.lock` matches).
Expand Down
74 changes: 74 additions & 0 deletions docs/adr/0001-release-plz-release-pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
status: accepted
---

# Release Proxy from main with a release-plz release PR

## Context

Releases of Proxy from main used to take two manual steps.
A person opened a prepare-release PR that bumped `[workspace.package] version` and renamed the `## [Unreleased]` section of `CHANGELOG.md`.
After it merged, they ran `mise run release vX.Y.Z` to tag the merged commit and create the GitHub release.
Publishing that release started `release.yml`, which builds the binaries and pushes the Docker images `X.Y.Z`, `X.Y` and `latest`.
Each step was a chance to slip, for example by tagging a commit before the version bump.

Other CipherStash Rust repositories use [release-plz](https://release-plz.dev).
Proxy differs from them in ways that rule out copying their setup as it is:

- Proxy ships a binary and a Docker image, and nothing is published to crates.io.
- `CHANGELOG.md` is written by hand for users, with compatibility notes that commit messages don't carry.
- The required `Build (Rust MSRV)` check comes from `test.yml`, and the Docker publish runs on `release: published`.
Neither event fires for a PR or release created with the default `GITHUB_TOKEN`.
- Every commit on main must be signed.
- 2.2.x maintenance releases come from `release/2.2`, while main is on 3.x.

## Decision

Run release-plz on every push to main, in `.github/workflows/release-plz.yml`, configured by `release-plz.toml`.

- **Release PR.** release-plz keeps one PR open on a `release-plz-*` branch, bumping the workspace version from the conventional commits since the last release.
It makes the commit through the GitHub API, so GitHub marks it Verified and it meets main's signed-commit rule.
Merging that PR is the release, and the approval it needs is the human gate before production.
- **Versions from git tags.** `git_only = true` on `cipherstash-proxy` reads the last version from `v*` tags instead of a registry, and `publish = false`.
release-plz takes the highest semver tag that matches, so 2.2.x tags never affect main's version.
`git_only` is set on the package, not the workspace: workspace-wide, release-plz also looks up every other member at the last tag, and fails on members added since then (`cipherstash-proxy-burn-in` was added after v3.0.1).
- **Only `cipherstash-proxy` is released.** Workspace members that inherit `version.workspace` move with it.
- **No semver checks.** `semver_check = false`, because Proxy's library target is internal and checking it would turn refactors into major version bumps.
- **Hand-written changelog.** `changelog_update = false`.
A person renames `## [Unreleased]` to `## [X.Y.Z] - <date>` in the release PR, and the `Release metadata` check fails any PR that changes the version without that heading.
- **Release only from the release PR.** `release_always = false`.
- **Tag format `vX.Y.Z`**, matching every existing tag, `release.yml` and the Docker tags.
- **GitHub App token.** The workflow mints a token from a GitHub App so the release PR runs `test.yml` and, once releases are automated, publishing a release starts `release.yml`.
The App credentials and the tag signing key are secrets in a `release` environment that only main can use and that has no required reviewers, so the job never waits for approval.
- **Signed tags.** Release tags are annotated and GPG-signed.
- **Hardened workflow.** Actions are pinned to commit SHAs, the default permission is `contents: read`, and the concurrency group never cancels a run in progress.
- **release/2.2 stays manual.** Its version would be read from the highest tag (3.x), so release-plz can't run there.
2.2.x releases keep the documented manual steps and `latest=false` on their Docker tags.

### Compared with cipherstash/stack

The same as stack: hand-written changelogs, release-plz creates the tags and GitHub releases, tags are annotated and GPG-signed, and merging a PR on main is the only human gate.

Different from stack, deliberately:

| | stack | Proxy | Why Proxy differs |
| --- | --- | --- | --- |
| Version bump | Written by hand in an ordinary PR | release-plz release PR | Automating the bump is the reason to adopt release-plz |
| Token | `GITHUB_TOKEN` | GitHub App token | The release PR must run the required check, and the release must start the Docker build |
| Secrets | Repository secrets | `release` environment, main only | Keeps the App key and signing key away from other workflows |
| Version source | crates.io | `v*` git tags | Nothing is published to a registry |
| Tag format | `<crate>-vX.Y.Z` | `vX.Y.Z` | Existing tags, `release.yml` and Docker tags depend on it |

### Rollout

The release PR comes first.
Until the release step is added to the same workflow, a person still tags the merged release PR with `mise run release`, which makes an unsigned lightweight tag.
The release step will create the signed tag and the GitHub release when a release PR merges, and then `mise run release` is retired for main.

## Consequences

- Approving the release PR approves shipping main as it is at that moment.
Main keeps approvals when new commits land, and release-plz rebuilds the PR whenever main moves, so approve and merge in one sitting.
- When a person commits to the release PR (the changelog rename), release-plz closes it and opens a fresh one on the next push to main, so make that edit last.
- Switching on `changelog_update` to match other repositories would put generated entries beside the curated notes, so keep it off.
- If the App or its installation is removed, release PRs stop running CI and releases stop publishing Docker images, without any error from release-plz itself.
29 changes: 29 additions & 0 deletions release-plz.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# release-plz configuration for CipherStash Proxy.
# Why it looks like this, and how it differs from other CipherStash repos:
# docs/adr/0001-release-plz-release-pr.md

[workspace]
# Proxy ships as a binary and Docker image, never to crates.io.
publish = false
# Only cipherstash-proxy is released (below). Members that inherit
# `version.workspace` move with it.
release = false
# When the release job runs, release only if a release-plz release PR has
# just merged, never on other pushes to main.
release_always = false
# CHANGELOG.md is written by hand. A person renames `## [Unreleased]` in the
# release PR; the "Release metadata" check fails the PR until they do.
changelog_update = false
# Existing tags, release.yml and the Docker tags all use vX.Y.Z.
git_tag_name = "v{{ version }}"
# Proxy's library target is internal, not a public API. Semver checks would
# turn internal refactors into major version bumps.
semver_check = false

[[package]]
name = "cipherstash-proxy"
release = true
# Read the last released version from `v*` git tags instead of a registry.
# Set here, not under [workspace]: workspace-wide, release-plz also looks up
# every other member at the last tag and fails on members added since then.
git_only = true
Loading