diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index c08ce696d..4b8369905 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -12,13 +12,6 @@ on: # the flake 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-csharp-, - # e.g. payjoin-csharp-0.24.0-preview.1. The [0-9] anchor keeps this glob - # collision-free against the repo's existing payjoin-* and payjoin-cli-* - # tags (which a payjoin-csharp-* prefix cannot match anyway) while dropping - # the `v` so the scheme matches the version-first convention already in use. push: tags: - "payjoin-csharp-[0-9]*" @@ -254,12 +247,6 @@ jobs: # stays the only place the package version is maintained. run: bash ./scripts/smoke_nuget_package.sh artifacts/packages auto ${{ matrix.rid }} - # --------------------------------------------------------------------------- - # PUBLISH PATH (tag-gated). Runs only for a payjoin-csharp-* tag push, after - # the tag passes the hygiene checks and pack + every per-RID smoke job are - # green. Consumes the already-built, already-smoke-tested - # `payjoin-csharp-nuget-package` artifact — never repacks. - # --------------------------------------------------------------------------- verify-tag: name: "Verify release tag" if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') @@ -272,10 +259,6 @@ jobs: runs-on: ubuntu-latest needs: [pack-nuget, smoke-nuget, verify-tag] if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') - # Optional but recommended: a GitHub Actions environment lets you bind the - # nuget.org policy to `release` AND add required reviewers, turning - # publish into a manual-approval gate. Create the environment first, or - # remove this line to publish without an approval gate. See README. environment: release permissions: id-token: write # OIDC: used by BOTH NuGet/login and attest-build-provenance @@ -322,10 +305,6 @@ jobs: - 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 - # (To attest each native .so/.dylib/.dll as an independent subject as - # well, add a second attest step with subject-checksums pointing at the - # SHA256SUMS produced in github-release. Not required — the nupkg is the - # published unit.) uses: actions/attest-build-provenance@v4 with: subject-path: ${{ steps.locate.outputs.nupkg }} @@ -334,19 +313,10 @@ jobs: id: login uses: NuGet/login@v1 with: - # nuget.org username (profile name), NOT an email. Individual member of - # the org that owns the Trusted Publishing policy. Kept in a secret only - # to avoid leaking the account name in logs — it is NOT a credential; - # there is no long-lived API key anywhere in this workflow. user: ${{ secrets.NUGET_USER }} - name: Push to nuget.org run: | - # --skip-duplicate keeps re-runs idempotent: if a prior run published - # this version but a later job (github-release) failed, re-triggering - # 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.locate.outputs.nupkg }}" \ --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \ --source https://api.nuget.org/v3/index.json \ diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 2e1159598..284f8d836 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -12,6 +12,9 @@ on: # change this workflow's environment. - flake.nix - flake.lock + push: + tags: + - "payjoin-dart-[0-9]*" jobs: build-dart-and-test: @@ -29,3 +32,65 @@ 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: + 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 + + 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 + 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 + run: nix develop .#dart -c bash -c 'cd payjoin-ffi/dart && dart pub publish --force' diff --git a/payjoin-ffi/csharp/RELEASING.md b/payjoin-ffi/csharp/RELEASING.md index 6d97c1afe..44a478e1f 100644 --- a/payjoin-ffi/csharp/RELEASING.md +++ b/payjoin-ffi/csharp/RELEASING.md @@ -126,42 +126,4 @@ is ever stored. The workflow is the attestation: `gh attestation verify Payjoin..nupkg -R payjoin/rust-payjoin`. -One-time setup — the nuget.org Trusted Publishing policy (bound to -`payjoin/rust-payjoin`, workflow file `csharp.yml`, environment -`release`), the `release` GitHub Actions environment with required -reviewers, and the `NUGET_USER` secret (the publishing member's nuget.org -profile name) — is a one-time account and repository configuration, not part -of the per-release flow. - -## Manual fallback - -Use only if the CI publish path is unavailable. Requires a maintainer with -nuget.org ownership of the `Payjoin` package ID. - -1. Work through the release readiness checklist above. -2. Download the CI-built `payjoin-csharp-nuget-package` artifact from the - workflow run on the release commit (do not pack from a development machine — - a local pack only contains the native assets present on that host), then - push it: - - ```shell - dotnet nuget push Payjoin..nupkg \ - --source https://api.nuget.org/v3/index.json \ - --api-key - ``` - - Prefer a scoped, push-only, short-expiry key per [scoped API keys], or the - `NUGET_API_KEY` environment variable (.NET SDK 10.0.300+) so the key never - appears in shell history. - -3. Complete the post-publish verification section of the checklist. - -[SemVer]: https://semver.org/ -[package versioning]: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning -[publish guide]: https://learn.microsoft.com/en-us/nuget/nuget-org/publish-a-package -[package authoring best practices]: https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices -[native library packaging]: https://learn.microsoft.com/en-us/nuget/create-packages/native-files-in-net-packages -[readme preview]: https://learn.microsoft.com/en-us/nuget/nuget-org/package-readme-on-nuget-org -[scoped API keys]: https://learn.microsoft.com/en-us/nuget/nuget-org/scoped-api-keys [trusted publishing]: https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing -[`NuGet/login`]: https://github.com/NuGet/login diff --git a/payjoin-ffi/csharp/scripts/generate_bindings.ps1 b/payjoin-ffi/csharp/scripts/generate_bindings.ps1 index c6aa74dd8..3adc7d8cd 100644 --- a/payjoin-ffi/csharp/scripts/generate_bindings.ps1 +++ b/payjoin-ffi/csharp/scripts/generate_bindings.ps1 @@ -67,7 +67,6 @@ if ($ProductionBindings) { } elseif ($null -ne $env:PAYJOIN_FFI_FEATURES) { $payjoinFfiFeatures = $env:PAYJOIN_FFI_FEATURES } else { - # Keep parity with other language test scripts: include _test-utils by default. $payjoinFfiFeatures = "_test-utils" } diff --git a/payjoin-ffi/csharp/scripts/generate_bindings.sh b/payjoin-ffi/csharp/scripts/generate_bindings.sh index 6fd7af2f0..7826fe30d 100755 --- a/payjoin-ffi/csharp/scripts/generate_bindings.sh +++ b/payjoin-ffi/csharp/scripts/generate_bindings.sh @@ -29,7 +29,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR/../.." echo "Generating payjoin C#..." -# 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 diff --git a/payjoin-ffi/dart/CONTRIBUTING.md b/payjoin-ffi/dart/CONTRIBUTING.md index 5c4790285..47fedc818 100644 --- a/payjoin-ffi/dart/CONTRIBUTING.md +++ b/payjoin-ffi/dart/CONTRIBUTING.md @@ -45,39 +45,38 @@ 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). The + tag must be annotated and signed by a maintainer key in + `contrib/release/keys/`, and the tagged commit must be on `master`; + `verify-tag` refuses to publish otherwise. ```shell - bash ./scripts/generate_bindings.sh - dart pub publish --dry-run + git tag -s 'payjoin-dart-0.2.1+payjoin-1.0.0-rc.8' -m '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. - -5. Publish. + 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. - ```shell - dart pub publish - ``` +5. Verify the [pub.dev listing](https://pub.dev/packages/payjoin) shows the + new version and its changelog. -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. +[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 diff --git a/payjoin-ffi/dart/scripts/generate_bindings.sh b/payjoin-ffi/dart/scripts/generate_bindings.sh index 1eae49889..939a6c840 100755 --- a/payjoin-ffi/dart/scripts/generate_bindings.sh +++ b/payjoin-ffi/dart/scripts/generate_bindings.sh @@ -19,7 +19,19 @@ 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/ +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!"