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
6 changes: 6 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ fi
echo "✓ Pre-commit hook passed"
```

### Changelog

Each release crate (`payjoin`, `payjoin-cli`, `payjoin-mailroom`) keeps a `CHANGELOG.md` with an `## Unreleased` section at the top. If your pull request changes what a user of that crate sees, add a line there in the same commit. Write it for the person reading the release notes, not for the reviewer. The release PR renames the heading to the version number and nothing else.

If the change has no user-visible effect (refactoring, tests, CI, comments), say so in the pull request description and a maintainer will apply the `changelog: none` label. CI fails a pull request that touches a release crate's `src/` without either a changelog line or that label.

## AI Assistance Notice

> [!IMPORTANT]
Expand Down
103 changes: 52 additions & 51 deletions .github/ISSUE_TEMPLATE/minor-release.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,66 @@
---
name: Minor Release
about: Checklist for releasing a new minor version bump
title: Release MAJOR.MINOR+1.0
about: Checklist for releasing a new minor version of a release crate
title: Release CRATE-MAJOR.MINOR+1.0
labels: ""
assignees: ""
---

## Create a new minor release
## Release CRATE MAJOR.MINOR+1.0

### Summary

<!-- release summary to be used in announcements -->

### Commit
Release-managed crates are `payjoin`, `payjoin-cli` and `payjoin-mailroom`. Versions follow
[Semantic Versioning]. Tags are `<crate>-<version>`. The changelog for this release is the
`## Unreleased` section of `CRATE/CHANGELOG.md`; it was written by the pull requests that
landed since the last release, so there is nothing to compile here.

<!-- latest commit ID to include in this release -->

### Changelog
### Summary

<!-- add notices from PRs merged since the prior release, see ["keep a changelog"] -->
<!-- two or three sentences for the announcement -->

### Checklist

Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
branch **development** version is _MAJOR.MINOR.0_. Release-managed crates are `payjoin`,
`payjoin-cli`, and `payjoin-mailroom`; tags use the `<crate>-<version>` scheme.

#### On the day of the feature freeze

Change the `master` branch to the next MINOR+1 version:

- [ ] Switch to the `master` branch.
- [ ] Create a new PR branch called `bump-CRATE-MAJOR-MINOR+1`, eg. `bump-CRATE-0-22`.
- [ ] Bump the `bump-CRATE-MAJOR-MINOR+1` branch to the next development MINOR+1 version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0` for the crate being released,
and update every workspace member's version requirement on it to match.
- Run `contrib/update-lock-files.sh` to apply upgrades to the Cargo lock files.
- Update the crate's `CHANGELOG.md` file, adding a `## MAJOR.MINOR+1.0` section that
summarizes the PRs merged since the last release tag.
- The commit message should be "Bump CRATE version to MAJOR.MINOR+1.0".
- [ ] Create PR for the `bump-CRATE-MAJOR-MINOR+1` branch to `master`.
- Title PR "Bump CRATE version to MAJOR.MINOR+1.0".
- [ ] Merge the `bump-CRATE-MAJOR-MINOR+1` branch to `master`.

If any issues need to be fixed before the _MAJOR.MINOR+1.0_ version is released:

- [ ] Merge fix PRs to the `master` branch.
- [ ] Git cherry-pick fix commits to the `bump-CRATE-MAJOR.MINOR+1` branch.
- [ ] Verify fixes in `bump-CRATE-MAJOR.MINOR+1` branch.

#### On the day of the release

- [ ] Create a signed annotated tag on the `HEAD` commit in the `master` branch.
- The tag name should be `CRATE-MAJOR.MINOR+1.0`, eg. `payjoin-1.0.0`.
- The first line of the tag message should be "Release CRATE-MAJOR.MINOR+1.0".
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
- Sign with a key committed under `contrib/release/keys/`, using the explicit `--sign` flag.
- [ ] Push the new tag to the `payjoin/rust-payjoin` repo.
- [ ] Approve the `release` environment when it requests a reviewer.
- [ ] Announce the release, using the **Summary**, on Discord, Twitter, Nostr, and stacker.news.
- [ ] Celebrate 🎉
#### Bump

- [ ] Branch `bump-CRATE-MAJOR-MINOR+1` from `master`.
- [ ] Run `contrib/release/bump.sh CRATE MAJOR.MINOR+1.0`. It sets the version, rewrites
every workspace member's requirement on the crate, renames `## Unreleased` to
`## MAJOR.MINOR+1.0` with a fresh empty `## Unreleased` above it, regenerates both
lock files and runs check-invariants. It needs jq, and the lock file step calls
`cargo +nightly`, so run it where rustup has a nightly toolchain. `nix develop .#release`
has jq but no rustup; there, pass `--no-lock`, then run `contrib/update-lock-files.sh`
from a rustup shell and `contrib/release/check-invariants.sh CRATE` again.
- [ ] Read the new changelog section once; fix wording, do not add history.
- [ ] One commit, "Bump CRATE version to MAJOR.MINOR+1.0". Open the PR against `master`.
The `Check release version bump` job runs check-invariants, a publish dry run and,
for `payjoin`, cargo-semver-checks.
- [ ] Merge.

If a fix must land before the tag, merge it to `master` as usual; it carries its own
changelog line. The bump PR is not edited for it.

#### Tag and publish

