Skip to content

Commit 2e513dc

Browse files
committed
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-<version> 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. A verify-tag job calls the shared verify-tag-hygiene workflow, so publishing requires a tag that is annotated, signed by a maintainer key in contrib/release/keys/, and an ancestor of master, and a bad tag fails before the release environment asks for approval. CONTRIBUTING.md's releasing section now documents the CI flow, the one-time pub.dev automated-publishing setup, and the manual fallback.
1 parent aea6325 commit 2e513dc

3 files changed

Lines changed: 179 additions & 23 deletions

File tree

‎.github/workflows/dart.yml‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ on:
1212
# change this workflow's environment.
1313
- flake.nix
1414
- flake.lock
15+
# Publishing path. A tag push runs the build/test and archive-verification
16+
# jobs at the tagged commit (no path filter applies to tags), then the
17+
# tag-gated publish job below. Tag scheme: payjoin-dart-<pubspec version>,
18+
# e.g. payjoin-dart-0.2.1+payjoin-1.0.0-rc.8 (the full version, including
19+
# build metadata), matching the version-first payjoin-csharp-* convention.
20+
push:
21+
tags:
22+
- "payjoin-dart-[0-9]*"
1523

1624
jobs:
1725
build-dart-and-test:
@@ -29,3 +37,78 @@ jobs:
2937
uses: ./.github/actions/setup-nix
3038
- name: "Build and test"
3139
run: nix develop .#dart -c bash ./payjoin-ffi/dart/contrib/test.sh
40+
41+
verify-pub-package:
42+
# pub.dev ships source that consumers compile through hook/build.dart,
43+
# so there is no artifact to pack or smoke test.
44+
name: "Verify pub.dev package"
45+
runs-on: ubuntu-26.04
46+
steps:
47+
- name: Checkout
48+
uses: actions/checkout@v6
49+
- name: "Use cache"
50+
uses: Swatinem/rust-cache@v2
51+
- name: Set up nix
52+
uses: ./.github/actions/setup-nix
53+
- name: Generate production bindings and dry-run publish
54+
run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh
55+
56+
# ---------------------------------------------------------------------------
57+
# PUBLISH PATH (tag-gated). Runs only for a payjoin-dart-* tag push, after
58+
# the tag passes the hygiene checks and build/test and the archive
59+
# verification are green. The published unit is the pub.dev source archive,
60+
# so there is no artifact to attach to a GitHub release afterwards.
61+
# ---------------------------------------------------------------------------
62+
verify-tag:
63+
name: "Verify release tag"
64+
if: startsWith(github.ref, 'refs/tags/payjoin-dart-')
65+
permissions:
66+
contents: read
67+
uses: ./.github/workflows/verify-tag-hygiene.yml
68+
69+
publish-pub:
70+
name: "Publish to pub.dev (automated publishing / OIDC)"
71+
runs-on: ubuntu-26.04
72+
needs: [build-dart-and-test, verify-pub-package, verify-tag]
73+
if: startsWith(github.ref, 'refs/tags/payjoin-dart-')
74+
# Pauses for a required reviewer before anything reaches the registry;
75+
# the pub.dev automated-publishing config requires this environment.
76+
environment: release
77+
permissions:
78+
id-token: write # OIDC: pub.dev automated publishing
79+
contents: read
80+
steps:
81+
- name: Checkout
82+
uses: actions/checkout@v6
83+
84+
- name: "Use cache"
85+
uses: Swatinem/rust-cache@v2
86+
- name: Set up nix
87+
uses: ./.github/actions/setup-nix
88+
89+
- name: Read pubspec version
90+
id: pubspec
91+
shell: bash
92+
run: |
93+
set -euo pipefail
94+
version="$(sed -n 's/^version: *//p' payjoin-ffi/dart/pubspec.yaml)"
95+
if [ -z "$version" ]; then
96+
echo "::error::could not read version from pubspec.yaml"
97+
exit 1
98+
fi
99+
echo "version=$version" >>"$GITHUB_OUTPUT"
100+
101+
- name: Verify tag matches pubspec version
102+
uses: ./.github/actions/verify-tag-version
103+
with:
104+
tag-prefix: payjoin-dart-
105+
version: ${{ steps.pubspec.outputs.version }}
106+
107+
- name: Prepare the package
108+
run: nix develop .#dart -c ./payjoin-ffi/dart/contrib/prepare-publish.sh
109+
110+
- name: Publish to pub.dev
111+
# dart pub exchanges the job's OIDC token for short-lived pub.dev
112+
# credentials; there is no long-lived token anywhere in this
113+
# workflow. --force skips the interactive confirmation.
114+
run: nix develop .#dart -c bash -c 'cd payjoin-ffi/dart && dart pub publish --force'

