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
4 changes: 2 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ 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.
Each release crate (`payjoin`, `payjoin-cli`, `payjoin-mailroom`) and `payjoin-ffi` 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. `payjoin-ffi/CHANGELOG.md` is the release notes for the Python, JavaScript, C# and Dart packages, which are released together at its version.

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.
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 one of those crates' `src/` without either a changelog line or that label.

### Nix Development Shells

Expand Down
10 changes: 4 additions & 6 deletions .github/ISSUE_TEMPLATE/minor-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ landed since the last release, so there is nothing to compile here.
`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.
- [ ] For a `payjoin` bump, prepare the common bindings version bump in this PR
as well, following the [bindings instructions](../../payjoin-ffi/RELEASING.md).
The checker requires the FFI core dependency and binding metadata to agree.
Publish the Rust core before pushing the corresponding FFI tag.
- [ ] 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,
Expand All @@ -55,10 +51,12 @@ changelog line. The bump PR is not edited for it.

#### After a `payjoin` release

- [ ] Confirm the prepared bindings bump is merged and follow the
- [ ] Prepare the bindings bump with `contrib/release/bump.sh payjoin-ffi X.Y.Z`,
which also sets the language package manifests, their lock files and
changelogs, and follow the
[bindings release instructions](../../payjoin-ffi/RELEASING.md). Dart's
published native source is pinned automatically to the release checkout.
Create one signed tag with
Once merged, create one signed tag with
`nix develop .#release -c contrib/release/tag.sh payjoin-ffi`, then push it.
- [ ] Bump dependent release crates (`payjoin-cli`, `payjoin-mailroom`) on their own
schedule, each with this checklist. Their tags verify that `payjoin` is already on
Expand Down
20 changes: 8 additions & 12 deletions .github/workflows/bindings-release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Release language bindings