- [ ] Check out the merged `master` commit. Run
`nix develop .#release -c contrib/release/tag.sh CRATE`. It checks the tree is clean
and on payjoin/rust-payjoin's `master` (fetched by URL, so any remote layout works),
runs check-invariants, and creates the signed tag `CRATE-MAJOR.MINOR+1.0` with a key
from `contrib/release/keys/`.
- [ ] Push the tag with the command the script prints.
- [ ] Approve the `release` environment when the workflow asks. Publication, the GitHub
release and the crates.io/docs.rs verification run from there.

#### After a `payjoin` release

- [ ] Open the bindings bump PR (python, javascript, csharp, dart manifests and changelogs;
Dart's `native/Cargo.toml` pins the `payjoin-MAJOR.MINOR+1.0` tag commit, never a
branch commit). Merge, then run
`nix develop .#release -c contrib/release/tag.sh --bindings` and push the tags.
- [ ] Bump dependent release crates (`payjoin-cli`, `payjoin-mailroom`) on their own
schedule, each with this checklist. Their tags verify that `payjoin` is already on
crates.io.

#### Announce

- [ ] Discord, Twitter, Nostr, stacker.news, using the Summary.

[Semantic Versioning]: https://semver.org/
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
41 changes: 41 additions & 0 deletions .github/workflows/changelog-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# .github/workflows/changelog-check.yml
#
# A pull request that changes a release crate's source must add a line to
# that crate's CHANGELOG.md under "## Unreleased", or carry the
# "changelog: none" label. The bump PR then only renames the heading, so a
# merge under an open bump PR never requires editing it.
name: Changelog

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

permissions:
contents: read

jobs:
changelog:
name: Changelog entry
if: "${{ !contains(github.event.pull_request.labels.*.name, 'changelog: none') }}"
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Require a changelog line for each changed release crate
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
source contrib/release/crates.sh
changed="$(git diff --name-only "$BASE" "$HEAD")"
status=0
for crate in $RELEASE_CRATES; do
grep -q "^$crate/src/" <<<"$changed" || continue
if ! grep -q "^$crate/CHANGELOG.md$" <<<"$changed"; then
echo "::error::$crate/src changed but $crate/CHANGELOG.md did not; add a line under '## Unreleased', or say in the PR description that there is no user-visible change so a maintainer can label it 'changelog: none'"
status=1
fi
done
exit "$status"
78 changes: 78 additions & 0 deletions contrib/release/bump.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
#
# Prepare a release crate's bump commit. Sets the crate's version, rewrites
# every workspace member's version requirement on it, renames the
# "## Unreleased" changelog section to the new version and opens a fresh
# empty one above it, regenerates both lock files, then runs
# check-invariants. Leaves the changes uncommitted for review.
#
# usage: bump.sh [--no-lock] <crate> <version>
# --no-lock skip contrib/update-lock-files.sh (then check-invariants
# reports the stale lock files, as it should)
#
# Needs jq, and without --no-lock a rustup nightly toolchain, which the
# lock file script calls as `cargo +nightly`.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=contrib/release/crates.sh
source "$DIR/crates.sh"
cd "$REPO_ROOT"

lock=1
if [ "${1:-}" = "--no-lock" ]; then
lock=0
shift
fi
[ "$#" -eq 2 ] || {
sed -n '2,14p' "$0" >&2
exit 1
}
crate="$1"
new="$2"
die() {
echo "bump: $*" >&2
exit 1
}

case " $RELEASE_CRATES " in
*" $crate "*) ;;
*) die "$crate is not a release crate ($RELEASE_CRATES)" ;;
esac
[[ $new =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] || die "$new is not a semver version"
[ -z "$(git status --porcelain --untracked-files=no)" ] || die "commit or discard your changes first"

RELEASE_METADATA="$(cargo_metadata)"
old="$(manifest_version "$crate")"
[ "$old" != "$new" ] || die "$crate is already $new"
echo "Bumping $crate $old -> $new"

# The crate's own version: the first line-anchored `version =` in its manifest.
sed -i "0,/^version = \"$old\"/s//version = \"$new\"/" "$crate/Cargo.toml"
[ "$(sed -n 's/^version = "\(.*\)"/\1/p' "$crate/Cargo.toml" | head -1)" = "$new" ] ||
die "could not set version in $crate/Cargo.toml"

# Every workspace member that requires it by version. Members use the
# single-line table form `crate = { version = "x", ... }`.
while IFS= read -r manifest; do
[ -f "$manifest" ] || continue
if grep -q "^$crate = { version = \"$old\"" "$manifest"; then
sed -i "s/^\($crate = { version = \"\)$old\"/\1$new\"/" "$manifest"
echo " $manifest: requirement on $crate -> $new"
fi
done < <(cargo_metadata | jq -r '.packages[].manifest_path')

changelog="$crate/CHANGELOG.md"
grep -qx '## Unreleased' "$changelog" ||
die "$changelog has no '## Unreleased' section; add the lines for this release under one first"
sed -i "0,/^## Unreleased$/s//## Unreleased\n\n## $new/" "$changelog"
echo " $changelog: '## Unreleased' -> '## $new', fresh Unreleased section added"

if [ "$lock" -eq 1 ]; then
echo "Regenerating lock files (contrib/update-lock-files.sh)"
./contrib/update-lock-files.sh
fi

"$DIR/check-invariants.sh" "$crate"
echo
echo "Review the '## $new' section of $changelog, then commit as:"
echo " git commit -am 'Bump $crate version to $new'"
Loading
Loading