‎payjoin-ffi/dart/CONTRIBUTING.md‎

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,39 +45,66 @@ patch bump at minimum, since the same Dart API gets new behavior.
4545

4646
### Publishing
4747

48+
CI is the publish path. On every pull request touching `payjoin-ffi/**`,
49+
the `Build and Test Dart` workflow regenerates the production bindings and
50+
validates the archive with a publish dry run
51+
([`contrib/prepare-publish.sh`](contrib/prepare-publish.sh)).
52+
4853
1. Point the `payjoin-ffi` dependency in `native/Cargo.toml` at the commit
4954
tagged for the `payjoin` release being wrapped. Consumers build from that
5055
revision. `.cargo/config.toml` redirects it to the local workspace for
5156
development only, and `.pubignore` withholds that file from the archive.
5257
2. Set the version in `pubspec.yaml` and describe the consumer-visible
5358
changes under a matching heading in `CHANGELOG.md`.
54-
3. Run the tests: `bash ./contrib/test.sh`.
55-
4. Generate the bindings to be shipped and inspect the archive.
59+
3. Confirm every `Build and Test Dart` job is green on the release commit
60+
in `master`.
61+
4. Tag that commit `payjoin-dart-<version>`, where `<version>` is the
62+
`pubspec.yaml` version exactly (including the `+` build metadata). The
63+
tag must be annotated and signed by a maintainer key in
64+
`contrib/release/keys/`, and the tagged commit must be on `master`;
65+
`verify-tag` refuses to publish otherwise.
5666

5767
```shell
58-
bash ./scripts/generate_bindings.sh
59-
dart pub publish --dry-run
68+
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'
69+
git push upstream 'payjoin-dart-0.2.1+payjoin-1.0.0-rc.8'
6070
```
6171

62-
`.pubignore` replaces `.gitignore` for publishing, so a gitignored file is
63-
only kept out of the archive if `.pubignore` also lists it. Two build
64-
artifacts decide the contents here: `lib/payjoin.dart` has to be present
65-
and current, since it is the binding surface consumers import, and
66-
`native/Cargo.lock` has to be absent, since publishing one resolved
67-
against the `.cargo/config.toml` path overlay would hand consumers a
68-
lockfile pinned to a dependency graph they cannot reproduce. Delete it
69-
before publishing if a local build left one behind.
72+
The tag reruns the tests and the archive verification at the tagged
73+
commit, then `publish-pub` verifies the tag matches `pubspec.yaml`,
74+
regenerates the production bindings, and publishes through pub.dev
75+
[automated publishing] (OIDC), so no long-lived credential is stored
76+
anywhere. The job runs in the `release` environment: approve the paused
77+
run before anything reaches the registry.
7078