# Pushing a signed `payjoin-ffi-<version>+payjoin-<core>` tag verifies the
# tag and the shared binding version, runs every language's build, package
# and smoke checks at that commit, then publishes Python, JavaScript, C#,
# and Dart through the `release` environment (required reviewer) with
# keyless OIDC and cuts one GitHub release holding every package.
# Pushing a signed `payjoin-ffi-<version>` tag verifies the tag and the
# shared binding version, runs every language's build, package and smoke
# checks at that commit, then publishes Python, JavaScript, C#, and Dart
# through the `release` environment (required reviewer) with keyless OIDC
# and cuts one GitHub release holding every package.
#
# Each registry has its own publish job. All four wait on every language
# build, so they reach the environment gate together and one approval
Expand Down Expand Up @@ -55,17 +55,13 @@ jobs:
source contrib/release/crates.sh
{
echo "version=$version"
# A hyphen in the version proper marks a pre-release; the
# +payjoin-<core> build metadata never does.
is_prerelease "${version%%+*}" && echo "prerelease=true" || echo "prerelease=false"
is_prerelease "$version" && echo "prerelease=true" || echo "prerelease=false"
} >>"$GITHUB_OUTPUT"
- name: Verify the wrapped payjoin core is on crates.io
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
source contrib/release/crates.sh
core="${VERSION##*+payjoin-}"
core="$(nix --quiet develop .#release -c bash -c 'source contrib/release/crates.sh && manifest_version payjoin')"
# Dart consumers build the bindings from source against it.
crate_published payjoin "$core" || {
echo "::error::payjoin $core is not on crates.io; publish it before the bindings"
Expand Down Expand Up @@ -297,7 +293,7 @@ jobs:
run: |
{
echo "body<<CHANGELOG_EOF"
./contrib/release/extract-changelog.sh payjoin-ffi "${VERSION%%+*}"
./contrib/release/extract-changelog.sh payjoin-ffi "$VERSION"
echo "CHANGELOG_EOF"
} >>"$GITHUB_OUTPUT"
- name: Create release
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/changelog-check.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# .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.
# A pull request that changes a release crate's or payjoin-ffi'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:
Expand All @@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Require a changelog line for each changed release crate
- name: Require a changelog line for each changed crate
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
Expand All @@ -31,7 +31,7 @@ jobs:
source contrib/release/crates.sh
changed="$(git diff --name-only "$BASE" "$HEAD")"
status=0
for crate in $RELEASE_CRATES; do
for crate in $RELEASE_CRATES payjoin-ffi; 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'"
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/crates-release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
name: Release

# A pull request that bumps a release crate's version is checked for
# consistency and publishability. Pushing a `<crate>-<version>` tag verifies
# consistency and publishability; one that bumps payjoin-ffi's version is
# checked for consistency across the binding packages, which
# bindings-release.yml publishes. Pushing a `<crate>-<version>` tag verifies
# the tag, waits for CI at that commit, then publishes to crates.io through
# the `release` environment (required reviewer) with keyless OIDC,
# cuts the GitHub release, and confirms the upload.
Expand All @@ -12,6 +14,7 @@ on:
- payjoin/Cargo.toml
- payjoin-cli/Cargo.toml
- payjoin-mailroom/Cargo.toml
- payjoin-ffi/Cargo.toml
push:
tags:
- payjoin-[0-9]*
Expand Down
23 changes: 10 additions & 13 deletions contrib/release/bindings-version.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env bash
#
# Check that every binding package carries the shared payjoin-ffi version
# and print the version a payjoin-ffi release tag names:
# <ffi version>+payjoin-<core version>. With --tag <tag>, also require
# that exact tag.
# and print it: the version a payjoin-ffi-<version> release tag names.
# With --tag <tag>, also require that exact tag.
#
# PyPI and npm cannot carry build metadata, so their manifests hold the
# bare version and package.json records the full one in releaseTag.
# Python spells a -rc.N pre-release as rcN (PEP 440).
# The manifests hold the bare version; the wrapped payjoin release is
# recorded in payjoin-ffi/CHANGELOG.md, and payjoin-ffi's own requirement
# on it must match the payjoin manifest. Python spells a -rc.N pre-release
# as rcN (PEP 440).
#
# usage: bindings-version.sh [--tag <tag>]
set -euo pipefail
Expand All @@ -34,7 +34,6 @@ ffi="$(manifest_version payjoin-ffi)"
core="$(manifest_version payjoin)"
[[ $ffi =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] ||
die "payjoin-ffi version $ffi must be MAJOR.MINOR.PATCH, optionally followed by -rc.N"
full="$ffi+payjoin-$core"
python="${ffi/-rc./rc}"

status=0
Expand All @@ -52,12 +51,10 @@ expect "python/pyproject.toml version" "$python" \
"$(sed -n 's/^version = "\(.*\)"$/\1/p' payjoin-ffi/python/pyproject.toml | head -1)"
expect "javascript/package.json version" "$ffi" \
"$(jq -r .version payjoin-ffi/javascript/package.json)"
expect "javascript/package.json releaseTag" "$full" \
"$(jq -r .releaseTag payjoin-ffi/javascript/package.json)"
expect "dart/pubspec.yaml version" "$full" \
expect "dart/pubspec.yaml version" "$ffi" \
"$(sed -n 's/^version: *//p' payjoin-ffi/dart/pubspec.yaml)"
expect "csharp/Payjoin.csproj Version" "$full" \
expect "csharp/Payjoin.csproj Version" "$ffi" \
"$(sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p' payjoin-ffi/csharp/Payjoin.csproj | head -1)"
[ -z "$tag" ] || expect "release tag" "payjoin-ffi-$full" "$tag"
[ -z "$tag" ] || expect "release tag" "payjoin-ffi-$ffi" "$tag"
[ "$status" -eq 0 ] || exit 1
printf '%s\n' "$full"
printf '%s\n' "$ffi"
129 changes: 122 additions & 7 deletions contrib/release/bump.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
# empty one above it, regenerates both lock files, then runs
# check-invariants. Leaves the changes uncommitted for review.
#
# payjoin-ffi is bumped the same way, except that its changelog entry
# opens by naming the wrapped payjoin release, and its version is also
# set in the Python, JavaScript, Dart and C# package manifests, the uv and
# npm lock files, and the language changelogs, which open with a stock
# entry that points at payjoin-ffi/CHANGELOG.md. bindings-version.sh then
# confirms the packages agree.
#
# usage: bump.sh [--no-lock] <crate> <version>
# crate a release crate, or payjoin-ffi for the language bindings
# --no-lock skip contrib/update-lock-files.sh (then check-invariants
# reports the stale lock files, as it should)
#
Expand All @@ -24,7 +32,7 @@ if [ "${1:-}" = "--no-lock" ]; then
shift
fi
[ "$#" -eq 2 ] || {
sed -n '2,14p' "$0" >&2
sed -n '2,22p' "$0" >&2
exit 1
}
crate="$1"
Expand All @@ -34,11 +42,91 @@ die() {
exit 1
}

case " $RELEASE_CRATES " in
# Rename a changelog's "## Unreleased" section to <heading>, open a fresh
# empty one above it, and make <line> the first entry of the renamed
# section, above any lines pull requests left there. The list stays tight,
# without a blank line after <line>, so prettier does not reflow it.
open_section() { # <file> <heading> <line>
awk -v h="$2" -v l="$3" '
!seen && $0 == "## Unreleased" { seen = 1; print; print ""; print h; print ""; print l; next }
seen == 1 { seen = 2; if ($0 == "") { held = 1; next } }
held { held = 0; if ($0 !~ /^- /) print "" }
{ print }
' "$1" >"$1.tmp" && mv "$1.tmp" "$1"
}

# Set the binding packages to the payjoin-ffi version. Each manifest is
# edited in place with sed and read back, as the Cargo.toml edit is, so a
# line that no longer looks the way this expects fails instead of being
# skipped. The lock files are edited the same way rather than regenerated,
# so the only change in them is the version.
bump_bindings() { # <old> <new>
local old="$1" new="$2" old_py new_py f line
# Python spells a -rc.N pre-release as rcN (PEP 440), in uv.lock too.
old_py="${old/-rc./rc}"
new_py="${new/-rc./rc}"

f=payjoin-ffi/python/pyproject.toml
sed -i "0,/^version = \"$old_py\"/s//version = \"$new_py\"/" "$f"
[ "$(sed -n 's/^version = "\(.*\)"$/\1/p' "$f" | head -1)" = "$new_py" ] ||
die "could not set version in $f"
echo " $f: version -> $new_py"

# The package's own entry: the version line that follows its name.
f=payjoin-ffi/python/uv.lock
sed -i "/^name = \"payjoin\"$/{n;s/^version = \"$old_py\"$/version = \"$new_py\"/}" "$f"
[ "$(grep -A1 '^name = "payjoin"$' "$f" | sed -n 's/^version = "\(.*\)"$/\1/p')" = "$new_py" ] ||
die "could not set version in $f"
echo " $f: version -> $new_py"

# The root entry of package.json and, in the lock, of packages[""].
for f in payjoin-ffi/javascript/package.json payjoin-ffi/javascript/package-lock.json; do
sed -i "/^ *\"name\": \"payjoin\",$/{n;s/^\( *\"version\": \"\)$old\"/\1$new\"/}" "$f"
[ "$(jq -r '[.version, .packages[""].version // .version] | unique | join(" ")' "$f")" = "$new" ] ||
die "could not set version in $f"
echo " $f: version -> $new"
done

f=payjoin-ffi/dart/pubspec.yaml
sed -i "s/^version: $old$/version: $new/" "$f"
[ "$(sed -n 's/^version: *//p' "$f")" = "$new" ] || die "could not set version in $f"
echo " $f: version -> $new"

f=payjoin-ffi/csharp/Payjoin.csproj
sed -i "s#<Version>$old</Version>#<Version>$new</Version>#" "$f"
[ "$(sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p' "$f" | head -1)" = "$new" ] ||
die "could not set Version in $f"
echo " $f: Version -> $new"

# Each language changelog opens the new section with a stock entry that
# points at the shared changelog, above any language-specific lines a
# pull request left under "## Unreleased". The URL is absolute because
# pub.dev renders Dart's changelog and cannot resolve ../CHANGELOG.md.
line="- Wraps payjoin-ffi $new; see its [changelog](https://github.com/payjoin/rust-payjoin/blob/payjoin-ffi-$new/payjoin-ffi/CHANGELOG.md)."
for f in payjoin-ffi/python/CHANGELOG.md payjoin-ffi/javascript/CHANGELOG.md payjoin-ffi/dart/CHANGELOG.md; do
if grep -qx '## Unreleased' "$f"; then
open_section "$f" "## [$new]" "$line"
echo " $f: '## Unreleased' -> '## [$new]', fresh Unreleased section added"
else
{ printf '## [%s]\n\n%s\n\n' "$new" "$line" && cat "$f"; } >"$f.tmp" && mv "$f.tmp" "$f"
echo " $f: '## [$new]' section added"
fi
{ grep -qxF -e "## [$new]" "$f" && grep -qxF -e "$line" "$f"; } ||
die "could not add the $new section to $f"
done
}

case " $RELEASE_CRATES payjoin-ffi " in
*" $crate "*) ;;
*) die "$crate is not a release crate ($RELEASE_CRATES)" ;;
*) die "$crate is not a release crate ($RELEASE_CRATES) or payjoin-ffi" ;;
esac
[[ $new =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] || die "$new is not a semver version"
if [ "$crate" = payjoin-ffi ]; then
# The rule bindings-version.sh enforces: pre-releases are -rc.N only.
[[ $new =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] ||
die "$new must be MAJOR.MINOR.PATCH, optionally followed by -rc.N"
else
[[ $new =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] || die "$new is not a semver version"
fi
[ -z "$(git status --porcelain --untracked-files=no)" ] || die "commit or discard your changes first"

RELEASE_METADATA="$(cargo_metadata)"
Expand All @@ -61,18 +149,45 @@ while IFS= read -r manifest; do
fi
done < <(cargo_metadata | jq -r '.packages[].manifest_path')

[ "$crate" != payjoin-ffi ] || bump_bindings "$old" "$new"

# The crate's changelog keeps its heading style: the binding changelogs
# bracket versions, the crates do not.
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 grep -q '^## \[' "$changelog"; then
heading="## [$new]"
else
heading="## $new"
fi
if [ "$crate" = payjoin-ffi ]; then
# The entry names the wrapped payjoin release first; the manifests no
# longer carry it, and the package docs point readers here for it.
line="- Wraps payjoin $(manifest_version payjoin)"
open_section "$changelog" "$heading" "$line"
grep -qxF -e "$line" "$changelog" || die "could not add '$line' to $changelog"
echo " $changelog: '## Unreleased' -> '$heading' opening with '$line', fresh Unreleased section added"
else
sed -i "0,/^## Unreleased$/s//## Unreleased\n\n$heading/" "$changelog"
echo " $changelog: '## Unreleased' -> '$heading', fresh Unreleased section added"
fi

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"
if [ "$crate" = payjoin-ffi ]; then
# Assigned first so a failing check stops the script under set -e.
agreed="$("$DIR/bindings-version.sh")"
echo "Binding packages agree at $agreed"
fi
echo
echo "Review the '## $new' section of $changelog, then commit as:"
if [ "$crate" = payjoin-ffi ]; then
echo "Review the '$heading' sections of $changelog and the language changelogs, then commit as:"
else
echo "Review the '$heading' section of $changelog, then commit as:"
fi
echo " git commit -am 'Bump $crate version to $new'"
20 changes: 17 additions & 3 deletions contrib/release/check-bump.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
# to the base commit, confirm the bump is consistent (check-invariants), the
# crate still publishes (cargo publish --dry-run), and the bump is large
# enough for the API changes since the base version (cargo semver-checks).
# No-ops when no release version changed. A sibling release crate not yet on
# crates.io is skipped, not failed, since a PR may bump two crates at once.
# A payjoin-ffi bump is checked for consistency only, including that every
# binding package carries its version (bindings-version), since it is not
# published to crates.io. No-ops when no release version changed. A sibling
# release crate not yet on crates.io is skipped, not failed, since a PR may
# bump two crates at once.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=contrib/release/crates.sh
Expand All @@ -31,12 +34,23 @@ changed=()
for crate in $RELEASE_CRATES; do
[ "$(version_at "$base" "$crate")" = "$(manifest_version "$crate")" ] || changed+=("$crate")
done
ffi=0
[ "$(version_at "$base" payjoin-ffi)" = "$(manifest_version payjoin-ffi)" ] || ffi=1

if [ "${#changed[@]}" -eq 0 ]; then
if [ "${#changed[@]}" -eq 0 ] && [ "$ffi" -eq 0 ]; then
echo "No release crate version changed"
exit 0
fi

if [ "$ffi" -eq 1 ]; then
echo "Version changed for: payjoin-ffi"
"$DIR/check-invariants.sh" payjoin-ffi
# Assigned first so a failing check stops the script under set -e.
agreed="$("$DIR/bindings-version.sh")"
echo "Binding packages agree at $agreed"
fi
[ "${#changed[@]}" -gt 0 ] || exit 0

echo "Version changed for: ${changed[*]}"
"$DIR/check-invariants.sh" "${changed[@]}"

Expand Down
Loading
Loading