diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml index 517e73a74..385c03b21 100644 --- a/.github/workflows/crates-release.yml +++ b/.github/workflows/crates-release.yml @@ -118,7 +118,7 @@ jobs: - name: Package the crate run: cargo package --locked -p "$CRATE" - name: Attest build provenance - uses: actions/attest-build-provenance@v4 + uses: actions/attest@v4 with: subject-path: target/package/${{ needs.verify-tag.outputs.crate }}-${{ needs.verify-tag.outputs.version }}.crate - name: Authenticate to crates.io diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 4b8369905..d9e6cc461 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -261,8 +261,8 @@ jobs: if: startsWith(github.ref, 'refs/tags/payjoin-csharp-') environment: release permissions: - id-token: write # OIDC: used by BOTH NuGet/login and attest-build-provenance - attestations: write # actions/attest-build-provenance writes the attestation + id-token: write # OIDC: used by BOTH NuGet/login and actions/attest + attestations: write # actions/attest writes the attestation contents: read # needed only to check out the in-repo verify-tag-version action steps: - name: Checkout @@ -305,7 +305,7 @@ 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 - uses: actions/attest-build-provenance@v4 + uses: actions/attest@v4 with: subject-path: ${{ steps.locate.outputs.nupkg }} diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index bcefffe5b..c7bf11f4d 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -12,6 +12,9 @@ on: # change this workflow's environment. - flake.nix - flake.lock + push: + tags: + - "payjoin-javascript-[0-9]*" jobs: build-js-and-test: @@ -29,3 +32,135 @@ 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"); + ' + + verify-tag: + name: "Verify release tag" + if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') + permissions: + contents: read + uses: ./.github/workflows/verify-tag-hygiene.yml + + publish-npm: + name: "Publish to npmjs.com (trusted publishing / OIDC)" + runs-on: ubuntu-26.04 + needs: [build-js-and-test, pack-npm, smoke-npm, verify-tag] + if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') + environment: release + permissions: + id-token: write # OIDC: npm trusted publishing and its provenance statement + attestations: write # actions/attest writes the attestation + contents: read # needed only to check out the in-repo verify-tag-version action + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Download npm package + uses: actions/download-artifact@v4 + with: + name: payjoin-javascript-npm-package + path: dist + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + + - name: Update npm + run: | + npm install -g npm@11 + npm --version + + - name: Locate packed artifact + id: locate + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + pkgs=(dist/payjoin-*.tgz) + if [ "${#pkgs[@]}" -ne 1 ]; then + echo "::error::expected exactly one .tgz in dist/, found ${#pkgs[@]}: ${pkgs[*]:-none}" + exit 1 + fi + # payjoin-0.1.1.tgz -> 0.1.1 + name="$(basename "${pkgs[0]}" .tgz)" + echo "tarball=${pkgs[0]}" >> "$GITHUB_OUTPUT" + echo "version=${name#payjoin-}" >> "$GITHUB_OUTPUT" + + - name: Verify tag matches packed artifact version + uses: ./.github/actions/verify-tag-version + with: + tag-prefix: payjoin-javascript- + version: ${{ steps.locate.outputs.version }} + + - name: Attest build provenance (tarball) + # A consumer runs: gh attestation verify .tgz -R payjoin/rust-payjoin + uses: actions/attest@v4 + with: + subject-path: ${{ steps.locate.outputs.tarball }} + + - name: Publish to npmjs.com + run: npm publish "${{ steps.locate.outputs.tarball }}" + + github-release: + name: "Attach tarball + SHA256SUMS to the GitHub release" + needs: [publish-npm] + if: startsWith(github.ref, 'refs/tags/payjoin-javascript-') + permissions: + contents: write # create/update the Release for this tag and upload assets + uses: ./.github/workflows/release-assets.yml + with: + artifact-pattern: payjoin-javascript-npm-package + tag-prefix: payjoin-javascript- diff --git a/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/RELEASING.md b/payjoin-ffi/javascript/RELEASING.md new file mode 100644 index 000000000..b0c114b42 --- /dev/null +++ b/payjoin-ffi/javascript/RELEASING.md @@ -0,0 +1,50 @@ +# 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. 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 + git tag -s payjoin-javascript-0.1.1 -m 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`. +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. 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!"