diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 00000000..92fa20ed --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -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 }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 52624573..0be03ccb 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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] - `, 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). diff --git a/docs/adr/0001-release-plz-release-pr.md b/docs/adr/0001-release-plz-release-pr.md new file mode 100644 index 00000000..2a404c38 --- /dev/null +++ b/docs/adr/0001-release-plz-release-pr.md @@ -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] - ` 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 | `-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. diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 00000000..e487632c --- /dev/null +++ b/release-plz.toml @@ -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