From 5e88655523474b55b607bc9c3f638d1b91b8c273 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 21:05:33 +0000 Subject: [PATCH 01/12] Extract nix setup into a composite action Six workflows repeat the same two steps to prepare a job for nix develop: the Determinate nix installer and the magic nix cache with its FlakeHub lookup disabled. Upcoming release workflows for the language bindings would copy the pair four more times. Move the pair into .github/actions/setup-nix. The Swatinem Rust build cache stays an explicit separate step in the jobs that compile, since it is unrelated to nix. No job's steps change beyond the substitution. --- .github/actions/setup-nix/action.yml | 14 ++++++++++++ .github/workflows/crates-release.yml | 32 +++++++--------------------- .github/workflows/csharp.yml | 8 ++----- .github/workflows/dart.yml | 8 ++----- .github/workflows/format.yml | 8 ++----- .github/workflows/javascript.yml | 8 ++----- .github/workflows/python.yml | 8 ++----- 7 files changed, 32 insertions(+), 54 deletions(-) create mode 100644 .github/actions/setup-nix/action.yml diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 000000000..bdd358d8f --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,14 @@ +name: Set up nix +description: > + Install nix with the magic cache, keeping its FlakeHub lookup disabled. + Must run after actions/checkout so the cache can key off the repository + contents. +runs: + using: composite + steps: + - name: Install nix + uses: DeterminateSystems/determinate-nix-action@main + - name: Use nix cache + uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-flakehub: false diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml index 5bea7e7b3..517e73a74 100644 --- a/.github/workflows/crates-release.yml +++ b/.github/workflows/crates-release.yml @@ -31,12 +31,8 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Install nix - uses: DeterminateSystems/determinate-nix-action@main - - name: Use nix cache - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: Check the bump is consistent and publishable env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -57,12 +53,8 @@ jobs: fetch-depth: 0 - name: Fetch master run: git fetch --no-tags origin +refs/heads/master:refs/remotes/origin/master - - name: Install nix - uses: DeterminateSystems/determinate-nix-action@main - - name: Use nix cache - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: Compute tag metadata id: meta env: @@ -155,12 +147,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v6 - - name: Install nix - uses: DeterminateSystems/determinate-nix-action@main - - name: Use nix cache - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: Download packaged crate uses: actions/download-artifact@v4 with: @@ -200,12 +188,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v6 - - name: Install nix - uses: DeterminateSystems/determinate-nix-action@main - - name: Use nix cache - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: Download packaged crate uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index b5476545d..1e97694ae 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -35,12 +35,8 @@ jobs: uses: actions/checkout@v6 - name: "Use cache" uses: Swatinem/rust-cache@v2 - - name: "Install nix" - uses: DeterminateSystems/determinate-nix-action@main - - name: "Use nix cache" - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#csharp -c ./payjoin-ffi/csharp/contrib/test.sh diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 317ace255..2e1159598 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -25,11 +25,7 @@ jobs: uses: actions/checkout@v6 - name: "Use cache" uses: Swatinem/rust-cache@v2 - - name: "Install nix" - uses: DeterminateSystems/determinate-nix-action@main - - name: "Use nix cache" - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#dart -c bash ./payjoin-ffi/dart/contrib/test.sh diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f1740156b..e99777e93 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -8,12 +8,8 @@ jobs: steps: - name: "Checkout repo" uses: actions/checkout@v6 - - name: "Install nix" - uses: DeterminateSystems/determinate-nix-action@main - - name: "Use nix cache" - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: Run treefmt run: | set -eo pipefail diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index 0feafb77d..bcefffe5b 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -25,11 +25,7 @@ jobs: uses: actions/checkout@v6 - name: "Use cache" uses: Swatinem/rust-cache@v2 - - name: "Install nix" - uses: DeterminateSystems/determinate-nix-action@main - - name: "Use nix cache" - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#javascript -c ./payjoin-ffi/javascript/contrib/test.sh diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 187ea365b..aebbe9cf2 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -25,11 +25,7 @@ jobs: uses: actions/checkout@v6 - name: "Use cache" uses: Swatinem/rust-cache@v2 - - name: "Install nix" - uses: DeterminateSystems/determinate-nix-action@main - - name: "Use nix cache" - uses: DeterminateSystems/magic-nix-cache-action@main - with: - use-flakehub: false + - name: Set up nix + uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#python --command bash ./payjoin-ffi/python/contrib/test.sh From b437c4a8cb6bac0f2aa042777996d97129cda426 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 21:06:17 +0000 Subject: [PATCH 02/12] Extract tag-version check into a composite action Every registry publish job must refuse to push an artifact whose version differs from what the pushed tag calls for. The check was written inline in the NuGet publish job; the upcoming npm, pub.dev, and PyPI publish jobs would each duplicate it. Move the prefix-strip-and-compare into a verify-tag-version composite action taking the tag prefix and the actual packed version. The NuGet job keeps its locate step (exactly one .nupkg, version parsed from the filename) and hands the result to the action. Local actions resolve from the workspace, so the job gains a checkout step it previously avoided; its contents: read permission already covers it. --- .github/actions/verify-tag-version/action.yml | 37 +++++++++++++++++++ .github/workflows/csharp.yml | 32 +++++++++------- 2 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 .github/actions/verify-tag-version/action.yml diff --git a/.github/actions/verify-tag-version/action.yml b/.github/actions/verify-tag-version/action.yml new file mode 100644 index 000000000..a21e155f0 --- /dev/null +++ b/.github/actions/verify-tag-version/action.yml @@ -0,0 +1,37 @@ +name: Verify tag version +description: > + Check that the pushed tag is the expected prefix followed by exactly the + version about to be published, refusing to publish on any mismatch. +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"}" + if [[ $tag_version != "$VERSION" ]]; then + echo "::error::tag $TAG implies version $tag_version but the packed version is $VERSION; refusing to publish" + exit 1 + fi + echo "version=$tag_version" >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 1e97694ae..3b88ad94c 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -272,8 +272,11 @@ jobs: permissions: id-token: write # OIDC: used by BOTH NuGet/login and attest-build-provenance attestations: write # actions/attest-build-provenance writes the attestation - contents: read # checkout of the (already-packed) repo is not needed; read is the floor + 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: @@ -285,27 +288,28 @@ jobs: with: dotnet-version: "10.0.x" - - name: Verify tag matches packed artifact version - id: verify + - name: Locate packed artifact + id: locate shell: bash run: | set -euo pipefail - # payjoin-csharp-0.24.0-preview.1 -> 0.24.0-preview.1 - version="${GITHUB_REF_NAME#payjoin-csharp-}" 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 - expected="Payjoin.${version}.nupkg" - actual="$(basename "${pkgs[0]}")" - if [ "${actual}" != "${expected}" ]; then - echo "::error::tag ${GITHUB_REF_NAME} implies ${expected} but packed artifact is ${actual}; refusing to publish" - 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=${version}" >> "$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 @@ -316,7 +320,7 @@ jobs: # published unit.) uses: actions/attest-build-provenance@v4 with: - subject-path: ${{ steps.verify.outputs.nupkg }} + subject-path: ${{ steps.locate.outputs.nupkg }} - name: NuGet login (OIDC -> short-lived API key) id: login @@ -335,7 +339,7 @@ jobs: # the tag would otherwise 409 on the already-published version and # redden the whole run. With it, an already-present version is a # no-op success and the run converges. - dotnet nuget push "${{ steps.verify.outputs.nupkg }}" \ + 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 \ From 4d955a3a9f32056314e54d0a39fcd3940e12f6d5 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 21:07:18 +0000 Subject: [PATCH 03/12] Extract GitHub release job into a reusable workflow Publishing ends the same way for every binding: download the packed artifact, generate SHA256SUMS, and attach both to the GitHub release for the pushed tag. The NuGet workflow carried this as an inline job; the upcoming npm and PyPI publish paths would each copy it. Move the job to release-assets.yml as a workflow_call and point the C# github-release job at it. The artifact-pattern input downloads and merges multiple artifacts for workflows that build one per platform, and unzip-globs generalizes the C#-specific step that hashes native libraries nested inside the package. Two behavior notes: the job now runs on ubuntu-26.04 like the rest of the repo instead of ubuntu-latest, and pre-release detection derives from a SemVer hyphen in the tag's version (ignoring + build metadata) instead of matching only -preview and -rc, which classifies all existing tags identically. --- .github/workflows/csharp.yml | 45 +++----------- .github/workflows/release-assets.yml | 90 ++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/release-assets.yml diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 3b88ad94c..e2ab03766 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -347,44 +347,15 @@ jobs: github-release: name: "Attach nupkg + SHA256SUMS to the GitHub release" - runs-on: ubuntu-latest needs: [publish-nuget] if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') permissions: contents: write # create/update the Release for this tag and upload assets - steps: - - name: Download packed NuGet package - uses: actions/download-artifact@v4 - with: - name: payjoin-csharp-nuget-package - path: dist - - - name: Generate SHA256SUMS (nupkg + each native lib) - working-directory: dist - shell: bash - run: | - set -euo pipefail - # Hash the package itself. - sha256sum *.nupkg > SHA256SUMS - # Also hash each shipped native library extracted from inside the - # package, so a consumer can verify an individual .so/.dylib/.dll - # (paths are runtimes//native/). Matches the tor/hwi pattern. - tmp="$(mktemp -d)" - unzip -q *.nupkg -d "$tmp" 'runtimes/*/native/*' - ( cd "$tmp" && find runtimes -type f -print0 | sort -z | xargs -0 sha256sum ) >> SHA256SUMS - echo "----- SHA256SUMS -----" - cat SHA256SUMS - - - name: Create / update GitHub release - uses: softprops/action-gh-release@v3 - with: - files: | - dist/*.nupkg - dist/SHA256SUMS - fail_on_unmatched_files: true - draft: false - # Mark preview / rc tags as pre-releases on the Releases page. - prerelease: ${{ contains(github.ref_name, '-preview') || contains(github.ref_name, '-rc') }} - # Dan's GPG detached signature over SHA256SUMS is added out-of-band - # (locally, then uploaded as SHA256SUMS.asc) — his private key must not - # live on a runner. See README "GPG signature". + uses: ./.github/workflows/release-assets.yml + with: + artifact-pattern: payjoin-csharp-nuget-package + tag-prefix: payjoin-csharp- + # Also hash each shipped native library from inside the package, so a + # consumer can verify an individual .so/.dylib/.dll (paths are + # runtimes//native/). Matches the tor/hwi pattern. + unzip-globs: runtimes/*/native/* diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml new file mode 100644 index 000000000..104a635f8 --- /dev/null +++ b/.github/workflows/release-assets.yml @@ -0,0 +1,90 @@ +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 + unzip-globs: + description: > + Optional whitespace-separated globs extracted from each + zip-format asset and appended to SHA256SUMS, so files nested + inside a package (e.g. native libraries) can be verified + individually. + 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: Download release artifacts + uses: actions/download-artifact@v4 + with: + pattern: ${{ inputs.artifact-pattern }} + merge-multiple: true + path: dist + + - name: Generate SHA256SUMS + working-directory: dist + shell: bash + env: + UNZIP_GLOBS: ${{ inputs.unzip-globs }} + run: | + set -euo pipefail + find . -type f -printf '%P\0' | sort -z | xargs -0 sha256sum >SHA256SUMS + if [ -n "$UNZIP_GLOBS" ]; then + read -ra globs <<<"$UNZIP_GLOBS" + tmp="$(mktemp -d)" + shopt -s nullglob + for archive in *.nupkg *.whl *.zip; do + unzip -q "$archive" -d "$tmp" "${globs[@]}" + done + (cd "$tmp" && find . -type f -printf '%P\0' | sort -z | xargs -0 sha256sum) >>SHA256SUMS + fi + echo "----- SHA256SUMS -----" + cat 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 }} From 2fea9bd11611011673230329fae9ec5edf0cf2b3 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 21:52:50 +0000 Subject: [PATCH 04/12] Add production pack script for the npm package contrib/pack.sh builds the package from the maintained recent lockfile and packs the npm tarball into artifacts/, mirroring how contrib/test.sh drives the test build. The tarball ships only dist/ (wasm + compiled TypeScript), which is platform-independent, so a single pack is the entire release build. The test-utils napi addon is a dev-only helper for the integration tests, so generate_bindings.sh gains a PAYJOIN_JS_BUILD_TEST_UTILS toggle (default on, preserving test behavior) that the pack script switches off. --- payjoin-ffi/javascript/.gitignore | 1 + payjoin-ffi/javascript/contrib/pack.sh | 27 +++++++++++++++++++ .../javascript/scripts/generate_bindings.sh | 6 ++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100755 payjoin-ffi/javascript/contrib/pack.sh diff --git a/payjoin-ffi/javascript/.gitignore b/payjoin-ffi/javascript/.gitignore index 2c04b1dce..5556909ef 100644 --- a/payjoin-ffi/javascript/.gitignore +++ b/payjoin-ffi/javascript/.gitignore @@ -1,6 +1,7 @@ # Build outputs dist/ node_modules/ +artifacts/ # Generated by uniffi-bindgen-react-native rust_modules/ diff --git a/payjoin-ffi/javascript/contrib/pack.sh b/payjoin-ffi/javascript/contrib/pack.sh new file mode 100755 index 000000000..014c4e103 --- /dev/null +++ b/payjoin-ffi/javascript/contrib/pack.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the production package and pack the npm tarball into artifacts/. +# The tarball ships only dist/ (wasm + compiled TypeScript), which is +# platform-independent, so a single pack is the entire release build. + +# Build against the maintained lockfile instead of resolving the dependency +# graph fresh on every run. use_lockfile copies Cargo-recent.lock into place +# and restores the previous state when this script exits. +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "$REPO_ROOT" +source contrib/lockfile.sh +use_lockfile Cargo-recent.lock + +cd "$REPO_ROOT/payjoin-ffi/javascript" + +echo "==> Installing JavaScript dependencies..." +npm ci + +echo "==> Generating FFI bindings..." +PAYJOIN_JS_BUILD_TEST_UTILS=0 bash ./scripts/generate_bindings.sh + +echo "==> Packing npm tarball..." +rm -rf artifacts +mkdir -p artifacts +npm pack --pack-destination artifacts diff --git a/payjoin-ffi/javascript/scripts/generate_bindings.sh b/payjoin-ffi/javascript/scripts/generate_bindings.sh index 6b4c7f309..31f601fab 100755 --- a/payjoin-ffi/javascript/scripts/generate_bindings.sh +++ b/payjoin-ffi/javascript/scripts/generate_bindings.sh @@ -32,6 +32,10 @@ if command -v rustup >/dev/null 2>&1 && fi npm run build -npm run build:test-utils + +# The test-utils addon is a dev-only native helper for the integration tests. +if [[ ${PAYJOIN_JS_BUILD_TEST_UTILS:-1} == 1 ]]; then + npm run build:test-utils +fi echo "All done!" From 902250eee44c95bfc56cb0ad3080008648066662 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 22:46:16 +0000 Subject: [PATCH 05/12] Publish the npm package from CI on tag push Add the tag-gated publishing path to the JavaScript workflow, mirroring the NuGet flow in csharp.yml: every run packs the tarball and smoke-installs it on Linux and macOS; pushing a payjoin-javascript- tag additionally reruns the graph at the tagged commit, then publishes the smoke-tested tarball to npmjs.com and attaches it to the GitHub release. The publish job verifies the tag matches the packed version, attests build provenance, and authenticates with npm trusted publishing (OIDC), so no long-lived registry token exists anywhere. It runs in the release environment for a required-reviewer gate and also waits for the build-and-test jobs at the tagged commit before anything reaches the registry. RELEASING.md records the release steps and the one-time npmjs.com trusted-publisher and environment setup. --- .github/workflows/javascript.yml | 144 ++++++++++++++++++++++++++++ payjoin-ffi/javascript/RELEASING.md | 67 +++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 payjoin-ffi/javascript/RELEASING.md diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index bcefffe5b..45555c5e4 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -12,6 +12,14 @@ on: # change this workflow's environment. - flake.nix - flake.lock + # Publishing path. A tag push runs the full build/pack/smoke graph at the + # tagged commit (no path filter applies to tags), then the tag-gated + # publish/release jobs below. Tag scheme: payjoin-javascript-, e.g. payjoin-javascript-0.1.1, matching the version-first + # payjoin-csharp-* convention. + push: + tags: + - "payjoin-javascript-[0-9]*" jobs: build-js-and-test: @@ -29,3 +37,139 @@ jobs: uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#javascript -c ./payjoin-ffi/javascript/contrib/test.sh + + pack-npm: + # The package ships only dist/ (wasm + compiled TypeScript), which is + # platform-independent, so one pack on Linux is the entire release build. + name: "Pack npm package" + runs-on: ubuntu-26.04 + 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: Build and pack + run: nix develop .#javascript -c ./payjoin-ffi/javascript/contrib/pack.sh + - name: Upload npm package + uses: actions/upload-artifact@v4 + with: + name: payjoin-javascript-npm-package + path: payjoin-ffi/javascript/artifacts/*.tgz + if-no-files-found: error + + smoke-npm: + name: "Smoke test npm package" + runs-on: ${{ matrix.os }} + needs: pack-npm + strategy: + matrix: + os: [ubuntu-26.04, macos-latest] + steps: + - name: Download npm package + uses: actions/download-artifact@v4 + with: + name: payjoin-javascript-npm-package + path: pkg + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "24" + - name: Install and exercise the packed package + shell: bash + run: | + set -euo pipefail + mkdir smoke && cd smoke + npm init -y >/dev/null + npm install ../pkg/payjoin-*.tgz + node --input-type=module -e ' + import { payjoin, uniffiInitAsync } from "payjoin"; + await uniffiInitAsync(); + payjoin.Url.parse("https://example.com"); + console.log("smoke ok"); + ' + + # --------------------------------------------------------------------------- + # PUBLISH PATH (tag-gated). Runs only for a payjoin-javascript-* tag push, + # after build/test, pack, and smoke are green. Consumes the already-built, + # already-smoke-tested tarball and never repacks. + # --------------------------------------------------------------------------- + publish-npm: + name: "Publish to npmjs.com (trusted publishing / OIDC)" + runs-on: ubuntu-26.04 + needs: [build-js-and-test, pack-npm, smoke-npm] + if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') + # Pauses for a required reviewer before anything reaches the registry; + # the npmjs.com trusted-publishing policy is bound to this environment. + environment: release + permissions: + id-token: write # OIDC: npm trusted publishing and its provenance statement + 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 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 + # Trusted publishing requires npm 11.5.1 or newer. + 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-build-provenance@v4 + with: + subject-path: ${{ steps.locate.outputs.tarball }} + + - name: Publish to npmjs.com + # Trusted publishing exchanges the job's OIDC token for short-lived + # registry credentials and attaches npm provenance automatically; + # there is no long-lived token anywhere in this workflow. + 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/payjoin-ffi/javascript/RELEASING.md b/payjoin-ffi/javascript/RELEASING.md new file mode 100644 index 000000000..be85f4a64 --- /dev/null +++ b/payjoin-ffi/javascript/RELEASING.md @@ -0,0 +1,67 @@ +# 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). + +## Versioning + +- The package version is set in `package.json`. +- It is the package's own semantic version, independent of the + `payjoin-ffi` crate version while the JavaScript API stabilizes. +- `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. + +## Publishing + +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. + +1. Confirm every `Build and Test JavaScript` job is green on the release + commit in `master`. +2. Tag that commit `payjoin-javascript-`, where `` is the + `package.json` version exactly, and push the tag: + + ```shell + git tag payjoin-javascript-0.1.1 + git push upstream payjoin-javascript-0.1.1 + ``` + + 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`; never place a GPG key on a runner. +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. + +One-time setup is account and repository configuration, not part of the +per-release flow: the npmjs.com trusted publisher for the `payjoin` +package (GitHub Actions, repository `payjoin/rust-payjoin`, workflow file +`javascript.yml`, environment `release`), and the `release` GitHub +Actions environment with required reviewers. + +## Manual fallback + +Use only if the CI publish path is unavailable. Requires npm ownership of +the `payjoin` package and an account with two-factor authentication. + +```shell +nix develop .#javascript -c ./payjoin-ffi/javascript/contrib/pack.sh +npm publish payjoin-ffi/javascript/artifacts/payjoin-.tgz +``` + +A manual publish carries no provenance statement; prefer republishing +through CI for anything consumers will install. From 1fbf755262413650a8399b855cc24e22a6ae779d Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 22:46:16 +0000 Subject: [PATCH 06/12] Parameterize dart bindings build for release The script hardcoded --features dart,_test-utils --profile dev, so every published release shipped bindings declaring test-only APIs whose symbols are absent from consumer builds, a limitation CONTRIBUTING.md called out. Adopt the PAYJOIN_FFI_FEATURES / PAYJOIN_FFI_PROFILE convention the C# generate_bindings.sh already uses: defaults are unchanged for tests, and a release build sets PAYJOIN_FFI_FEATURES= (empty) to emit bindings with no test-only declarations. --- payjoin-ffi/dart/scripts/generate_bindings.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/payjoin-ffi/dart/scripts/generate_bindings.sh b/payjoin-ffi/dart/scripts/generate_bindings.sh index 1eae49889..3cd4fbfb4 100755 --- a/payjoin-ffi/dart/scripts/generate_bindings.sh +++ b/payjoin-ffi/dart/scripts/generate_bindings.sh @@ -19,7 +19,20 @@ fi cd ../ echo "Generating payjoin dart..." -cargo build --features dart,_test-utils --profile dev -cargo run --features dart,_test-utils --profile dev --bin uniffi-bindgen -- --library ../target/debug/$LIBNAME --language dart --out-dir dart/lib/ +# Keep parity with other language test scripts: include _test-utils by default. +PAYJOIN_FFI_FEATURES=${PAYJOIN_FFI_FEATURES-_test-utils} +PAYJOIN_FFI_PROFILE=${PAYJOIN_FFI_PROFILE:-dev} +if [[ $PAYJOIN_FFI_PROFILE == "dev" ]]; then + TARGET_PROFILE_DIR=debug +else + TARGET_PROFILE_DIR=$PAYJOIN_FFI_PROFILE +fi +GENERATOR_FEATURES="dart" +if [[ -n $PAYJOIN_FFI_FEATURES ]]; then + GENERATOR_FEATURES="$GENERATOR_FEATURES,$PAYJOIN_FFI_FEATURES" +fi + +cargo build --features "$GENERATOR_FEATURES" --profile "$PAYJOIN_FFI_PROFILE" +cargo run --features "$GENERATOR_FEATURES" --profile dev --bin uniffi-bindgen -- --library "../target/$TARGET_PROFILE_DIR/$LIBNAME" --language dart --out-dir dart/lib/ echo "All done!" From b4144b7c205be8763578c043415b458fda7127ce Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 22:46:16 +0000 Subject: [PATCH 07/12] Publish the dart package from CI on tag push Add the tag-gated publishing path to the Dart workflow, following the NuGet flow in csharp.yml adapted to a source-only registry: pub.dev ships Dart source plus the native/ wrapper crate and consumers compile the Rust through hook/build.dart, so there is no artifact to pack, smoke test, or attach to a GitHub release. Every run regenerates the production bindings and validates the archive with a publish dry run (contrib/prepare-publish.sh). The dry run tolerates only the known analyzer warning that generated bindings unavoidably carry; any other validation finding fails the job, since the real publish runs with --force, which would proceed over a new warning. Pushing a payjoin-dart- tag reruns tests and verification at the tagged commit, then publish-pub checks the tag against pubspec.yaml, regenerates the bindings, and publishes through pub.dev automated publishing (OIDC), so no long-lived credential exists anywhere. The job runs in the release environment for a required-reviewer gate. CONTRIBUTING.md's releasing section now documents the CI flow, the one-time pub.dev automated-publishing setup, and the manual fallback. --- .github/workflows/dart.yml | 76 +++++++++++++++++++++ payjoin-ffi/dart/CONTRIBUTING.md | 71 ++++++++++++------- payjoin-ffi/dart/contrib/prepare-publish.sh | 46 +++++++++++++ 3 files changed, 170 insertions(+), 23 deletions(-) create mode 100755 payjoin-ffi/dart/contrib/prepare-publish.sh diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 2e1159598..8b33e48e9 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -12,6 +12,14 @@ on: # change this workflow's environment. - flake.nix - flake.lock + # Publishing path. A tag push runs the build/test and archive-verification + # jobs at the tagged commit (no path filter applies to tags), then the + # tag-gated publish job below. Tag scheme: payjoin-dart-, + # e.g. payjoin-dart-0.2.1+payjoin-1.0.0-rc.8 (the full version, including + # build metadata), matching the version-first payjoin-csharp-* convention. + push: + tags: + - "payjoin-dart-[0-9]*" jobs: build-dart-and-test: @@ -29,3 +37,71 @@ jobs: uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#dart -c bash ./payjoin-ffi/dart/contrib/test.sh + + verify-pub-package: + # pub.dev ships source that consumers compile through hook/build.dart, + # so there is no artifact to pack or smoke test. + name: "Verify pub.dev package" + runs-on: ubuntu-26.04 + 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: Generate production bindings and dry-run publish + run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh + + # --------------------------------------------------------------------------- + # PUBLISH PATH (tag-gated). Runs only for a payjoin-dart-* tag push, after + # build/test and the archive verification are green. The published unit is + # the pub.dev source archive, so there is no artifact to attach to a GitHub + # release afterwards. + # --------------------------------------------------------------------------- + publish-pub: + name: "Publish to pub.dev (automated publishing / OIDC)" + runs-on: ubuntu-26.04 + needs: [build-dart-and-test, verify-pub-package] + if: startsWith(github.ref, 'refs/tags/payjoin-dart-') + # Pauses for a required reviewer before anything reaches the registry; + # the pub.dev automated-publishing config requires this environment. + 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 + 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 + + - name: Publish to pub.dev + # dart pub exchanges the job's OIDC token for short-lived pub.dev + # credentials; there is no long-lived token anywhere in this + # workflow. --force skips the interactive confirmation. + run: nix develop .#dart -c bash -c 'cd payjoin-ffi/dart && dart pub publish --force' diff --git a/payjoin-ffi/dart/CONTRIBUTING.md b/payjoin-ffi/dart/CONTRIBUTING.md index 5c4790285..b0cf93051 100644 --- a/payjoin-ffi/dart/CONTRIBUTING.md +++ b/payjoin-ffi/dart/CONTRIBUTING.md @@ -45,39 +45,64 @@ patch bump at minimum, since the same Dart API gets new behavior. ### Publishing +CI is the publish path. On every pull request touching `payjoin-ffi/**`, +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. 2. Set the version in `pubspec.yaml` and describe the consumer-visible changes under a matching heading in `CHANGELOG.md`. -3. Run the tests: `bash ./contrib/test.sh`. -4. Generate the bindings to be shipped and inspect the archive. +3. Confirm every `Build and Test Dart` job is green on the release commit + in `master`. +4. Tag that commit `payjoin-dart-`, where `` is the + `pubspec.yaml` version exactly (including the `+` build metadata), and + push the tag: ```shell - bash ./scripts/generate_bindings.sh - dart pub publish --dry-run + git tag 'payjoin-dart-0.2.1+payjoin-1.0.0-rc.8' + git push upstream 'payjoin-dart-0.2.1+payjoin-1.0.0-rc.8' ``` - `.pubignore` replaces `.gitignore` for publishing, so a gitignored file is - only kept out of the archive if `.pubignore` also lists it. Two build - artifacts decide the contents here: `lib/payjoin.dart` has to be present - and current, since it is the binding surface consumers import, and - `native/Cargo.lock` has to be absent, since publishing one resolved - against the `.cargo/config.toml` path overlay would hand consumers a - lockfile pinned to a dependency graph they cannot reproduce. Delete it - before publishing if a local build left one behind. + 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. Publish. +5. Verify the [pub.dev listing](https://pub.dev/packages/payjoin) shows the + new version and its changelog. - ```shell - dart pub publish - ``` +One-time setup is account and repository configuration, not part of the +per-release flow: on the pub.dev package admin page, enable GitHub +Actions publishing with repository `payjoin/rust-payjoin` and tag +pattern `payjoin-dart-{{version}}`, requiring the GitHub Actions +environment `release`; and create the `release` GitHub Actions +environment with required reviewers. + +#### Manual fallback + +Use only if the CI publish path is unavailable. Requires uploader rights on +the pub.dev package. + +```shell +bash ./contrib/prepare-publish.sh +dart pub publish +``` -Known limitation: `scripts/generate_bindings.sh` always builds with -`_test-utils`, so the bindings it emits declare test-only APIs such as -`TestServices` and `BitcoindEnv`. Consumers build the native library without -that feature, which leaves those declarations backed by symbols that are -absent at runtime. Every release so far ships them. Giving the script a -production mode, as `payjoin-ffi/csharp` does with `PAYJOIN_FFI_FEATURES`, -remains to be done. +The script generates the bindings in production mode +(`PAYJOIN_FFI_FEATURES=`, no test-only APIs), deletes `native/Cargo.lock`, +and runs the dry-run validation. `.pubignore` replaces `.gitignore` for +publishing, so a gitignored file is only kept out of the archive if +`.pubignore` also lists it. Two build artifacts decide the contents: +`lib/payjoin.dart` has to be present and current, since it is the binding +surface consumers import, and `native/Cargo.lock` has to be absent, since +publishing one resolved against the `.cargo/config.toml` path overlay would +hand consumers a lockfile pinned to a dependency graph they cannot +reproduce. + +[automated publishing]: https://dart.dev/tools/pub/automated-publishing diff --git a/payjoin-ffi/dart/contrib/prepare-publish.sh b/payjoin-ffi/dart/contrib/prepare-publish.sh new file mode 100755 index 000000000..1fd533e18 --- /dev/null +++ b/payjoin-ffi/dart/contrib/prepare-publish.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 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; +# consumers compile the Rust themselves via hook/build.dart, so no binaries +# are packed here. Publishing itself stays a separate step so this script +# can run anywhere, including on pull requests. + +# Build against the maintained lockfile instead of resolving the dependency +# graph fresh on every run. use_lockfile copies Cargo-recent.lock into place +# and restores the previous state when this script exits. +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "$REPO_ROOT" +source contrib/lockfile.sh +use_lockfile Cargo-recent.lock + +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 + +echo "==> Verifying the publish archive..." +# The dry run exits 65 whenever validation reports anything, and one +# warning is unavoidable: the generated bindings carry analyzer warnings +# (unused imports in uniffi-dart output, and the .pubignore'd +# lib/test_utils.dart references test-only APIs that production bindings +# omit). The real publish runs with --force, which proceeds over +# warnings, so anything beyond that known finding has to fail here. +status=0 +report="$(dart pub publish --dry-run 2>&1)" || status=$? +printf '%s\n' "$report" +if [[ $status -ne 0 && $status -ne 65 ]]; then + exit "$status" +fi +unexpected="$(grep '^\* ' <<<"$report" | grep -v "^\* \`dart analyze\` found" || true)" +if [[ -n $unexpected ]]; then + echo "Unexpected validation findings; fix them before publishing:" >&2 + printf '%s\n' "$unexpected" >&2 + exit 1 +fi From d420de785ca25ee75f0313c7ed5211400228cb56 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 21:10:22 +0000 Subject: [PATCH 08/12] Fail python macOS build when either target fails The two per-target cargo builds run in the background and were reaped with a bare `wait`, which always returns zero no matter how the children exited. Under `set -e` a failed build therefore fell through to lipo, which would reuse a stale library from an earlier build if one was present. Wait on each build's PID so either failure stops the script. --- payjoin-ffi/python/scripts/generate_bindings.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/payjoin-ffi/python/scripts/generate_bindings.sh b/payjoin-ffi/python/scripts/generate_bindings.sh index 04253d5f2..87d2cf150 100644 --- a/payjoin-ffi/python/scripts/generate_bindings.sh +++ b/payjoin-ffi/python/scripts/generate_bindings.sh @@ -36,8 +36,14 @@ if [[ $OS == "Darwin" ]]; then ensure_target aarch64-apple-darwin x86_64-apple-darwin # This is a test script the actual release should not include the test utils feature cargo build --profile dev --target aarch64-apple-darwin --features _test-utils & + aarch64_pid=$! cargo build --profile dev --target x86_64-apple-darwin --features _test-utils & - wait + x86_64_pid=$! + # A bare `wait` always returns 0; wait on each build so a failure + # cannot slip through to lipo, which would happily reuse a stale + # library from an earlier build. + wait "$aarch64_pid" + wait "$x86_64_pid" echo "Building macos fat library" lipo -create -output python/src/payjoin/$LIBNAME \ From 23ca815771d6489a7fdbed410315596c7c279c91 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 21:10:48 +0000 Subject: [PATCH 09/12] Parameterize python bindings build for release The script hardcoded --profile dev and --features _test-utils, each annotated with a reminder that a real release must not ship the test utils feature. Adopt the PAYJOIN_FFI_FEATURES / PAYJOIN_FFI_PROFILE convention the C# generate_bindings.sh already uses: features default to _test-utils and the profile to dev, so tests behave exactly as before, while a release build sets PAYJOIN_FFI_FEATURES= and PAYJOIN_FFI_PROFILE=release to get an optimized library with no test-only APIs. --- .../python/scripts/generate_bindings.sh | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/payjoin-ffi/python/scripts/generate_bindings.sh b/payjoin-ffi/python/scripts/generate_bindings.sh index 87d2cf150..055850c3d 100644 --- a/payjoin-ffi/python/scripts/generate_bindings.sh +++ b/payjoin-ffi/python/scripts/generate_bindings.sh @@ -26,18 +26,29 @@ ensure_target() { fi } +# Keep parity with other language test scripts: include _test-utils by default. +PAYJOIN_FFI_FEATURES=${PAYJOIN_FFI_FEATURES-_test-utils} +PAYJOIN_FFI_PROFILE=${PAYJOIN_FFI_PROFILE:-dev} +if [[ $PAYJOIN_FFI_PROFILE == "dev" ]]; then + TARGET_PROFILE_DIR=debug +else + TARGET_PROFILE_DIR=$PAYJOIN_FFI_PROFILE +fi +FEATURE_ARGS=() +if [[ -n $PAYJOIN_FFI_FEATURES ]]; then + FEATURE_ARGS=(--features "$PAYJOIN_FFI_FEATURES") +fi + cd ../ -# This is a test script the actual release should not include the test utils feature -cargo build --features _test-utils --profile dev -cargo run --features _test-utils --profile dev --bin uniffi-bindgen generate --library ../target/debug/$LIBNAME --language python --out-dir python/src/payjoin/ +cargo build "${FEATURE_ARGS[@]}" --profile "$PAYJOIN_FFI_PROFILE" +cargo run "${FEATURE_ARGS[@]}" --profile dev --bin uniffi-bindgen generate --library "../target/$TARGET_PROFILE_DIR/$LIBNAME" --language python --out-dir python/src/payjoin/ if [[ $OS == "Darwin" ]]; then echo "Generating native binaries..." ensure_target aarch64-apple-darwin x86_64-apple-darwin - # This is a test script the actual release should not include the test utils feature - cargo build --profile dev --target aarch64-apple-darwin --features _test-utils & + cargo build --profile "$PAYJOIN_FFI_PROFILE" --target aarch64-apple-darwin "${FEATURE_ARGS[@]}" & aarch64_pid=$! - cargo build --profile dev --target x86_64-apple-darwin --features _test-utils & + cargo build --profile "$PAYJOIN_FFI_PROFILE" --target x86_64-apple-darwin "${FEATURE_ARGS[@]}" & x86_64_pid=$! # A bare `wait` always returns 0; wait on each build so a failure # cannot slip through to lipo, which would happily reuse a stale @@ -47,17 +58,16 @@ if [[ $OS == "Darwin" ]]; then echo "Building macos fat library" lipo -create -output python/src/payjoin/$LIBNAME \ - ../target/aarch64-apple-darwin/debug/$LIBNAME \ - ../target/x86_64-apple-darwin/debug/$LIBNAME + "../target/aarch64-apple-darwin/$TARGET_PROFILE_DIR/$LIBNAME" \ + "../target/x86_64-apple-darwin/$TARGET_PROFILE_DIR/$LIBNAME" else echo "Generating native binaries..." ensure_target x86_64-unknown-linux-gnu - # This is a test script the actual release should not include the test utils feature - cargo build --profile dev --target x86_64-unknown-linux-gnu --features _test-utils + cargo build --profile "$PAYJOIN_FFI_PROFILE" --target x86_64-unknown-linux-gnu "${FEATURE_ARGS[@]}" echo "Copying payjoin_ffi binary" - cp ../target/x86_64-unknown-linux-gnu/debug/$LIBNAME python/src/payjoin/$LIBNAME + cp "../target/x86_64-unknown-linux-gnu/$TARGET_PROFILE_DIR/$LIBNAME" python/src/payjoin/$LIBNAME fi echo "All done!" From 17c85b2a8fb15a8096af0da5a19fef0460b682e0 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 22:46:16 +0000 Subject: [PATCH 10/12] Add release wheel build script for python contrib/build-wheel.sh builds the production wheel into dist/ from the maintained recent lockfile, mirroring how contrib/test.sh drives the test build: release profile, no _test-utils, then uv build. Two retags follow the build. The bindings load the bundled library through ctypes, so the wheel is retagged py3-none instead of the building interpreter's cpXY tag, letting every CPython covered by requires-python install it. On Linux, auditwheel then verifies the library's external dependencies and applies the manylinux tag the binary satisfies, since PyPI rejects raw linux_x86_64 wheels; on macOS the lipo fat dylib gets the universal2 tag. The wheel CLI and auditwheel come from the python dev shell (the uv2nix venv omits build backends). contrib/test.sh now clears dist/ before building its wheel: the release wheel carries different tags than the test build, so a leftover copy would otherwise sit beside the fresh wheel and break the install step's glob. --- flake.nix | 27 +++++--- payjoin-ffi/python/contrib/build-wheel.sh | 82 +++++++++++++++++++++++ payjoin-ffi/python/contrib/test.sh | 1 + 3 files changed, 102 insertions(+), 8 deletions(-) create mode 100755 payjoin-ffi/python/contrib/build-wheel.sh diff --git a/flake.nix b/flake.nix index 7d86c760e..a24aa0953 100644 --- a/flake.nix +++ b/flake.nix @@ -397,14 +397,16 @@ "rustfmt" "llvm-tools-preview" ]; - targets = - pkgs.lib.optionals pkgs.stdenv.isDarwin [ - "aarch64-apple-darwin" - "x86_64-apple-darwin" - ] - ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ - "x86_64-unknown-linux-gnu" - ]; + targets = [ + # On Darwin the native test builds lipo both arches; on Linux + # cargo-zigbuild cross-links the release wheel's universal2 + # dylib from these same targets. + "aarch64-apple-darwin" + "x86_64-apple-darwin" + ] + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + "x86_64-unknown-linux-gnu" + ]; }; pythonDevShell = pkgs.mkShell { @@ -416,11 +418,20 @@ uv pythonRustToolchain bzip2 # needed for some machines to have access to libzip at runtime + # Provides the wheel CLI; the uv2nix venv omits build backends. + python3Packages.wheel ] ++ lib.optionals pkgs.stdenv.isLinux [ pkg-config openssl clang + # Applies the manylinux platform tag the release wheel satisfies; + # PyPI rejects raw linux_x86_64 wheels. + auditwheel + # Cross-links the macOS release wheel against zig's bundled + # Apple SDK stubs, so the dylib records system install names + # instead of nix store paths. + cargo-zigbuild ]; env = { diff --git a/payjoin-ffi/python/contrib/build-wheel.sh b/payjoin-ffi/python/contrib/build-wheel.sh new file mode 100755 index 000000000..4622e8398 --- /dev/null +++ b/payjoin-ffi/python/contrib/build-wheel.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the production wheel for one platform into dist/: release profile, +# no test utils. Select the platform with PAYJOIN_WHEEL_PLATFORM +# (linux-x64, the default, or macos-universal2). +# +# Both wheels build on a Linux host, like the C# native assets: a dylib +# linked inside the nix dev shell on a mac records nix store install names +# that exist on no user machine, while cargo-zigbuild links against zig's +# bundled Apple SDK stubs and records the system ones. + +if [[ "$(uname -s)" != Linux ]]; then + echo "error: release wheels build on a Linux host only" >&2 + exit 1 +fi + +PLATFORM=${PAYJOIN_WHEEL_PLATFORM:-linux-x64} + +# Build against the maintained lockfile instead of resolving the dependency +# graph fresh on every run. use_lockfile copies Cargo-recent.lock into place +# and restores the previous state when this script exits. +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "$REPO_ROOT" +source contrib/lockfile.sh +use_lockfile Cargo-recent.lock + +cd "$REPO_ROOT/payjoin-ffi/python" + +echo "==> Generating production FFI bindings..." +PAYJOIN_FFI_FEATURES="" PAYJOIN_FFI_PROFILE=release bash ./scripts/generate_bindings.sh + +# generate_bindings.sh stages the host's library; make sure the wheel +# ships exactly one platform's binary. +case "$PLATFORM" in + linux-x64) + rm -f src/payjoin/libpayjoin_ffi.dylib + ;; + macos-universal2) + echo "==> Cross-compiling the universal2 macOS library..." + (cd "$REPO_ROOT/payjoin-ffi" && + cargo zigbuild --profile release --target universal2-apple-darwin) + rm -f src/payjoin/libpayjoin_ffi.so + cp "$REPO_ROOT/target/universal2-apple-darwin/release/libpayjoin_ffi.dylib" \ + src/payjoin/ + ;; + *) + echo "error: unsupported PAYJOIN_WHEEL_PLATFORM: $PLATFORM" >&2 + exit 1 + ;; +esac + +echo "==> Building the wheel..." +# Drop setuptools' build/ staging dir too: it survives across runs and +# would leak the other platform's library into this wheel. +rm -rf build dist +uv build --wheel + +# The generated bindings load the bundled library through ctypes, so the +# wheel does not depend on a CPython ABI; setup.py tags it with the +# building interpreter's version only because has_ext_modules marks the +# wheel platform-specific. +echo "==> Retagging the wheel..." +wheel tags --python-tag py3 --abi-tag none --remove dist/*.whl + +if [[ $PLATFORM == linux-x64 ]]; then + # PyPI rejects the raw linux_x86_64 platform tag. auditwheel verifies + # the library's external dependencies and applies the manylinux tag the + # binary actually satisfies; nothing is grafted into the wheel since + # the library links only glibc. + auditwheel repair --wheel-dir dist dist/*-linux_x86_64.whl + rm dist/*-linux_x86_64.whl +else + # setup.py tagged the wheel with the build machine's (Linux) platform. + # The macOS floor is zig's: its bundled SDK stubs currently support + # macOS >= 13, and that is the minos both slices record. Keep this tag + # in sync with the LC_BUILD_VERSION of the built dylib if zig moves. + wheel tags --platform-tag macosx_13_0_universal2 --remove dist/*.whl +fi + +echo "==> Built wheel:" +ls -l dist diff --git a/payjoin-ffi/python/contrib/test.sh b/payjoin-ffi/python/contrib/test.sh index ac43385d0..03ebd007c 100755 --- a/payjoin-ffi/python/contrib/test.sh +++ b/payjoin-ffi/python/contrib/test.sh @@ -18,6 +18,7 @@ echo "==> Generating FFI bindings..." bash ./scripts/generate_bindings.sh echo "==> Building wheel..." +rm -rf dist uv build --wheel echo "==> Installing wheel..." From 00b7c2e291d63bcf8468d9cd53312f5c6e766941 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 11 Aug 2026 22:46:16 +0000 Subject: [PATCH 11/12] Publish the python wheels from CI on tag push Add the tag-gated publishing path to the Python workflow, mirroring the NuGet flow in csharp.yml: every run builds the release wheels (manylinux x86_64 and macOS universal2) and smoke-installs them on Linux, arm64 macOS, and Intel macOS with a CPython other than the one they were built with, proving the py3-none retag. Pushing a payjoin-python- tag reruns the graph at the tagged commit, then publishes the smoke-tested wheels to PyPI and attaches them to the GitHub release. The publish job verifies the tag matches the wheel versions (the version is the payjoin-ffi crate version, read by setup.py), attests build provenance, and uploads through PyPI trusted publishing (OIDC) with PEP 740 attestations, so no long-lived registry token exists anywhere. It runs in the release environment for a required-reviewer gate and also waits for the build-and-test jobs at the tagged commit before anything reaches the registry. RELEASING.md records the release steps and the one-time PyPI trusted-publisher and environment setup. --- .github/workflows/python.yml | 165 ++++++++++++++++++++++++++++++++ payjoin-ffi/python/RELEASING.md | 81 ++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 payjoin-ffi/python/RELEASING.md diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index aebbe9cf2..fd39f61d0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -12,6 +12,15 @@ on: # change this workflow's environment. - flake.nix - flake.lock + # Publishing path. A tag push runs the full build/wheel/smoke graph at the + # tagged commit (no path filter applies to tags), then the tag-gated + # publish/release jobs below. Tag scheme: payjoin-python-, e.g. + # payjoin-python-0.24.0, matching the version-first payjoin-csharp-* + # convention. The version is the payjoin-ffi crate version: setup.py + # derives the wheel version from payjoin-ffi/Cargo.toml. + push: + tags: + - "payjoin-python-[0-9]*" jobs: build-python-and-test: @@ -29,3 +38,159 @@ jobs: uses: ./.github/actions/setup-nix - name: "Build and test" run: nix develop .#python --command bash ./payjoin-ffi/python/contrib/test.sh + + build-wheel: + # The wheel bundles a prebuilt native library, so each supported + # platform packs its own wheel: x86_64 on Linux (retagged manylinux by + # auditwheel) and a fat universal2 dylib for macOS. Both wheels build + # on a Linux host, like the C# native assets: a dylib linked inside + # the nix dev shell on a mac records nix store install names that + # exist on no user machine, while cargo-zigbuild links against zig's + # bundled Apple SDK stubs and records the system ones. The smoke jobs + # below verify each wheel on real hardware. + name: "Build wheel" + runs-on: ubuntu-26.04 + strategy: + # Let every platform report its own result; a failure in one target + # should not cancel the signal from the others. + fail-fast: false + matrix: + platform: [linux-x64, macos-universal2] + 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: Build the wheel + run: nix develop .#python --command bash ./payjoin-ffi/python/contrib/build-wheel.sh + env: + PAYJOIN_WHEEL_PLATFORM: ${{ matrix.platform }} + - name: Upload wheel + uses: actions/upload-artifact@v4 + with: + name: payjoin-python-wheel-${{ matrix.platform }} + path: payjoin-ffi/python/dist/*.whl + if-no-files-found: error + + smoke-wheel: + name: "Smoke test wheel" + runs-on: ${{ matrix.os }} + needs: build-wheel + strategy: + matrix: + os: [ubuntu-26.04, macos-latest, macos-15-intel] + steps: + - name: Download wheels + uses: actions/download-artifact@v4 + with: + pattern: payjoin-python-wheel-* + merge-multiple: true + path: dist + - name: Install Python + uses: actions/setup-python@v5 + with: + # Deliberately not the version the wheel was built with: the + # wheels are tagged py3-none (the bindings load the library + # through ctypes), and installing on a different CPython proves + # the retag. + python-version: "3.12" + - name: Install and exercise the wheel + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + pkgs=(dist/payjoin-*.whl) + # download-artifact succeeds even when the pattern matches nothing. + if [ "${#pkgs[@]}" -eq 0 ]; then + echo "::error::no payjoin wheels found in dist/" + exit 1 + fi + version="$(basename "${pkgs[0]}" | cut -d- -f2)" + # pip picks whichever wheel matches this runner's platform; + # dependencies come from PyPI. + python -m pip install --only-binary payjoin --find-links dist "payjoin==$version" + python -c ' + import payjoin + payjoin.Url.parse("bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com") + print("smoke ok") + ' + + # --------------------------------------------------------------------------- + # PUBLISH PATH (tag-gated). Runs only for a payjoin-python-* tag push, after + # build/test, both wheel builds, and every smoke job are green. Consumes the + # already-built, already-smoke-tested wheels and never rebuilds. + # --------------------------------------------------------------------------- + publish-pypi: + name: "Publish to PyPI (trusted publishing / OIDC)" + runs-on: ubuntu-26.04 + needs: [build-python-and-test, build-wheel, smoke-wheel] + if: startsWith(github.ref, 'refs/tags/payjoin-python-') + # Pauses for a required reviewer before anything reaches the registry; + # the PyPI trusted-publishing policy is bound to this environment. + 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 + # Exchanges the job's OIDC token for short-lived registry + # credentials and uploads PEP 740 attestations alongside the + # wheels; there is no long-lived token anywhere in this workflow. + 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/payjoin-ffi/python/RELEASING.md b/payjoin-ffi/python/RELEASING.md new file mode 100644 index 000000000..c4e64d2c2 --- /dev/null +++ b/payjoin-ffi/python/RELEASING.md @@ -0,0 +1,81 @@ +# Releasing the payjoin Python package + +Maintainer documentation for publishing the `payjoin` package to +[PyPI](https://pypi.org/project/payjoin/). Consumer documentation lives in +[`README.md`](README.md). + +## Versioning + +- The package version is the `payjoin-ffi` crate version: `setup.py` reads + it from `payjoin-ffi/Cargo.toml` at build time, so a release always + requires the crate version to be correct first. +- There is no separate Python version to maintain: the publish job derives + the version from the built wheels and refuses to publish if it does not + match the pushed tag. + +## Producing the wheels + +CI is the release path. On every pull request touching `payjoin-ffi/**`, +the `Build and Test Python` workflow builds release wheels with +[`contrib/build-wheel.sh`](contrib/build-wheel.sh) (release profile, no +`_test-utils`) and smoke-installs them on every supported platform: + +- `manylinux` x86_64, tagged by auditwheel with the glibc floor the binary + actually satisfies; +- macOS `universal2` (a fat x86_64 + arm64 dylib), cross-compiled from the + Linux host with cargo-zigbuild like the C# native assets, so the dylib + links the Apple SDK stubs zig bundles and records system install names + rather than nix store paths. + +The wheels are tagged `py3-none` because the generated bindings load the +bundled library through `ctypes` and do not depend on a CPython ABI; any +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 `payjoin-python-`, where `` is the + `payjoin-ffi` crate version exactly, and push the tag: + + ```shell + git tag payjoin-python-0.24.0 + git push upstream payjoin-python-0.24.0 + ``` + + 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`; never place a GPG key on a runner. +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. + +One-time setup is account and repository configuration, not part of the +per-release flow: the PyPI trusted publisher for the `payjoin` project +(GitHub Actions, repository `payjoin/rust-payjoin`, workflow file +`python.yml`, environment `release`), and the `release` GitHub Actions +environment with required reviewers. + +## Manual fallback + +Use only if the CI publish path is unavailable. Requires maintainer rights +on the PyPI project and an account with two-factor authentication. Build +each platform's wheel on matching hardware, since a wheel only bundles +the native library built on that host: + +```shell +nix develop .#python -c bash ./payjoin-ffi/python/contrib/build-wheel.sh +uv publish payjoin-ffi/python/dist/*.whl +``` + +A manual upload carries no attestations; prefer republishing through CI +for anything consumers will install. From 81eee0bd82a285fc24afb5bb2bf1c13ba49737e2 Mon Sep 17 00:00:00 2001 From: spacebear Date: Thu, 13 Aug 2026 15:49:42 +0000 Subject: [PATCH 12/12] Declare only runtime dependencies for the python package The wheel advertised build, setuptools, wheel, semantic-version, and typing-extensions as exact-pinned runtime requirements, though the package imports none of them: the generated bindings use only the standard library and http.py needs httpx. Every consumer pulled in build tooling, and the stale exact pins (typing-extensions 4.0.1 is from 2021) conflict with any package wanting a newer release. Keep httpx as the only runtime dependency, loosened to a compatible range for the same reason, and leave the build backend's requirements to build-system.requires, which already lists them. Regenerate uv.lock accordingly. --- payjoin-ffi/python/pyproject.toml | 9 +-- payjoin-ffi/python/uv.lock | 117 +----------------------------- 2 files changed, 2 insertions(+), 124 deletions(-) diff --git a/payjoin-ffi/python/pyproject.toml b/payjoin-ffi/python/pyproject.toml index d3383ec09..8ac9a8b63 100644 --- a/payjoin-ffi/python/pyproject.toml +++ b/payjoin-ffi/python/pyproject.toml @@ -9,14 +9,7 @@ readme = "README.md" requires-python = ">=3.10" license = "MIT" dynamic = ["version"] -dependencies = [ - "build==1.3.0", - "semantic-version==2.9.0", - "setuptools==83.0.0", - "typing-extensions==4.0.1", - "wheel==0.46.3", - "httpx==0.28.1", -] +dependencies = ["httpx>=0.28.1,<1.0"] [tool.setuptools] packages = ["payjoin"] diff --git a/payjoin-ffi/python/uv.lock b/payjoin-ffi/python/uv.lock index 89888e3ee..591efc5b4 100644 --- a/payjoin-ffi/python/uv.lock +++ b/payjoin-ffi/python/uv.lock @@ -16,22 +16,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/4f/d010eca6914703d8e6be222165d02c3e708ed909cdb2b7af3743667f302e/anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f", size = 83924, upload-time = "2023-11-22T23:23:52.595Z" }, ] -[[package]] -name = "build" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, -] - [[package]] name = "certifi" version = "2025.8.3" @@ -41,15 +25,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -105,37 +80,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - [[package]] name = "payjoin" source = { editable = "." } dependencies = [ - { name = "build" }, { name = "httpx" }, - { name = "semantic-version" }, - { name = "setuptools" }, - { name = "typing-extensions" }, - { name = "wheel" }, ] [package.dev-dependencies] @@ -145,14 +94,7 @@ dev = [ ] [package.metadata] -requires-dist = [ - { name = "build", specifier = "==1.3.0" }, - { name = "httpx", specifier = "==0.28.1" }, - { name = "semantic-version", specifier = "==2.9.0" }, - { name = "setuptools", specifier = "==83.0.0" }, - { name = "typing-extensions", specifier = "==4.0.1" }, - { name = "wheel", specifier = "==0.46.3" }, -] +requires-dist = [{ name = "httpx", specifier = ">=0.28.1,<1.0" }] [package.metadata.requires-dev] dev = [ @@ -169,33 +111,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, -] - -[[package]] -name = "semantic-version" -version = "2.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/56/4aa487b46d09646eb1863faa7026551d8309ece2281794bf13b20f28ab94/semantic_version-2.9.0.tar.gz", hash = "sha256:abf54873553e5e07a6fd4d5f653b781f5ae41297a493666b59dcf214006a12b2", size = 51123, upload-time = "2022-02-06T18:45:20.132Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/ac/df31047966c4d0293e7bd16276ebc9f6654de36ad8e19061a09369380c0a/semantic_version-2.9.0-py2.py3-none-any.whl", hash = "sha256:db2504ab37902dd2c9876ece53567aa43a5b2a417fbe188097b2048fff46da3d", size = 15357, upload-time = "2022-02-06T18:45:17.996Z" }, -] - -[[package]] -name = "setuptools" -version = "83.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -253,27 +168,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/4a/60ba3706797b878016f16edc5fbaf1e222109e38d0fa4d7d9312cb53f8dd/typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", size = 22706, upload-time = "2021-12-01T01:45:39.576Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e4/baf0031e39cf545f0c9edd5b1a2ea12609b7fcba2d58e118b11753d68cf0/typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b", size = 22816, upload-time = "2021-12-01T01:45:37.772Z" }, -] - -[[package]] -name = "wheel" -version = "0.46.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, -] - [[package]] name = "yapf" version = "0.43.0" @@ -286,12 +180,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25 wheels = [ { url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158, upload-time = "2024-11-14T00:11:39.37Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -]