From 927cff7efd7b35b09ec3e9671221fbcd82db6eaf Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 7 Oct 2026 21:03:15 +0000 Subject: [PATCH 1/3] Pin Dart packages to their release commit The published native wrapper could use a different Rust revision from its generated bindings. Stage a package pinned to the generating checkout without development Cargo overlays or local lockfiles. --- .github/workflows/dart.yml | 6 +++--- flake.nix | 1 + payjoin-ffi/dart/.pubignore | 1 + payjoin-ffi/dart/CONTRIBUTING.md | 14 ++++++++++---- payjoin-ffi/dart/contrib/prepare-publish.sh | 21 +++++++++++++++++---- payjoin-ffi/dart/contrib/publish.sh | 15 ++++++++++++--- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index d8dc44124..092639f77 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -50,7 +50,7 @@ jobs: - name: Set up nix uses: ./.github/actions/setup-nix - name: Generate production bindings and dry-run publish - run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh + run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh "$RUNNER_TEMP/payjoin-pub" verify-tag: name: "Verify release tag" @@ -96,7 +96,7 @@ jobs: version: ${{ steps.pubspec.outputs.version }} - name: Prepare the package - run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh + run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh "$RUNNER_TEMP/payjoin-pub" - name: Publish to pub.dev - run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/publish.sh + run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/publish.sh "$RUNNER_TEMP/payjoin-pub" diff --git a/flake.nix b/flake.nix index afee40989..15618d502 100644 --- a/flake.nix +++ b/flake.nix @@ -359,6 +359,7 @@ bzip2 curl jq + rsync ] ++ lib.optionals pkgs.stdenv.isLinux [ pkg-config diff --git a/payjoin-ffi/dart/.pubignore b/payjoin-ffi/dart/.pubignore index c8c635895..d0ac83c8c 100644 --- a/payjoin-ffi/dart/.pubignore +++ b/payjoin-ffi/dart/.pubignore @@ -16,6 +16,7 @@ scripts/ # Cargo build output native/target/ +native/Cargo.lock # Local-only test helper re-export lib/test_utils.dart diff --git a/payjoin-ffi/dart/CONTRIBUTING.md b/payjoin-ffi/dart/CONTRIBUTING.md index e70533173..783454665 100644 --- a/payjoin-ffi/dart/CONTRIBUTING.md +++ b/payjoin-ffi/dart/CONTRIBUTING.md @@ -50,10 +50,16 @@ the `Build and Test Dart` workflow regenerates the production bindings and validates the archive with a publish dry run ([`contrib/prepare-publish.sh`](contrib/prepare-publish.sh)). -1. Point the `payjoin-ffi` dependency in `native/Cargo.toml` at the commit - tagged for the `payjoin` release being wrapped. Consumers build from that - revision. `.cargo/config.toml` redirects it to the local workspace for - development only, and `.pubignore` withholds that file from the archive. +1. The preparation script stages a separate package and pins its Rust + dependency to the checkout used to generate production bindings. The + development pin in `native/Cargo.toml` is replaced only in that staging + directory. Cargo overlays and local lockfiles are excluded. Both the + publish dry run and publication use the staged package. For local checks: + + ```shell + nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh /tmp/payjoin-pub + ``` + 2. Set the version in `pubspec.yaml` and describe the consumer-visible changes under a matching heading in `CHANGELOG.md`. 3. Confirm every `Build and Test Dart` job is green on the release commit diff --git a/payjoin-ffi/dart/contrib/prepare-publish.sh b/payjoin-ffi/dart/contrib/prepare-publish.sh index 1fd533e18..188be65c6 100755 --- a/payjoin-ffi/dart/contrib/prepare-publish.sh +++ b/payjoin-ffi/dart/contrib/prepare-publish.sh @@ -1,5 +1,11 @@ #!/usr/bin/env bash set -euo pipefail +[ "$#" -eq 1 ] || { + echo "usage: prepare-publish.sh " >&2 + exit 1 +} +# Resolve before changing directories; the destination must not exist. +stage="$(realpath -m "$1")" # Prepare the package for publishing to pub.dev and verify the archive with # a dry run. The archive ships Dart source plus the native/ wrapper crate; @@ -20,10 +26,17 @@ cd "$REPO_ROOT/payjoin-ffi/dart" echo "==> Generating production FFI bindings..." PAYJOIN_FFI_FEATURES="" bash ./scripts/generate_bindings.sh -# A Cargo.lock resolved against the .cargo/config.toml path overlay would -# hand consumers a dependency graph they cannot reproduce. -echo "==> Cleaning nested Cargo.lock..." -rm -f native/Cargo.lock +revision="$(git rev-parse HEAD)" +mkdir "$stage" +# Include generated bindings while excluding development Cargo overlays and +# lockfiles, then pin the native crate to the checkout that generated them. +rsync -a --exclude=.git --exclude-from=.pubignore ./ "$stage/" +sed -i "s/rev = \"[0-9a-f]*\"/rev = \"$revision\"/" "$stage/native/Cargo.toml" +grep -Fq "rev = \"$revision\"" "$stage/native/Cargo.toml" || { + echo "Could not pin the staged native crate to $revision" >&2 + exit 1 +} +cd "$stage" echo "==> Verifying the publish archive..." # The dry run exits 65 whenever validation reports anything, and one diff --git a/payjoin-ffi/dart/contrib/publish.sh b/payjoin-ffi/dart/contrib/publish.sh index e6917d551..3a6f7c877 100755 --- a/payjoin-ffi/dart/contrib/publish.sh +++ b/payjoin-ffi/dart/contrib/publish.sh @@ -1,5 +1,16 @@ #!/usr/bin/env bash set -euo pipefail +[ "$#" -eq 1 ] || { + echo "usage: publish.sh " >&2 + exit 1 +} +revision="$(git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse HEAD)" +cd "$1" +# Publish only what prepare-publish.sh staged for this checkout. +grep -Fq "rev = \"$revision\"" native/Cargo.toml || { + echo "Staged native/Cargo.toml must pin the release checkout $revision" >&2 + exit 1 +} # Publish the package to pub.dev via automated publishing (OIDC). # `dart pub publish` has no non-interactive authentication mode: without a @@ -9,8 +20,7 @@ set -euo pipefail # short-lived token that pub.dev verifies against the repository, tag # pattern, and environment configured on the package's admin page. This is # the same exchange the dart-lang/setup-dart action performs; we install -# Dart through nix, so it has to happen here instead. Run -# prepare-publish.sh first to generate the bindings that get packed. +# Dart through nix, so it has to happen here instead. : "${ACTIONS_ID_TOKEN_REQUEST_URL:?must run in a GitHub Actions job with id-token: write}" : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?must run in a GitHub Actions job with id-token: write}" @@ -24,5 +34,4 @@ PUB_TOKEN="$( export PUB_TOKEN dart pub token add https://pub.dev --env-var PUB_TOKEN -cd "$(dirname "$0")/.." dart pub publish --force From c630a5bbf6d748dcb315602c69299de4c2a7de35 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 7 Oct 2026 21:03:25 +0000 Subject: [PATCH 2/3] Align binding versions at 0.25.0 The Python, JavaScript, C#, and Dart packages each carried their own version sequence, so releasing all bindings took four tags and four approvals. Move every package to the payjoin-ffi version so one tag can name them all. 0.25.0 is above every existing sequence, so no registry sees a version go backwards. Keep the resolved dependency graphs unchanged. Let the Dart publish dry run proceed over pub's reminder that versions were skipped, since the jump is deliberate. --- Cargo-minimal.lock | 2 +- Cargo-recent.lock | 2 +- payjoin-ffi/CHANGELOG.md | 8 ++++++++ payjoin-ffi/Cargo.toml | 2 +- payjoin-ffi/csharp/Payjoin.csproj | 2 +- payjoin-ffi/dart/CHANGELOG.md | 6 ++++++ payjoin-ffi/dart/contrib/prepare-publish.sh | 4 ++++ payjoin-ffi/dart/pubspec.yaml | 2 +- payjoin-ffi/javascript/CHANGELOG.md | 6 ++++++ payjoin-ffi/javascript/package-lock.json | 4 ++-- payjoin-ffi/javascript/package.json | 4 ++-- payjoin-ffi/python/CHANGELOG.md | 6 ++++++ payjoin-ffi/python/pyproject.toml | 2 +- payjoin-ffi/python/uv.lock | 2 +- 14 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 0791c2824..77e5a70c3 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "payjoin-ffi" -version = "0.24.0" +version = "0.25.0" dependencies = [ "async-trait", "bdk_wallet", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index b4068625f..690b77535 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -2568,7 +2568,7 @@ dependencies = [ [[package]] name = "payjoin-ffi" -version = "0.24.0" +version = "0.25.0" dependencies = [ "async-trait", "bdk_wallet", diff --git a/payjoin-ffi/CHANGELOG.md b/payjoin-ffi/CHANGELOG.md index d762325c7..78ea889c6 100644 --- a/payjoin-ffi/CHANGELOG.md +++ b/payjoin-ffi/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.25.0] + +- Publish the Python, JavaScript, C#, and Dart packages from one signed + `payjoin-ffi` tag at this common version. Each package jumps from its + own sequence (Python and JavaScript 0.3.0, C# 0.2.0, Dart + 0.3.0+payjoin-1.2.0) with no API changes. +- Wraps payjoin 1.2.0, unchanged. + ## [0.24.0] ### Session Persister diff --git a/payjoin-ffi/Cargo.toml b/payjoin-ffi/Cargo.toml index c89310d3a..c4c8df83d 100644 --- a/payjoin-ffi/Cargo.toml +++ b/payjoin-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "payjoin-ffi" -version = "0.24.0" +version = "0.25.0" edition = "2024" license = "MIT OR Apache-2.0" exclude = ["tests"] diff --git a/payjoin-ffi/csharp/Payjoin.csproj b/payjoin-ffi/csharp/Payjoin.csproj index 66af6a92f..2bcad112d 100644 --- a/payjoin-ffi/csharp/Payjoin.csproj +++ b/payjoin-ffi/csharp/Payjoin.csproj @@ -7,7 +7,7 @@ true true Payjoin - 0.2.0+payjoin-1.2.0 + 0.25.0+payjoin-1.2.0 Payjoin Payjoin Dev Kit Contributors C# bindings for payjoin-ffi, generated from rust-payjoin via UniFFI. diff --git a/payjoin-ffi/dart/CHANGELOG.md b/payjoin-ffi/dart/CHANGELOG.md index 6c209115a..3179933dc 100644 --- a/payjoin-ffi/dart/CHANGELOG.md +++ b/payjoin-ffi/dart/CHANGELOG.md @@ -1,3 +1,9 @@ +## [0.25.0+payjoin-1.2.0] + +- Version aligned with payjoin-ffi 0.25.0. Releases now share one tag + and the [payjoin-ffi changelog](../CHANGELOG.md). No API changes since + 0.3.0+payjoin-1.2.0. + ## [0.3.0+payjoin-1.2.0] - Bindings for payjoin-1.2.0, which adds the non-blocking receive diff --git a/payjoin-ffi/dart/contrib/prepare-publish.sh b/payjoin-ffi/dart/contrib/prepare-publish.sh index 188be65c6..5a57e6706 100755 --- a/payjoin-ffi/dart/contrib/prepare-publish.sh +++ b/payjoin-ffi/dart/contrib/prepare-publish.sh @@ -52,6 +52,10 @@ if [[ $status -ne 0 && $status -ne 65 ]]; then exit "$status" fi unexpected="$(grep '^\* ' <<<"$report" | grep -v "^\* \`dart analyze\` found" || true)" +# Skipping ahead of the last published version is a release decision made +# when the shared binding version is bumped, so pub's reminder of the +# previous version is not a finding. +unexpected="$(grep -v '^\* The previous version is ' <<<"$unexpected" || true)" if [[ -n $unexpected ]]; then echo "Unexpected validation findings; fix them before publishing:" >&2 printf '%s\n' "$unexpected" >&2 diff --git a/payjoin-ffi/dart/pubspec.yaml b/payjoin-ffi/dart/pubspec.yaml index e84b56a02..037557182 100644 --- a/payjoin-ffi/dart/pubspec.yaml +++ b/payjoin-ffi/dart/pubspec.yaml @@ -1,6 +1,6 @@ name: payjoin description: Dart bindings for payjoin -version: 0.3.0+payjoin-1.2.0 +version: 0.25.0+payjoin-1.2.0 homepage: https://github.com/payjoin/rust-payjoin repository: https://github.com/payjoin/rust-payjoin diff --git a/payjoin-ffi/javascript/CHANGELOG.md b/payjoin-ffi/javascript/CHANGELOG.md index 9631fee1c..a643596cd 100644 --- a/payjoin-ffi/javascript/CHANGELOG.md +++ b/payjoin-ffi/javascript/CHANGELOG.md @@ -1,3 +1,9 @@ +## [0.25.0] + +- Version aligned with payjoin-ffi 0.25.0. Releases now share one tag + and the [payjoin-ffi changelog](../CHANGELOG.md). No API changes since + 0.3.0. + ## [0.3.0] - Bindings for payjoin-1.2.0, which adds the non-blocking receive diff --git a/payjoin-ffi/javascript/package-lock.json b/payjoin-ffi/javascript/package-lock.json index 77bf6f1c1..38c75753f 100644 --- a/payjoin-ffi/javascript/package-lock.json +++ b/payjoin-ffi/javascript/package-lock.json @@ -1,12 +1,12 @@ { "name": "payjoin", - "version": "0.3.0", + "version": "0.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "payjoin", - "version": "0.3.0", + "version": "0.25.0", "dependencies": { "@ubjs/core": "^0.31.0-3", "uniffi-bindgen-react-native": "github:spacebear21/uniffi-bindgen-react-native#d10f04d83299" diff --git a/payjoin-ffi/javascript/package.json b/payjoin-ffi/javascript/package.json index 6f76422b6..2b6e40e9a 100644 --- a/payjoin-ffi/javascript/package.json +++ b/payjoin-ffi/javascript/package.json @@ -1,7 +1,7 @@ { "name": "payjoin", - "version": "0.3.0", - "releaseTag": "0.3.0+payjoin-1.2.0", + "version": "0.25.0", + "releaseTag": "0.25.0+payjoin-1.2.0", "description": "JavaScript/WASM bindings for rust-payjoin (EXPERIMENTAL)", "scripts": { "ubrn:nodejs": "ubrn build web --release --and-generate --config ubrn.nodejs.config.yaml", diff --git a/payjoin-ffi/python/CHANGELOG.md b/payjoin-ffi/python/CHANGELOG.md index d14b0b563..a3423e325 100644 --- a/payjoin-ffi/python/CHANGELOG.md +++ b/payjoin-ffi/python/CHANGELOG.md @@ -1,3 +1,9 @@ +## [0.25.0] + +- Version aligned with payjoin-ffi 0.25.0. Releases now share one tag + and the [payjoin-ffi changelog](../CHANGELOG.md). No API changes since + 0.3.0. + ## [0.3.0] - Bindings for payjoin-1.2.0, which adds the non-blocking receive diff --git a/payjoin-ffi/python/pyproject.toml b/payjoin-ffi/python/pyproject.toml index 33d8f00bc..4134342ab 100644 --- a/payjoin-ffi/python/pyproject.toml +++ b/payjoin-ffi/python/pyproject.toml @@ -8,7 +8,7 @@ description = "The Python language bindings for the Payjoin Dev Kit" readme = "README.md" requires-python = ">=3.10" license = "MIT" -version = "0.3.0" +version = "0.25.0" dependencies = ["httpx>=0.28.1,<1.0"] [tool.setuptools] diff --git a/payjoin-ffi/python/uv.lock b/payjoin-ffi/python/uv.lock index bbf696091..f644d2e19 100644 --- a/payjoin-ffi/python/uv.lock +++ b/payjoin-ffi/python/uv.lock @@ -82,7 +82,7 @@ wheels = [ [[package]] name = "payjoin" -version = "0.3.0" +version = "0.25.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From 00d9588acc5ec0784c301c3376ee9dae77b9a8da Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 13 Oct 2026 17:19:46 +0000 Subject: [PATCH 3/3] Release every language binding from one FFI tag Each binding had its own tag, workflow, approval, and GitHub release, so shipping all four meant four signed tags and four approvals, and nothing checked that they wrapped the same payjoin-ffi commit. Push one signed payjoin-ffi-+payjoin- tag instead. A new workflow verifies the tag and that every manifest carries the tagged version, runs each language's existing build and smoke checks through workflow_call, then publishes each registry from its own job. The four publish jobs wait on every build so they reach the release environment together and one approval covers them; a registry failure is retried with "Re-run failed jobs" against the same tested artifacts, which is why there is no separate recovery path. One GitHub release carries every package and a single SHA256SUMS, with the canonical payjoin-ffi changelog section as its body. Keep the publish jobs in the triggered workflow rather than the language workflows because PyPI's trusted publisher rejects reusable workflows. Replace the per-language release-version readers with bindings-version.sh, which tag.sh and CI both run, and teach the invariants and changelog helpers the bracketed headings the binding changelogs use so payjoin-ffi passes the same checks as the crates. tag.sh also refuses to tag bindings whose wrapped core is not on crates.io, since Dart consumers build against it. --- .github/ISSUE_TEMPLATE/minor-release.md | 13 +- .github/actions/verify-tag-version/action.yml | 48 --- .github/workflows/bindings-release.yml | 311 ++++++++++++++++++ .github/workflows/csharp.yml | 93 +----- .github/workflows/dart.yml | 61 +--- .github/workflows/javascript.yml | 84 +---- .github/workflows/kotlin.yml | 1 + .github/workflows/python.yml | 79 +---- .github/workflows/release-assets.yml | 91 ----- .github/workflows/verify-tag-hygiene.yml | 31 -- contrib/release/bindings-version.sh | 63 ++++ contrib/release/check-invariants.sh | 3 +- contrib/release/extract-changelog.sh | 9 +- contrib/release/tag.sh | 74 ++--- payjoin-ffi/README.md | 4 +- payjoin-ffi/RELEASING.md | 86 +++++ payjoin-ffi/csharp/RELEASING.md | 75 ++--- payjoin-ffi/csharp/contrib/release-version.sh | 9 - payjoin-ffi/dart/CONTRIBUTING.md | 37 +-- payjoin-ffi/dart/contrib/release-version.sh | 9 - payjoin-ffi/javascript/RELEASING.md | 73 ++-- .../javascript/contrib/release-version.sh | 8 - payjoin-ffi/python/RELEASING.md | 59 +--- payjoin-ffi/python/contrib/release-version.sh | 15 - 24 files changed, 585 insertions(+), 751 deletions(-) delete mode 100644 .github/actions/verify-tag-version/action.yml create mode 100644 .github/workflows/bindings-release.yml delete mode 100644 .github/workflows/release-assets.yml delete mode 100644 .github/workflows/verify-tag-hygiene.yml create mode 100755 contrib/release/bindings-version.sh create mode 100644 payjoin-ffi/RELEASING.md delete mode 100755 payjoin-ffi/csharp/contrib/release-version.sh delete mode 100755 payjoin-ffi/dart/contrib/release-version.sh delete mode 100755 payjoin-ffi/javascript/contrib/release-version.sh delete mode 100755 payjoin-ffi/python/contrib/release-version.sh diff --git a/.github/ISSUE_TEMPLATE/minor-release.md b/.github/ISSUE_TEMPLATE/minor-release.md index d77f7000f..a45dba8ef 100644 --- a/.github/ISSUE_TEMPLATE/minor-release.md +++ b/.github/ISSUE_TEMPLATE/minor-release.md @@ -29,6 +29,10 @@ 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, @@ -51,10 +55,11 @@ changelog line. The bump PR is not edited for it. #### 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. +- [ ] Confirm the prepared bindings bump is merged 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 + `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 crates.io. diff --git a/.github/actions/verify-tag-version/action.yml b/.github/actions/verify-tag-version/action.yml deleted file mode 100644 index 437de3c5a..000000000 --- a/.github/actions/verify-tag-version/action.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Verify tag version -description: > - Check that the pushed tag is the expected prefix followed by the version - about to be published, refusing to publish on any mismatch. Tags carry - the full `{version}+payjoin-{version}` string; when the packed version - has no build metadata (npm, PyPI, and NuGet artifacts cannot carry it), - the tag's metadata is stripped before comparing. -inputs: - tag-prefix: - description: Expected tag prefix, e.g. payjoin-csharp- - required: true - version: - description: Version of the artifact or package about to be published - required: true -outputs: - version: - description: The version the tag calls for - value: ${{ steps.check.outputs.version }} -runs: - using: composite - steps: - - name: Compare tag against version - id: check - shell: bash - env: - TAG: ${{ github.ref_name }} - PREFIX: ${{ inputs.tag-prefix }} - VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - if [[ $TAG != "$PREFIX"* ]]; then - echo "::error::tag $TAG does not start with $PREFIX; refusing to publish" - exit 1 - fi - tag_version="${TAG#"$PREFIX"}" - compare_version="$tag_version" - # A packed version without build metadata comes from a registry - # that cannot represent it, so the tag is compared with its - # metadata stripped; a packed version that carries metadata must - # match the tag exactly. - if [[ $VERSION != *+* ]]; then - compare_version="${tag_version%%+*}" - fi - if [[ $compare_version != "$VERSION" ]]; then - echo "::error::tag $TAG implies version $compare_version but the packed version is $VERSION; refusing to publish" - exit 1 - fi - echo "version=$tag_version" >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/bindings-release.yml b/.github/workflows/bindings-release.yml new file mode 100644 index 000000000..18e782f97 --- /dev/null +++ b/.github/workflows/bindings-release.yml @@ -0,0 +1,311 @@ +name: Release language bindings + +# Pushing a signed `payjoin-ffi-+payjoin-` 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 +# releases them all; a registry failure is retried with "Re-run failed +# jobs" on the same run, reusing the tested artifacts. + +on: + push: + tags: + - "payjoin-ffi-[0-9]*" + +# Keep two attempts at the same tag from publishing concurrently. +concurrency: + group: bindings-release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + verify-tag: + name: Verify release tag + runs-on: ubuntu-26.04 + outputs: + version: ${{ steps.meta.outputs.version }} + prerelease: ${{ steps.meta.outputs.prerelease }} + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Fetch master + run: git fetch --no-tags origin +refs/heads/master:refs/remotes/origin/master + - name: Set up nix + uses: ./.github/actions/setup-nix + - name: Verify tag hygiene + env: + TAG: ${{ github.ref_name }} + run: nix --quiet develop .#release -c ./contrib/release/verify-tag-hygiene.sh "$TAG" + - name: Verify every binding carries the tagged version + id: meta + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + version="$(nix --quiet develop .#release -c ./contrib/release/bindings-version.sh --tag "$TAG")" + nix --quiet develop .#release -c ./contrib/release/check-invariants.sh payjoin-ffi + source contrib/release/crates.sh + { + echo "version=$version" + # A hyphen in the version proper marks a pre-release; the + # +payjoin- build metadata never does. + 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-}" + # 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" + exit 1 + } + + python: + needs: verify-tag + uses: ./.github/workflows/python.yml + javascript: + needs: verify-tag + uses: ./.github/workflows/javascript.yml + csharp: + needs: verify-tag + uses: ./.github/workflows/csharp.yml + dart: + needs: verify-tag + uses: ./.github/workflows/dart.yml + kotlin: + needs: verify-tag + uses: ./.github/workflows/kotlin.yml + + publish-python: + name: Publish to PyPI + needs: [python, javascript, csharp, dart, kotlin] + runs-on: ubuntu-26.04 + environment: release + permissions: + id-token: write # OIDC: PyPI trusted publishing and its PEP 740 attestations + attestations: write # actions/attest-build-provenance writes the attestation + steps: + - name: Download wheels + uses: actions/download-artifact@v4 + with: + pattern: payjoin-python-wheel-* + merge-multiple: true + path: dist + - name: Check both wheels are present + run: | + set -euo pipefail + shopt -s nullglob + pkgs=(dist/payjoin-*.whl) + if [ "${#pkgs[@]}" -ne 2 ]; then + echo "::error::expected exactly two wheels in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" + exit 1 + fi + - name: Attest build provenance + # A consumer runs: gh attestation verify .whl -R payjoin/rust-payjoin + uses: actions/attest-build-provenance@v4 + with: + subject-path: dist/*.whl + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist + + publish-javascript: + name: Publish to npmjs.com + needs: [verify-tag, python, javascript, csharp, dart, kotlin] + runs-on: ubuntu-26.04 + environment: release + permissions: + id-token: write # OIDC: npm trusted publishing and its provenance statement + attestations: write # actions/attest writes the attestation + steps: + - name: Download npm package + uses: actions/download-artifact@v4 + with: + name: payjoin-javascript-npm-package + path: dist + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - name: Update npm + run: | + npm install -g npm@11 + npm --version + - name: Locate the tarball + id: locate + run: | + set -euo pipefail + shopt -s nullglob + pkgs=(dist/payjoin-*.tgz) + if [ "${#pkgs[@]}" -ne 1 ]; then + echo "::error::expected exactly one .tgz in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" + exit 1 + fi + echo "tarball=${pkgs[0]}" >>"$GITHUB_OUTPUT" + - name: Attest build provenance + # A consumer runs: gh attestation verify .tgz -R payjoin/rust-payjoin + uses: actions/attest@v4 + with: + subject-path: ${{ steps.locate.outputs.tarball }} + - name: Publish to npmjs.com + env: + TARBALL: ${{ steps.locate.outputs.tarball }} + # Pre-releases go to the `next` dist-tag so `npm install payjoin` + # keeps resolving the latest stable version. + CHANNEL: ${{ needs.verify-tag.outputs.prerelease == 'true' && 'next' || 'latest' }} + run: npm publish "./$TARBALL" --tag "$CHANNEL" + + publish-csharp: + name: Publish to nuget.org + needs: [python, javascript, csharp, dart, kotlin] + runs-on: ubuntu-26.04 + environment: release + permissions: + id-token: write # OIDC: used by both NuGet/login and actions/attest + attestations: write # actions/attest writes the attestation + steps: + - name: Download NuGet package + uses: actions/download-artifact@v4 + with: + name: payjoin-csharp-nuget-package + path: dist + - name: Install .NET 10 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + - name: Locate the package + id: locate + run: | + set -euo pipefail + shopt -s nullglob + pkgs=(dist/*.nupkg) + if [ "${#pkgs[@]}" -ne 1 ]; then + echo "::error::expected exactly one .nupkg in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" + exit 1 + fi + echo "nupkg=${pkgs[0]}" >>"$GITHUB_OUTPUT" + - name: Attest build provenance + # Attesting the .nupkg covers every RID native lib inside it; a consumer + # runs: gh attestation verify .nupkg -R payjoin/rust-payjoin + uses: actions/attest@v4 + with: + subject-path: ${{ steps.locate.outputs.nupkg }} + - name: NuGet login (OIDC -> short-lived API key) + id: login + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} + - name: Push to nuget.org + env: + NUPKG: ${{ steps.locate.outputs.nupkg }} + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: | + dotnet nuget push "$NUPKG" \ + --api-key "$NUGET_API_KEY" \ + --source https://api.nuget.org/v3/index.json \ + --no-symbols \ + --skip-duplicate + + publish-dart: + name: Publish to pub.dev + needs: [python, javascript, csharp, dart, kotlin] + runs-on: ubuntu-26.04 + environment: release + permissions: + id-token: write # OIDC: pub.dev automated publishing + contents: read + steps: + - name: Checkout repo + uses: actions/checkout@v6 + - name: Set up nix + uses: ./.github/actions/setup-nix + - name: Download the staged package + uses: actions/download-artifact@v4 + with: + name: payjoin-dart-pub-package + path: dist + - name: Extract the staged package + run: | + mkdir -p "$RUNNER_TEMP/payjoin-pub" + tar -xzf dist/payjoin-dart.tar.gz -C "$RUNNER_TEMP/payjoin-pub" + - name: Publish to pub.dev + run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/publish.sh "$RUNNER_TEMP/payjoin-pub" + + github-release: + name: Create GitHub release + needs: + [ + verify-tag, + publish-python, + publish-javascript, + publish-csharp, + publish-dart, + ] + runs-on: ubuntu-26.04 + permissions: + contents: write # create the Release for this tag and upload assets + env: + VERSION: ${{ needs.verify-tag.outputs.version }} + steps: + - name: Checkout repo + uses: actions/checkout@v6 + - name: Download every package + uses: actions/download-artifact@v4 + with: + pattern: payjoin-* + merge-multiple: true + path: dist + - name: Check the asset set is complete + run: | + set -euo pipefail + shopt -s nullglob + wheels=(dist/*.whl) + npm=(dist/*.tgz) + nuget=(dist/*.nupkg) + if [[ ${#wheels[@]} != 2 || ${#npm[@]} != 1 || ${#nuget[@]} != 1 || ! -f dist/payjoin-dart.tar.gz ]]; then + echo "::error::expected two wheels, one npm tarball, one NuGet package, and the Dart archive in dist/" + ls dist + exit 1 + fi + - name: Generate SHA256SUMS + # The NuGet package's native libraries are listed individually so + # each can be verified on its own. A maintainer's GPG signature over + # SHA256SUMS is made locally and uploaded as SHA256SUMS.asc; no + # signing key reaches a runner. + env: + ARCHIVE_GLOBS: "*.nupkg" + MEMBER_GLOBS: runtimes/*/native/* + run: | + ./contrib/release/generate-sha256sums.sh dist + cat dist/SHA256SUMS + - name: Extract changelog section + id: changelog + run: | + { + echo "body<>"$GITHUB_OUTPUT" + - name: Create release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: ${{ steps.changelog.outputs.body }} + prerelease: ${{ needs.verify-tag.outputs.prerelease }} + fail_on_unmatched_files: true + files: dist/* diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 8e4846460..418775d9f 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -12,9 +12,7 @@ on: # the flake change this workflow's environment. - flake.nix - flake.lock - push: - tags: - - "payjoin-csharp-[0-9]*" + workflow_call: jobs: build-csharp-and-test-unix: @@ -252,92 +250,3 @@ jobs: # 'auto' derives the version from the packed artifact, so Payjoin.csproj # stays the only place the package version is maintained. run: bash ./scripts/smoke_nuget_package.sh artifacts/packages auto ${{ matrix.rid }} - - verify-tag: - name: "Verify release tag" - if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') - permissions: - contents: read - uses: ./.github/workflows/verify-tag-hygiene.yml - - publish-nuget: - name: "Publish to nuget.org (trusted publishing / OIDC)" - runs-on: ubuntu-latest - needs: [pack-nuget, smoke-nuget, verify-tag] - if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') - environment: release - permissions: - id-token: write # OIDC: used by BOTH NuGet/login and actions/attest - attestations: write # actions/attest writes the attestation - contents: read # needed only to check out the in-repo verify-tag-version action - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download packed NuGet package - uses: actions/download-artifact@v4 - with: - name: payjoin-csharp-nuget-package - path: dist - - - name: Install .NET 10 SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "10.0.x" - - - name: Locate packed artifact - id: locate - shell: bash - run: | - set -euo pipefail - shopt -s nullglob - pkgs=(dist/*.nupkg) - if [ "${#pkgs[@]}" -ne 1 ]; then - echo "::error::expected exactly one .nupkg in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" - exit 1 - fi - # Payjoin.0.24.0-preview.1.nupkg -> 0.24.0-preview.1 - name="$(basename "${pkgs[0]}" .nupkg)" - echo "nupkg=${pkgs[0]}" >> "$GITHUB_OUTPUT" - echo "version=${name#Payjoin.}" >> "$GITHUB_OUTPUT" - - - name: Verify tag matches packed artifact version - id: verify - uses: ./.github/actions/verify-tag-version - with: - tag-prefix: payjoin-csharp- - version: ${{ steps.locate.outputs.version }} - - - name: Attest build provenance (nupkg) - # Attesting the .nupkg covers every RID native lib inside it; a consumer - # runs: gh attestation verify .nupkg -R payjoin/rust-payjoin - uses: actions/attest@v4 - with: - subject-path: ${{ steps.locate.outputs.nupkg }} - - - name: NuGet login (OIDC -> short-lived API key) - id: login - uses: NuGet/login@v1 - with: - user: ${{ secrets.NUGET_USER }} - - - name: Push to nuget.org - run: | - dotnet nuget push "${{ steps.locate.outputs.nupkg }}" \ - --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \ - --source https://api.nuget.org/v3/index.json \ - --no-symbols \ - --skip-duplicate - - github-release: - name: "Attach nupkg + SHA256SUMS to the GitHub release" - needs: [publish-nuget] - if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') - permissions: - contents: write # create/update the Release for this tag and upload assets - uses: ./.github/workflows/release-assets.yml - with: - artifact-pattern: payjoin-csharp-nuget-package - tag-prefix: payjoin-csharp- - archive-globs: "*.nupkg" - archive-member-globs: runtimes/*/native/* diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 092639f77..9ea38d7b0 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -12,9 +12,7 @@ on: # change this workflow's environment. - flake.nix - flake.lock - push: - tags: - - "payjoin-dart-[0-9]*" + workflow_call: jobs: build-dart-and-test: @@ -51,52 +49,13 @@ jobs: uses: ./.github/actions/setup-nix - name: Generate production bindings and dry-run publish run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh "$RUNNER_TEMP/payjoin-pub" - - verify-tag: - name: "Verify release tag" - if: startsWith(github.ref, 'refs/tags/payjoin-dart-') - permissions: - contents: read - uses: ./.github/workflows/verify-tag-hygiene.yml - - publish-pub: - name: "Publish to pub.dev (automated publishing / OIDC)" - runs-on: ubuntu-26.04 - needs: [build-dart-and-test, verify-pub-package, verify-tag] - if: startsWith(github.ref, 'refs/tags/payjoin-dart-') - environment: release - permissions: - id-token: write # OIDC: pub.dev automated publishing - contents: read - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: "Use cache" - uses: Swatinem/rust-cache@v2 - - name: Set up nix - uses: ./.github/actions/setup-nix - - - name: Read pubspec version - id: pubspec - shell: bash - run: | - set -euo pipefail - version="$(sed -n 's/^version: *//p' payjoin-ffi/dart/pubspec.yaml)" - if [ -z "$version" ]; then - echo "::error::could not read version from pubspec.yaml" - exit 1 - fi - echo "version=$version" >>"$GITHUB_OUTPUT" - - - name: Verify tag matches pubspec version - uses: ./.github/actions/verify-tag-version + # The release workflow publishes this exact stage, so it is archived + # rather than regenerated in the publish job. + - name: Archive the staged package + run: tar -czf "$RUNNER_TEMP/payjoin-dart.tar.gz" -C "$RUNNER_TEMP/payjoin-pub" . + - name: Upload the staged package + uses: actions/upload-artifact@v4 with: - tag-prefix: payjoin-dart- - version: ${{ steps.pubspec.outputs.version }} - - - name: Prepare the package - run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh "$RUNNER_TEMP/payjoin-pub" - - - name: Publish to pub.dev - run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/publish.sh "$RUNNER_TEMP/payjoin-pub" + name: payjoin-dart-pub-package + path: ${{ runner.temp }}/payjoin-dart.tar.gz + if-no-files-found: error diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index 845fcbe9c..2d5ff13b4 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -12,9 +12,7 @@ on: # change this workflow's environment. - flake.nix - flake.lock - push: - tags: - - "payjoin-javascript-[0-9]*" + workflow_call: jobs: build-js-and-test: @@ -90,83 +88,3 @@ jobs: payjoin.Url.parse("https://example.com"); console.log("smoke ok"); ' - - verify-tag: - name: "Verify release tag" - if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') - permissions: - contents: read - uses: ./.github/workflows/verify-tag-hygiene.yml - - publish-npm: - name: "Publish to npmjs.com (trusted publishing / OIDC)" - runs-on: ubuntu-26.04 - needs: [build-js-and-test, pack-npm, smoke-npm, verify-tag] - if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') - environment: release - permissions: - id-token: write # OIDC: npm trusted publishing and its provenance statement - attestations: write # actions/attest writes the attestation - contents: read # needed only to check out the in-repo verify-tag-version action - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download npm package - uses: actions/download-artifact@v4 - with: - name: payjoin-javascript-npm-package - path: dist - - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: "24" - registry-url: https://registry.npmjs.org - - - name: Update npm - run: | - npm install -g npm@11 - npm --version - - - name: Locate packed artifact - id: locate - shell: bash - run: | - set -euo pipefail - shopt -s nullglob - pkgs=(dist/payjoin-*.tgz) - if [ "${#pkgs[@]}" -ne 1 ]; then - echo "::error::expected exactly one .tgz in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" - exit 1 - fi - # payjoin-0.1.1.tgz -> 0.1.1 - name="$(basename "${pkgs[0]}" .tgz)" - echo "tarball=${pkgs[0]}" >> "$GITHUB_OUTPUT" - echo "version=${name#payjoin-}" >> "$GITHUB_OUTPUT" - - - name: Verify tag matches packed artifact version - uses: ./.github/actions/verify-tag-version - with: - tag-prefix: payjoin-javascript- - version: ${{ steps.locate.outputs.version }} - - - name: Attest build provenance (tarball) - # A consumer runs: gh attestation verify .tgz -R payjoin/rust-payjoin - uses: actions/attest@v4 - with: - subject-path: ${{ steps.locate.outputs.tarball }} - - - name: Publish to npmjs.com - run: npm publish "./${{ steps.locate.outputs.tarball }}" - - github-release: - name: "Attach tarball + SHA256SUMS to the GitHub release" - needs: [publish-npm] - if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') - permissions: - contents: write # create/update the Release for this tag and upload assets - uses: ./.github/workflows/release-assets.yml - with: - artifact-pattern: payjoin-javascript-npm-package - tag-prefix: payjoin-javascript- diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index a6fe71437..cad8b004e 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -13,6 +13,7 @@ on: - flake.nix - flake.lock - .github/workflows/kotlin.yml + workflow_call: jobs: build-kotlin-and-test: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 144b228f5..537dd6957 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -12,9 +12,7 @@ on: # change this workflow's environment. - flake.nix - flake.lock - push: - tags: - - "payjoin-python-[0-9]*" + workflow_call: jobs: build-python-and-test: @@ -110,78 +108,3 @@ jobs: payjoin.Uri.parse("bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com") print("smoke ok") ' - - verify-tag: - name: "Verify release tag" - if: startsWith(github.ref, 'refs/tags/payjoin-python-') - permissions: - contents: read - uses: ./.github/workflows/verify-tag-hygiene.yml - - publish-pypi: - name: "Publish to PyPI (trusted publishing / OIDC)" - runs-on: ubuntu-26.04 - needs: [build-python-and-test, build-wheel, smoke-wheel, verify-tag] - if: startsWith(github.ref, 'refs/tags/payjoin-python-') - environment: release - permissions: - id-token: write # OIDC: PyPI trusted publishing and its PEP 740 attestations - attestations: write # actions/attest-build-provenance writes the attestation - contents: read # needed only to check out the in-repo verify-tag-version action - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download wheels - uses: actions/download-artifact@v4 - with: - pattern: payjoin-python-wheel-* - merge-multiple: true - path: dist - - - name: Locate packed artifacts - id: locate - shell: bash - run: | - set -euo pipefail - shopt -s nullglob - pkgs=(dist/payjoin-*.whl) - if [ "${#pkgs[@]}" -ne 2 ]; then - echo "::error::expected exactly two wheels in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" - exit 1 - fi - # payjoin-0.24.0-py3-none-.whl -> 0.24.0 - versions="$(for pkg in "${pkgs[@]}"; do basename "$pkg" | cut -d- -f2; done | sort -u)" - if [ "$(echo "$versions" | wc -l)" -ne 1 ]; then - echo "::error::wheels disagree on version: $versions; refusing to publish" - exit 1 - fi - echo "version=$versions" >> "$GITHUB_OUTPUT" - - - name: Verify tag matches packed artifact version - uses: ./.github/actions/verify-tag-version - with: - tag-prefix: payjoin-python- - version: ${{ steps.locate.outputs.version }} - - - name: Attest build provenance (wheels) - # A consumer runs: gh attestation verify .whl -R payjoin/rust-payjoin - uses: actions/attest-build-provenance@v4 - with: - subject-path: dist/*.whl - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist - - github-release: - name: "Attach wheels + SHA256SUMS to the GitHub release" - needs: [publish-pypi] - if: startsWith(github.ref, 'refs/tags/payjoin-python-') - permissions: - contents: write # create/update the Release for this tag and upload assets - uses: ./.github/workflows/release-assets.yml - with: - artifact-pattern: payjoin-python-wheel-* - tag-prefix: payjoin-python- diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml deleted file mode 100644 index 3888fe07b..000000000 --- a/.github/workflows/release-assets.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Attach release assets - -# Reusable tail of every binding publish workflow: download the packed -# artifact(s), generate SHA256SUMS, and attach everything to the GitHub -# release for the pushed tag. A maintainer's GPG detached signature over -# SHA256SUMS is added out-of-band (locally, then uploaded as -# SHA256SUMS.asc), so no signing key ever reaches a runner. - -on: - workflow_call: - inputs: - artifact-pattern: - description: Name (or glob) of the artifact(s) holding the files to attach - required: true - type: string - tag-prefix: - description: > - Tag prefix before the version, e.g. payjoin-csharp-. Used to - derive the version for pre-release detection. - required: true - type: string - archive-globs: - description: > - Optional whitespace-separated globs naming which downloaded - assets are archives to open for member hashing. Required with - archive-member-globs. - required: false - type: string - default: "" - archive-member-globs: - description: > - Optional whitespace-separated globs of files inside each - matched archive to hash into SHA256SUMS, so files nested - inside a package (e.g. native libraries) can be verified - individually. Required with archive-globs. - required: false - type: string - default: "" - -jobs: - github-release: - name: Create GitHub release - runs-on: ubuntu-26.04 - permissions: - contents: write # create/update the Release for this tag and upload assets - steps: - - name: Checkout repo - uses: actions/checkout@v6 - - - name: Download release artifacts - uses: actions/download-artifact@v4 - with: - pattern: ${{ inputs.artifact-pattern }} - merge-multiple: true - path: dist - - - name: Generate SHA256SUMS - shell: bash - env: - ARCHIVE_GLOBS: ${{ inputs.archive-globs }} - MEMBER_GLOBS: ${{ inputs.archive-member-globs }} - run: | - ./contrib/release/generate-sha256sums.sh dist - echo "----- SHA256SUMS -----" - cat dist/SHA256SUMS - - - name: Derive pre-release status from the tag - id: meta - shell: bash - env: - TAG: ${{ github.ref_name }} - PREFIX: ${{ inputs.tag-prefix }} - run: | - set -euo pipefail - version="${TAG#"$PREFIX"}" - # SemVer: a hyphen in the version proper marks a pre-release; - # build metadata after a + never does. - version="${version%%+*}" - if [[ $version == *-* ]]; then - echo "prerelease=true" >>"$GITHUB_OUTPUT" - else - echo "prerelease=false" >>"$GITHUB_OUTPUT" - fi - - - name: Create / update GitHub release - uses: softprops/action-gh-release@v3 - with: - files: dist/* - fail_on_unmatched_files: true - draft: false - prerelease: ${{ steps.meta.outputs.prerelease }} diff --git a/.github/workflows/verify-tag-hygiene.yml b/.github/workflows/verify-tag-hygiene.yml deleted file mode 100644 index d80900fa6..000000000 --- a/.github/workflows/verify-tag-hygiene.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Verify tag hygiene - -# Reusable head of every binding publish workflow: verify the pushed tag is -# annotated, signed by a maintainer key in contrib/release/keys/, and an -# ancestor of master, so only a maintainer can cut a release. Call it as a -# job the publish job `needs`, so a bad tag fails before the `release` -# environment asks for approval. The tag comes from the triggering ref; the -# checks are prefix-agnostic, so there are no inputs. - -on: - workflow_call: - -jobs: - verify-tag-hygiene: - name: Verify tag hygiene - runs-on: ubuntu-26.04 - permissions: - contents: read - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Fetch master - run: git fetch --no-tags origin +refs/heads/master:refs/remotes/origin/master - - name: Set up nix - uses: ./.github/actions/setup-nix - - name: Verify tag hygiene - env: - TAG: ${{ github.ref_name }} - run: nix --quiet develop .#release -c ./contrib/release/verify-tag-hygiene.sh "$TAG" diff --git a/contrib/release/bindings-version.sh b/contrib/release/bindings-version.sh new file mode 100755 index 000000000..5435643b3 --- /dev/null +++ b/contrib/release/bindings-version.sh @@ -0,0 +1,63 @@ +#!/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: +# +payjoin-. With --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). +# +# usage: bindings-version.sh [--tag ] +set -euo pipefail +# shellcheck source=contrib/release/crates.sh +source "$(dirname "${BASH_SOURCE[0]}")/crates.sh" +cd "$REPO_ROOT" + +tag="" +case "${1:-}" in + "") ;; + --tag) tag="${2:?usage: bindings-version.sh [--tag ]}" ;; + *) + echo "usage: bindings-version.sh [--tag ]" >&2 + exit 1 + ;; +esac +die() { + echo "bindings-version: $*" >&2 + exit 1 +} + +RELEASE_METADATA="$(cargo_metadata)" +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 +expect() { # + [ "$2" = "$3" ] || { + echo "bindings-version: $1 is ${3:-missing}, expected $2" >&2 + status=1 + } +} +expect "payjoin-ffi's payjoin requirement" "$core" \ + "$(cargo_metadata | jq -r '.packages[] | select(.name == "payjoin-ffi") + | .dependencies[] | select(.name == "payjoin") + | .req | ltrimstr("^") | ltrimstr("=")')" +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" \ + "$(sed -n 's/^version: *//p' payjoin-ffi/dart/pubspec.yaml)" +expect "csharp/Payjoin.csproj Version" "$full" \ + "$(sed -n 's/.*\(.*\)<\/Version>.*/\1/p' payjoin-ffi/csharp/Payjoin.csproj | head -1)" +[ -z "$tag" ] || expect "release tag" "payjoin-ffi-$full" "$tag" +[ "$status" -eq 0 ] || exit 1 +printf '%s\n' "$full" diff --git a/contrib/release/check-invariants.sh b/contrib/release/check-invariants.sh index 0ef8b0b8e..f9a14498e 100755 --- a/contrib/release/check-invariants.sh +++ b/contrib/release/check-invariants.sh @@ -40,7 +40,8 @@ for crate in $crates; do recorded="$(lockfile_version "$REPO_ROOT/$lock" "$crate")" [ "$recorded" = "$version" ] || problem "$lock records $crate ${recorded:-}, expected $version" done - grep -qxF "## $version" "$REPO_ROOT/$crate/CHANGELOG.md" || problem "$crate/CHANGELOG.md has no ## $version section" + grep -qxF -e "## $version" -e "## [$version]" "$REPO_ROOT/$crate/CHANGELOG.md" || + problem "$crate/CHANGELOG.md has no ## $version section" done [ "$status" -eq 0 ] && echo "Release invariants hold for: $crates" diff --git a/contrib/release/extract-changelog.sh b/contrib/release/extract-changelog.sh index 8bd7549be..6baebc405 100755 --- a/contrib/release/extract-changelog.sh +++ b/contrib/release/extract-changelog.sh @@ -12,10 +12,11 @@ source "$(dirname "${BASH_SOURCE[0]}")/crates.sh" exit 1 } -# awk matches the heading exactly (a version has regex-special dots); sed -# drops leading blank lines and the command substitution drops trailing ones. -section="$(awk -v h="## $2" ' - $0 == h { inside = 1; next } +# awk matches the heading exactly (a version has regex-special dots), with +# or without the brackets the binding changelogs use; sed drops leading +# blank lines and the command substitution drops trailing ones. +section="$(awk -v h="## $2" -v b="## [$2]" ' + $0 == h || $0 == b { inside = 1; next } inside && /^## / { exit } inside ' "$REPO_ROOT/$1/CHANGELOG.md" | sed '/./,$!d')" diff --git a/contrib/release/tag.sh b/contrib/release/tag.sh index 52fc50ba8..5a2a26b2d 100755 --- a/contrib/release/tag.sh +++ b/contrib/release/tag.sh @@ -1,16 +1,15 @@ #!/usr/bin/env bash # # Sign the release tags for HEAD. For each release crate (and, with -# --bindings, each language package) whose manifest version has no tag yet, -# confirm the release invariants hold and HEAD is on payjoin/rust-payjoin's -# master, then create a signed annotated tag named - with the -# message "Release -". Prints the push command and never -# pushes. +# --bindings, the shared language-binding release) whose manifest version +# has no tag yet, confirm the release invariants hold and HEAD is on +# payjoin/rust-payjoin's master, then create a signed annotated tag named +# - with the message "Release -". Prints +# the push command and never pushes. # # usage: tag.sh [--dry-run] [--bindings] [--key ] [name...] -# name limit to these crates or packages: payjoin, payjoin-cli, -# payjoin-mailroom, or any payjoin-ffi/ with a -# contrib/release-version.sh +# name limit to payjoin, payjoin-cli, payjoin-mailroom, or payjoin-ffi +# (every language binding, from one tag) # # Run it in the release devShell, which pins jq, gpg and cargo: # nix develop .#release -c contrib/release/tag.sh [...] @@ -33,7 +32,7 @@ while [ "$#" -gt 0 ]; do shift ;; -h | --help) - sed -n '2,16p' "$0" + sed -n '2,15p' "$0" exit 0 ;; *) names+=("$1") ;; @@ -50,23 +49,6 @@ die() { # local ref can be stale. RELEASE_REPO="payjoin/rust-payjoin" -# Binding packages are the payjoin-ffi/ directories that ship a -# contrib/release-version.sh. It prints the version the next tag names, -# with +payjoin-X build metadata naming the wrapped payjoin version, and -# takes the payjoin version as its argument for packages whose manifest -# cannot hold that metadata. -binding_reader() { - printf '%s/payjoin-ffi/%s/contrib/release-version.sh' "$REPO_ROOT" "$1" -} -binding_names() { - local reader - for reader in "$REPO_ROOT"/payjoin-ffi/*/contrib/release-version.sh; do - [ -x "$reader" ] || continue - reader="${reader%/contrib/release-version.sh}" - printf '%s\n' "${reader##*/}" - done -} - [ -z "$(git status --porcelain --untracked-files=no)" ] || die "working tree has uncommitted changes; tags apply to HEAD" git fetch --quiet "https://github.com/$RELEASE_REPO.git" master || @@ -82,24 +64,21 @@ if [ "${#names[@]}" -gt 0 ]; then want=("${names[@]}") else read -r -a want <<<"$RELEASE_CRATES" - [ "$bindings" -eq 0 ] || mapfile -t -O "${#want[@]}" want < <(binding_names) + [ "$bindings" -eq 0 ] || want+=(payjoin-ffi) fi tags=() crates=() for name in "${want[@]}"; do - case " $RELEASE_CRATES " in - *" $name "*) - version="$(manifest_version "$name")" - prefix="$name" - ;; - *) - reader="$(binding_reader "$name")" - [ -x "$reader" ] || die "unknown name $name: not a release crate, and no $reader" - version="$("$reader" "$(manifest_version payjoin)")" || die "$reader failed" - prefix="payjoin-$name" - ;; - esac + if [[ " $RELEASE_CRATES " == *" $name "* ]]; then + version="$(manifest_version "$name")" + elif [ "$name" = payjoin-ffi ]; then + # One tag releases every language binding at this version. + version="$("$DIR/bindings-version.sh")" || die "binding versions disagree" + else + die "unknown name $name; use payjoin-ffi to release the language bindings" + fi + prefix="$name" [ -n "$version" ] || die "could not read a version for $name" tag="$prefix-$version" if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then @@ -114,16 +93,23 @@ for name in "${want[@]}"; do continue fi tags+=("$tag") - case " $RELEASE_CRATES " in - *" $name "*) crates+=("$name") ;; - esac + crates+=("$name") done [ "${#tags[@]}" -gt 0 ] || { echo "Nothing to tag: every manifest version is already tagged" exit 0 } -[ "${#crates[@]}" -eq 0 ] || "$DIR/check-invariants.sh" "${crates[@]}" +"$DIR/check-invariants.sh" "${crates[@]}" +# Dart consumers build the bindings from source against the wrapped payjoin +# on crates.io, so the bindings cannot ship before it. +case " ${crates[*]} " in + *" payjoin-ffi "*) + core="$(manifest_version payjoin)" + crate_published payjoin "$core" || + die "payjoin $core is not on crates.io; publish it before tagging payjoin-ffi" + ;; +esac sign=(--sign) [ -z "$key" ] || sign+=(--local-user "$key") @@ -145,7 +131,7 @@ remote="$(git remote -v | awk -v repo="$RELEASE_REPO" \ '$3 == "(push)" && $2 ~ ("[:/]" repo "(\\.git)?/?$") { print $1; exit }')" [ -n "$remote" ] || remote="https://github.com/$RELEASE_REPO.git" echo -echo "Push when ready; each tag starts its publish workflow and the release environment prompt:" +echo "Push when ready; each tag starts its release workflow and the release environment prompt:" printf ' git push %s' "$remote" printf ' %s' "${tags[@]}" echo diff --git a/payjoin-ffi/README.md b/payjoin-ffi/README.md index b56a17290..7b28aea4f 100644 --- a/payjoin-ffi/README.md +++ b/payjoin-ffi/README.md @@ -8,7 +8,9 @@ Our mission is to provide developers with cross-language libraries that seamless Each supported language is in its own directory. The Rust code in this project is in the `src` directory and is a wrapper around the Payjoin Dev Kit to expose its APIs uniformly using [UniFFI](https://github.com/mozilla/uniffi-rs) for each supported target language. -The directories below include instructions for using, building, and publishing the native language bindings supported by this project. +The directories below include instructions for using and building the language bindings. +See the [shared release instructions](RELEASING.md) for publishing all existing +packages with one signed payjoin-ffi tag. | Language | Platform | Repository | Published Package | | ---------- | --------------------- | ------------------------------------ | ----------------------------------------------- | diff --git a/payjoin-ffi/RELEASING.md b/payjoin-ffi/RELEASING.md new file mode 100644 index 000000000..d92b59007 --- /dev/null +++ b/payjoin-ffi/RELEASING.md @@ -0,0 +1,86 @@ +# Releasing the language bindings + +Python, JavaScript, C#, and Dart share the `payjoin-ffi` version and are +released together from one signed tag, +`payjoin-ffi-+payjoin-`, where `` is the wrapped +`payjoin` release. The first shared release is +`payjoin-ffi-0.25.0+payjoin-1.2.0`. Kotlin has no published package yet; +the tag only runs its checks. The `payjoin-ffi` crate itself is not +published to crates.io. + +## Versions + +`payjoin-ffi/Cargo.toml` holds the shared version. Each package manifest +repeats it in the form its registry accepts: + +| Manifest | Carries | +| ------------------------- | --------------------------------------------- | +| `python/pyproject.toml` | bare version, PEP 440 spelling | +| `javascript/package.json` | bare `version`, full version in `releaseTag` | +| `dart/pubspec.yaml` | full version | +| `csharp/Payjoin.csproj` | full version (NuGet drops the metadata again) | + +Pre-releases are `-rc.N` only; Python spells `0.26.0-rc.1` as `0.26.0rc1`, +and npm publishes pre-releases to the `next` dist-tag. Every release bumps +the version, including one that only changes the wrapped core, because +PyPI, npm, and NuGet ignore the build metadata when deciding whether a +version already exists. + +A bump PR changes all five manifests, both Cargo lockfiles +(`contrib/update-lock-files.sh`), `python/uv.lock`, +`javascript/package-lock.json`, and the changelogs. +`payjoin-ffi/CHANGELOG.md` is the canonical changelog and becomes the +GitHub release body. The language changelogs record only language-specific +changes and link to it; pub.dev displays Dart's. Check the manifests agree +with: + +```shell +nix develop .#release -c contrib/release/bindings-version.sh +``` + +## Registry configuration + +Before the first shared tag, point each trusted publisher at the new +workflow, keeping the existing package identities: PyPI, npm, and NuGet +use workflow `bindings-release.yml` and environment `release`; pub.dev +[automated publishing] uses tag pattern `payjoin-ffi-{{version}}` and +environment `release`. Keep required reviewers on the `release` +environment, and allow `payjoin-ffi-*` in the repository's tag rules. + +## Publishing + +1. Publish the wrapped `payjoin` release first. Dart consumers build the + bindings from source against it on crates.io. +2. Merge the bump PR and confirm every binding workflow is green on master. +3. Create the signed tag with a key in `contrib/release/keys/`: + + ```shell + nix develop .#release -c contrib/release/tag.sh payjoin-ffi + ``` + + The script checks that the manifests agree, that the lockfiles and + changelog match, that the core is published, and that HEAD is on + master. `tag.sh --bindings` selects the same tag alongside any pending + crate tags. Push the tag it prints. + +4. The `Release language bindings` run verifies the tag, builds and smoke + tests every language, then pauses four publish jobs on the `release` + environment. Approve the pending deployment once; it covers all four + jobs. Each registry is published with OIDC from the tested artifacts. +5. The run attaches the wheels, npm tarball, NuGet package, staged Dart + package, and one `SHA256SUMS` covering them all, including the native + libraries inside the NuGet package, to the GitHub release. Verify an + install from each registry and check provenance with + `gh attestation verify FILE -R payjoin/rust-payjoin`. Optionally sign + `SHA256SUMS` locally and upload `SHA256SUMS.asc`; no signing key reaches + a runner. + +## If a publish job fails + +Each registry has its own job, so use **Re-run failed jobs** on the same +run and approve the environment again. The re-run publishes from the same +tested artifacts and leaves the registries that already succeeded alone. +Never move a published tag or replace a published package; fix forward +with a new version. + +[automated publishing]: https://dart.dev/tools/pub/automated-publishing diff --git a/payjoin-ffi/csharp/RELEASING.md b/payjoin-ffi/csharp/RELEASING.md index 436f1ffe1..6145337e2 100644 --- a/payjoin-ffi/csharp/RELEASING.md +++ b/payjoin-ffi/csharp/RELEASING.md @@ -6,26 +6,14 @@ package readme. ## Versioning -- The package version is set in `Payjoin.csproj` (``). -- It is the package's own semantic version, independent of the - `payjoin-ffi` crate version. The language bindings follow a - `{version}+payjoin-{version}` convention: the [SemVer] build metadata - names the wrapped payjoin core release, so `0.1.0+payjoin-1.0.0` - packages payjoin 1.0.0. NuGet accepts build metadata per its - [package versioning] guidance but ignores it for version comparison - and strips it from the `.nupkg` filename, so the package identity is - the bare version. -- Bump `MAJOR.MINOR.PATCH` for C# API changes, and update the build - metadata whenever the wrapped payjoin core version changes. -- A release that only changes the wrapped payjoin core version still - needs at least a patch bump: the package identity ignores the build - metadata, and nuget.org rejects republishing an identity that - already exists. -- Versions up to `0.24.0-preview.1` tracked the `payjoin-ffi` crate - version instead. They predate this scheme and are unlisted on - nuget.org so they do not resolve as the latest version. -- `Payjoin.csproj` is the only place the version is maintained: the CI smoke - test derives the version from the packed artifact. +All published bindings share the payjoin-ffi version, starting with +0.25.0. Follow the [shared release instructions](../RELEASING.md) to bump +versions and publish using one signed FFI tag. Retained build metadata +identifies the wrapped payjoin core, and registries that omit metadata +publish the bare FFI version. + +NuGet retains the historical `0.24.0-preview.1` release. The common +`0.25.0` release supersedes it as well as the independent C# sequence. ## Producing a release candidate @@ -68,7 +56,7 @@ Review before every publish to nuget.org. Grounded in the NuGet - [ ] Native assets are release-profile builds without `_test-utils` (the pack step's validation target enforces both; confirm it ran in CI). - [ ] The package is under nuget.org's 250 MB size limit. -- [ ] Package version in `Payjoin.csproj` carries the intended C# version +- [ ] Package version in `Payjoin.csproj` carries the common FFI version and its `+payjoin-{version}` build metadata matches the wrapped payjoin core release. @@ -108,40 +96,15 @@ Review before every publish to nuget.org. Grounded in the NuGet ## Publishing -CI is the publish path. A tag push builds, packs, smoke-tests, and pushes the -package to nuget.org via [trusted publishing] (OIDC) — no long-lived API key -is ever stored. The workflow is -[`.github/workflows/csharp.yml`](../../.github/workflows/csharp.yml) (jobs -`publish-nuget` and `github-release`). - -1. Work through the release readiness checklist above on the release commit in - `master`; confirm every `Build and Test CSharp` job is green. -2. Tag that commit with `contrib/release/tag.sh` and push the tag it - names. The tag is `payjoin-csharp-`, where `` is the - `Payjoin.csproj` `` exactly, build metadata included, as - [`contrib/release-version.sh`](contrib/release-version.sh) prints it. - NuGet strips the metadata from the `Payjoin..nupkg` filename, - so the publish job strips it from the tag as well before comparing. - tag.sh signs it with your key, which must be in `contrib/release/keys/` for - `verify-tag` to accept it, refuses a commit that is not on `master`, - and prints the push command. - - ```shell - nix develop .#release -c contrib/release/tag.sh csharp - ``` - - The tag reruns the full build/pack/smoke graph at the tagged commit, then - `publish-nuget` verifies the tag matches the packed - `Payjoin..nupkg`, attests build provenance, exchanges the GitHub - OIDC token for a short-lived nuget.org key via [`NuGet/login`], and pushes. - The job runs in the `release` environment: if it has required - reviewers, approve the paused run before anything reaches nuget.org. - -3. `github-release` attaches the `.nupkg` and a generated `SHA256SUMS` to the - tag's GitHub release. Optionally sign `SHA256SUMS` locally and upload - `SHA256SUMS.asc` — never place a GPG key on a runner. -4. Complete the post-publish verification section of the checklist, and verify - the attestation: - `gh attestation verify Payjoin..nupkg -R payjoin/rust-payjoin`. +Follow the [shared release procedure](../RELEASING.md). The +`payjoin-ffi-+payjoin-` tag reruns this language's +build and smoke checks before publishing through the existing `release` +environment with OIDC. The package version must match the common FFI +version, and its core metadata must match the declared core dependency. + +The GitHub release includes all language packages and +`SHA256SUMS`. Optional local signatures use +`SHA256SUMS.asc`. Verify installation from the registry and +check provenance with `gh attestation verify FILE -R payjoin/rust-payjoin`. [trusted publishing]: https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing diff --git a/payjoin-ffi/csharp/contrib/release-version.sh b/payjoin-ffi/csharp/contrib/release-version.sh deleted file mode 100755 index fc2b1f6e5..000000000 --- a/payjoin-ffi/csharp/contrib/release-version.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -# -# Print the version the next payjoin-csharp release tag names: the -# of Payjoin.csproj. -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")/.." -version="$(sed -n 's/.*\(.*\)<\/Version>.*/\1/p' Payjoin.csproj | head -1)" -[ -n "$version" ] || exit 1 -printf '%s\n' "$version" diff --git a/payjoin-ffi/dart/CONTRIBUTING.md b/payjoin-ffi/dart/CONTRIBUTING.md index 783454665..b018b92ff 100644 --- a/payjoin-ffi/dart/CONTRIBUTING.md +++ b/payjoin-ffi/dart/CONTRIBUTING.md @@ -60,30 +60,13 @@ validates the archive with a publish dry run nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh /tmp/payjoin-pub ``` -2. Set the version in `pubspec.yaml` and describe the consumer-visible - changes under a matching heading in `CHANGELOG.md`. -3. Confirm every `Build and Test Dart` job is green on the release commit - in `master`. -4. Tag that commit with `contrib/release/tag.sh` and push the tag it - names. The tag is `payjoin-dart-`, where `` is the - `pubspec.yaml` version exactly (including the `+` build metadata), as - [`contrib/release-version.sh`](contrib/release-version.sh) prints it. - tag.sh signs it with your key, which must be in `contrib/release/keys/` for - `verify-tag` to accept it, refuses a commit that is not on `master`, - and prints the push command. - - ```shell - nix develop .#release -c contrib/release/tag.sh dart - ``` - - The tag reruns the tests and the archive verification at the tagged - commit, then `publish-pub` verifies the tag matches `pubspec.yaml`, - regenerates the production bindings, and publishes through pub.dev - [automated publishing] (OIDC), so no long-lived credential is stored - anywhere. The job runs in the `release` environment: approve the paused - run before anything reaches the registry. - -5. Verify the [pub.dev listing](https://pub.dev/packages/payjoin) shows the - new version and its changelog. - -[automated publishing]: https://dart.dev/tools/pub/automated-publishing +2. Follow the [shared bindings release instructions](../RELEASING.md). + `pubspec.yaml` carries the common FFI version and the wrapped core + metadata, for example `0.25.0+payjoin-1.2.0`. One signed + `payjoin-ffi-+payjoin-` tag starts all publishers. +3. Configure pub.dev automated publishing with tag pattern + `payjoin-ffi-{{version}}` and environment `release` before the first + shared release. Approve the release environment when prompted. +4. Verify the [pub.dev listing](https://pub.dev/packages/payjoin) shows the + new version and changelog. The staged package's native Rust revision + must equal the shared tag's commit. diff --git a/payjoin-ffi/dart/contrib/release-version.sh b/payjoin-ffi/dart/contrib/release-version.sh deleted file mode 100755 index 74fa4e79f..000000000 --- a/payjoin-ffi/dart/contrib/release-version.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -# -# Print the version the next payjoin-dart release tag names: the -# `version` of pubspec.yaml. -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")/.." -version="$(sed -n 's/^version: *\(.*\)$/\1/p' pubspec.yaml | head -1)" -[ -n "$version" ] || exit 1 -printf '%s\n' "$version" diff --git a/payjoin-ffi/javascript/RELEASING.md b/payjoin-ffi/javascript/RELEASING.md index 67059c4c5..d06aa5ab1 100644 --- a/payjoin-ffi/javascript/RELEASING.md +++ b/payjoin-ffi/javascript/RELEASING.md @@ -1,62 +1,27 @@ # Releasing the payjoin npm package -Maintainer documentation for publishing the `payjoin` package to -[npmjs.com](https://www.npmjs.com/package/payjoin). Consumer documentation -lives in [`README.md`](README.md). +Follow the [shared bindings release instructions](../RELEASING.md) for +version bumps, registry configuration, and tagging. One +`payjoin-ffi-+payjoin-` tag releases every +binding. ## Versioning -- The package version is set in `package.json`. -- It is the package's own semantic version, independent of the - `payjoin-ffi` crate version. The language bindings follow a - `{version}+payjoin-{version}` convention where the build metadata - names the wrapped payjoin core release, but npm strips build metadata - from published versions, so `package.json` carries the bare version - and records the full one in a `releaseTag` field for traceability. - The release tag carries the full version too. Keep `releaseTag` in - sync when bumping the version. -- A release that only changes the wrapped payjoin core version still - needs at least a patch bump: npm sees only the bare version and - rejects republishing one that already exists. -- `package.json` is the only place the version is maintained: the publish - job derives the version from the packed tarball and refuses to publish - if it does not match the pushed tag. +Starting with 0.25.0, `package.json` uses the common FFI version. Its +`releaseTag` field appends the wrapped payjoin core as build metadata. +Keep both version entries in `package-lock.json` synchronized. npm +publishes the bare version, so changing only metadata requires another +common version bump. -## Publishing +## Package checks -CI is the publish path. On every pull request touching `payjoin-ffi/**`, -the `Build and Test JavaScript` workflow builds the wasm package, packs the -tarball with [`contrib/pack.sh`](contrib/pack.sh), and smoke-installs it on -Linux and macOS. The tarball ships only `dist/` (wasm + compiled -TypeScript), which is platform-independent. +The JavaScript workflow builds production wasm and compiled TypeScript, +packs the tarball with `contrib/pack.sh`, and smoke tests installation on +Linux and macOS. The package ships only `dist/` and is platform independent. +The publishing job compares the tag with the packed artifact, attests it, +and publishes through npm trusted publishing in the `release` environment. -1. Confirm every `Build and Test JavaScript` job is green on the release - commit in `master`. -2. Tag that commit with `contrib/release/tag.sh` and push the tag it - names. The tag is `payjoin-javascript-`, where `` is - the `package.json` `releaseTag` value exactly, as - [`contrib/release-version.sh`](contrib/release-version.sh) prints it; - the publish job strips the build metadata before comparing the tag - against the packed tarball. tag.sh signs it with your key, which must be in `contrib/release/keys/` for - `verify-tag` to accept it, refuses a commit that is not on `master`, - and prints the push command. - - ```shell - nix develop .#release -c contrib/release/tag.sh javascript - ``` - - The tag reruns the full build/pack/smoke graph at the tagged commit, - then `publish-npm` verifies the tag matches the packed tarball, attests - build provenance, and publishes through npm - [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC), - so no long-lived token is stored anywhere. The job runs in the `release` - environment: approve the paused run before anything reaches the - registry. - -3. `github-release` attaches the tarball and a generated `SHA256SUMS` to - the tag's GitHub release. Optionally sign `SHA256SUMS` locally and - upload `SHA256SUMS.asc`. -4. Verify the publication: the npmjs.com listing shows the new version - with a provenance badge, and - `gh attestation verify payjoin-.tgz -R payjoin/rust-payjoin` - passes against the release asset. +The shared GitHub release includes the tarball and +`SHA256SUMS`. An optional local signature is named +`SHA256SUMS.asc`. Verify the registry version, its provenance +badge, and `gh attestation verify FILE -R payjoin/rust-payjoin`. diff --git a/payjoin-ffi/javascript/contrib/release-version.sh b/payjoin-ffi/javascript/contrib/release-version.sh deleted file mode 100755 index 94c86f2e4..000000000 --- a/payjoin-ffi/javascript/contrib/release-version.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# -# Print the version the next payjoin-javascript release tag names: the -# `releaseTag` field of package.json. npm rejects build metadata in -# `version`, so the full version lives in `releaseTag`. -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")/.." -jq -er '.releaseTag' package.json diff --git a/payjoin-ffi/python/RELEASING.md b/payjoin-ffi/python/RELEASING.md index c172aaa47..17412dee9 100644 --- a/payjoin-ffi/python/RELEASING.md +++ b/payjoin-ffi/python/RELEASING.md @@ -6,21 +6,11 @@ Maintainer documentation for publishing the `payjoin` package to ## Versioning -- The package version is set in `pyproject.toml` (`project.version`). -- It is the package's own semantic version, independent of the - `payjoin-ffi` crate version. The language bindings follow a - `{version}+payjoin-{version}` convention where the build metadata - names the wrapped payjoin core release, but PyPI rejects PEP 440 - local version labels (the `+` part), so the package publishes the - bare version. The release tag carries the full version, and the - wrapped payjoin core version is also recorded in - [`CHANGELOG.md`](CHANGELOG.md). -- A release that only changes the wrapped payjoin core version still - needs at least a patch bump: PyPI sees only the bare version and - rejects re-uploading one that already exists. -- `pyproject.toml` is the only place the version is maintained: the - publish job derives the version from the built wheels and refuses to - publish if it does not match the pushed tag. +All published bindings share the payjoin-ffi version, starting with +0.25.0. Follow the [shared release instructions](../RELEASING.md) to bump +versions and publish using one signed FFI tag. Retained build metadata +identifies the wrapped payjoin core, and registries that omit metadata +publish the bare FFI version. ## Producing the wheels @@ -41,34 +31,13 @@ CPython satisfying `requires-python` can install them. ## Publishing -1. Confirm every `Build and Test Python` job is green on the release - commit in `master`, including the per-platform smoke tests. -2. Tag that commit with `contrib/release/tag.sh` and push the tag it - names. The tag is `payjoin-python-`, where `` is the - `pyproject.toml` version plus `+payjoin-` build metadata - naming the wrapped payjoin core release, as - [`contrib/release-version.sh`](contrib/release-version.sh) prints it; - the publish job strips the metadata before comparing the tag against - the built wheels. tag.sh signs it with your key, which must be in `contrib/release/keys/` for - `verify-tag` to accept it, refuses a commit that is not on `master`, - and prints the push command. +Follow the [shared release procedure](../RELEASING.md). The +`payjoin-ffi-+payjoin-` tag reruns this language's +build and smoke checks before publishing through the existing `release` +environment with OIDC. The package version must match the common FFI +version, and its core metadata must match the declared core dependency. - ```shell - nix develop .#release -c contrib/release/tag.sh python - ``` - - The tag reruns the full build/wheel/smoke graph at the tagged commit, - then `publish-pypi` verifies the tag matches the built wheels, attests - build provenance, and uploads through PyPI - [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC) - with PEP 740 attestations, so no long-lived token is stored anywhere. - The job runs in the `release` environment: approve the paused run before - anything reaches the registry. - -3. `github-release` attaches the wheels and a generated `SHA256SUMS` to - the tag's GitHub release. Optionally sign `SHA256SUMS` locally and - upload `SHA256SUMS.asc`. -4. Verify the publication: the PyPI listing shows the new version, - `pip install payjoin==` resolves on a supported platform, and - `gh attestation verify -R payjoin/rust-payjoin` passes against - a release asset. +The GitHub release includes all language packages and +`SHA256SUMS`. Optional local signatures use +`SHA256SUMS.asc`. Verify installation from the registry and +check provenance with `gh attestation verify FILE -R payjoin/rust-payjoin`. diff --git a/payjoin-ffi/python/contrib/release-version.sh b/payjoin-ffi/python/contrib/release-version.sh deleted file mode 100755 index 9af3c7796..000000000 --- a/payjoin-ffi/python/contrib/release-version.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# -# Print the version the next payjoin-python release tag names: -# +payjoin-. PyPI rejects local -# versions, so pyproject.toml holds only the bare version and the build -# metadata is added here; the publish workflow strips it again. -# -# usage: release-version.sh -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")/.." -[ -n "${1:-}" ] || exit 1 -version="$(awk '/^\[/ { in_project = ($0 == "[project]") } - in_project && /^version *=/ { gsub(/^version *= *"|"$/, ""); print; exit }' pyproject.toml)" -[ -n "$version" ] || exit 1 -printf '%s+payjoin-%s\n' "$version" "$1"