Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions .github/workflows/csharp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<csproj Version>,
# 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]*"
Expand Down Expand Up @@ -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-')
Expand All @@ -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
Expand Down Expand Up @@ -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 <file>.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 }}
Expand All @@ -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 \
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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'
38 changes: 0 additions & 38 deletions payjoin-ffi/csharp/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,42 +126,4 @@ is ever stored. The workflow is
the attestation:
`gh attestation verify Payjoin.<version>.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.<version>.nupkg \
--source https://api.nuget.org/v3/index.json \
--api-key <nuget.org 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
1 change: 0 additions & 1 deletion payjoin-ffi/csharp/scripts/generate_bindings.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
1 change: 0 additions & 1 deletion payjoin-ffi/csharp/scripts/generate_bindings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 23 additions & 24 deletions payjoin-ffi/dart/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>`, where `<version>` 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
46 changes: 46 additions & 0 deletions payjoin-ffi/dart/contrib/prepare-publish.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 14 additions & 2 deletions payjoin-ffi/dart/scripts/generate_bindings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
Loading