71-
5. Publish.
79+
5. Verify the [pub.dev listing](https://pub.dev/packages/payjoin) shows the
80+
new version and its changelog.
7281

73-
```shell
74-
dart pub publish
75-
```
82+
One-time setup is account and repository configuration, not part of the
83+
per-release flow: on the pub.dev package admin page, enable GitHub
84+
Actions publishing with repository `payjoin/rust-payjoin` and tag
85+
pattern `payjoin-dart-{{version}}`, requiring the GitHub Actions
86+
environment `release`; and create the `release` GitHub Actions
87+
environment with required reviewers.
88+
89+
#### Manual fallback
90+
91+
Use only if the CI publish path is unavailable. Requires uploader rights on
92+
the pub.dev package.
93+
94+
```shell
95+
bash ./contrib/prepare-publish.sh
96+
dart pub publish
97+
```
7698

77-
Known limitation: `scripts/generate_bindings.sh` always builds with
78-
`_test-utils`, so the bindings it emits declare test-only APIs such as
79-
`TestServices` and `BitcoindEnv`. Consumers build the native library without
80-
that feature, which leaves those declarations backed by symbols that are
81-
absent at runtime. Every release so far ships them. Giving the script a
82-
production mode, as `payjoin-ffi/csharp` does with `PAYJOIN_FFI_FEATURES`,
83-
remains to be done.
99+
The script generates the bindings in production mode
100+
(`PAYJOIN_FFI_FEATURES=`, no test-only APIs), deletes `native/Cargo.lock`,
101+
and runs the dry-run validation. `.pubignore` replaces `.gitignore` for
102+
publishing, so a gitignored file is only kept out of the archive if
103+
`.pubignore` also lists it. Two build artifacts decide the contents:
104+
`lib/payjoin.dart` has to be present and current, since it is the binding
105+
surface consumers import, and `native/Cargo.lock` has to be absent, since
106+
publishing one resolved against the `.cargo/config.toml` path overlay would
107+
hand consumers a lockfile pinned to a dependency graph they cannot
108+
reproduce.
109+
110+
[automated publishing]: https://dart.dev/tools/pub/automated-publishing
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Prepare the package for publishing to pub.dev and verify the archive with
5+
# a dry run. The archive ships Dart source plus the native/ wrapper crate;
6+
# consumers compile the Rust themselves via hook/build.dart, so no binaries
7+
# are packed here. Publishing itself stays a separate step so this script
8+
# can run anywhere, including on pull requests.
9+
10+
# Build against the maintained lockfile instead of resolving the dependency
11+
# graph fresh on every run. use_lockfile copies Cargo-recent.lock into place
12+
# and restores the previous state when this script exits.
13+
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
14+
cd "$REPO_ROOT"
15+
source contrib/lockfile.sh
16+
use_lockfile Cargo-recent.lock
17+
18+
cd "$REPO_ROOT/payjoin-ffi/dart"
19+
20+
echo "==> Generating production FFI bindings..."
21+
PAYJOIN_FFI_FEATURES="" bash ./scripts/generate_bindings.sh
22+
23+
# A Cargo.lock resolved against the .cargo/config.toml path overlay would
24+
# hand consumers a dependency graph they cannot reproduce.
25+
echo "==> Cleaning nested Cargo.lock..."
26+
rm -f native/Cargo.lock
27+
28+
echo "==> Verifying the publish archive..."
29+
# The dry run exits 65 whenever validation reports anything, and one
30+
# warning is unavoidable: the generated bindings carry analyzer warnings
31+
# (unused imports in uniffi-dart output, and the .pubignore'd
32+
# lib/test_utils.dart references test-only APIs that production bindings
33+
# omit). The real publish runs with --force, which proceeds over
34+
# warnings, so anything beyond that known finding has to fail here.
35+
status=0
36+
report="$(dart pub publish --dry-run 2>&1)" || status=$?
37+
printf '%s\n' "$report"
38+
if [[ $status -ne 0 && $status -ne 65 ]]; then
39+
exit "$status"
40+
fi
41+
unexpected="$(grep '^\* ' <<<"$report" | grep -v "^\* \`dart analyze\` found" || true)"
42+
if [[ -n $unexpected ]]; then
43+
echo "Unexpected validation findings; fix them before publishing:" >&2
44+
printf '%s\n' "$unexpected" >&2
45+
exit 1
46+
fi

0 commit comments

Comments
 (0)