diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8efda7e..61fc23a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -201,6 +201,103 @@ jobs: run: cargo clippy --manifest-path pg-compat/Cargo.toml --all-targets --locked -- -D warnings # --------------------------------------------------------------------------- + # Public-API semver gate (#253). Fails a PR that breaks pg-core's or pg-wasm's + # public API without declaring the break. + # + # Versions here are bumped by release-plz, not by the PR that makes the + # change, so "declared" means the conventional-commit `!` marker that + # release-plz turns into a major bump. The marker is read off the PR title + # into SEMVER_RELEASE_TYPE; `edited` is already in this file's trigger list, + # so adding the `!` to the title re-runs the gate. + # + # Same always-reports shape as wire-compat-rust: the path filter is a step, + # not an `on: paths:` key, so a required check never sits pending. + # --------------------------------------------------------------------------- + semver-checks: + name: Public API semver + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read # paths-filter reads the PR's changed files + steps: + # pg-wasm is not published to crates.io, so its baseline is origin/main + # rather than a registry version, and the full history has to be here. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 + id: changes + with: + filters: | + api: + - 'pg-core/**' + - 'pg-wasm/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'scripts/semver-checks.sh' + - 'scripts/semver-checks-test.sh' + - '.github/workflows/build.yml' + + # Stubs cargo, so it needs no toolchain and runs in under a second. It + # covers the 100-vs-101 exit-code mapping the gate depends on: 100 is a + # semver violation, 101 is the tool or the build failing. + - name: Test the gate script + if: steps.changes.outputs.api == 'true' + shell: bash + run: ./scripts/semver-checks-test.sh + + - if: steps.changes.outputs.api == 'true' + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + # Pinned release binary rather than `cargo install` (a five-minute build) + # or a fourth third-party action. Bump the version and the digest together. + - name: Install cargo-semver-checks + if: steps.changes.outputs.api == 'true' + shell: bash + env: + VERSION: 0.49.0 + SHA256: 72f6834d75d28a66e02c9fd6a230ce901bb30eee6067b85867a97445df040e4a + run: | + curl -sSfL -o "$RUNNER_TEMP/cargo-semver-checks.tar.gz" \ + "https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v${VERSION}/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz" + echo "${SHA256} $RUNNER_TEMP/cargo-semver-checks.tar.gz" | sha256sum -c - + tar -xzf "$RUNNER_TEMP/cargo-semver-checks.tar.gz" -C "$HOME/.cargo/bin" cargo-semver-checks + + # The PR title, not the commit subject: PRs are squash-merged here, so the + # title is what release-plz and the Conventional Commit check read. Passed + # through the environment rather than interpolated into the script, so a + # PR title cannot inject shell. + # + # The title `!` is the only accepted declaration. A `BREAKING CHANGE:` + # footer in the PR *body* is deliberately not accepted: this repo's + # squash_merge_commit_message is COMMIT_MESSAGES, so the body never reaches + # the squashed commit, and release-plz reads commits. Honouring a body-only + # footer would pass the gate on a `fix(pg-core):` PR that release-plz then + # publishes as a patch release of a breaking change. + - name: Read the breaking-change declaration off the PR title + id: declared + if: steps.changes.outputs.api == 'true' + shell: bash + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + if [[ "$PR_TITLE" =~ ^[a-zA-Z]+(\([^\)]*\))?!: ]]; then + echo "Breaking change declared in the PR title; a major bump is allowed." + echo "release_type=major" >> "$GITHUB_OUTPUT" + else + echo "No '!' in the PR title; any breaking change will fail the gate." + fi + + - name: Check the public API + if: steps.changes.outputs.api == 'true' + shell: bash + env: + SEMVER_RELEASE_TYPE: ${{ steps.declared.outputs.release_type }} + run: ./scripts/semver-checks.sh # The Node half of the same gate (#261): the containers wire-compat-rust # sealed must also open with the published npm readers in COMPATIBILITY.md's # support window. It downloads rather than re-seals, so both halves are held diff --git a/.gitignore b/.gitignore index 247e826..2c171b2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ pkg_ibs.sec .env *.pem *.patch +# pg-wasm is under the root manifest's `exclude`, so the semver gate's +# `--manifest-path pg-wasm/Cargo.toml` resolves it on its own and writes a +# lockfile next to the manifest. The repo does not track it. +pg-wasm/Cargo.lock diff --git a/CLAUDE.md b/CLAUDE.md index 67bf958..f706ca7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,10 @@ Migrated from the dobby memory repo (`encryption4all/dobby`). This file is the s - JS-reader gotchas the Node gate ran into, all still true of `@e4a/pg-wasm` 0.6.1 and `@e4a/pg-js` 1.11.0/2.3.3: (a) `@e4a/pg-wasm`'s default (bundler) entry does `import * as wasm from "./index_bg.wasm"`, which plain Node cannot resolve — import `@e4a/pg-wasm/web` and pass the module bytes to its default export, resolving them via `new URL('index_bg.wasm', import.meta.resolve('@e4a/pg-wasm/web'))` since the `.wasm` file is not in the package's `exports`; (b) `Unsealer.unseal()`/`StreamUnsealer.unseal()` *consume* the unsealer (wasm-bindgen `__destroy_into_raw`), so a tidy `free()` afterwards is a double free reported as `null pointer passed to rust`, which reads exactly like a corrupt container; (c) `pg-js` is stream-mode only in both directions — `toBytes()` seals with `sealStream` and its decrypt path only ever builds a `StreamUnsealer`, so a memory-mode container fails with `mode is not supported: InMemory { size: N }`; (d) `pg-js` 1.x discards what `StreamUnsealer.unseal()` returns and reports `public_identity()` instead, so it never surfaces the private signing policy of a `*-privsig` container and its `sender.raw` is the bare header policy rather than 2.x's `{public, private?}`; (e) `pg-js`'s decrypt path needs a PKG at `pkgUrl` for exactly two GETs — `/v2/sign/parameters` for the verifying key and `/v2/irma/key/` (bearer token) for the USK — which is why the gate can drive it offline from the artifact's own `vk.json`/`usk-*.json`. - Appending a field at the *end* of `Header` really is additive: the header is a length-prefixed region and `bincode` ignores trailing bytes, so published `pg-core` 0.6.1 still opens it. A field inserted anywhere else, a changed field type, or a reorder shifts every following byte and the containers stop opening, but *not* with a decode error: 0.6.1 reads a garbage length prefix, attempts a ~20 GiB allocation, and the process aborts (SIGABRT). Expect `reader died on signal 6 ... memory allocation of N bytes failed`, not a message naming the header. This is also why `pg-compat` opens each case in a child process (its `pg-compat-case` binary): an abort is not a panic, `catch_unwind` cannot contain it, and in one process the first broken case would take the run down before the others were tried. Don't reason about "additive" from the struct alone; run the compat gate. - CI's `Format workspace` matrix runs `cargo fmt --manifest-path pg-/Cargo.toml --all -- --check` per crate over shared workspace files; always run `cargo fmt --all -- --check` from repo root before pushing, or one crate's drift fails the whole matrix. +- `scripts/semver-checks.sh` runs `cargo-semver-checks` over the two surfaces external consumers build against: `pg-core` against its crates.io release, and `pg-wasm` against `origin/main` (it has no crates.io release; the npm package is versioned from `pg-core`). The `semver-checks` job in `build.yml` calls it on any PR touching `pg-core`, `pg-wasm`, the root manifest or the script itself; run it yourself too before pushing such a change, since the job needs a wasm32 toolchain and a pinned cargo-semver-checks download and is therefore not the fastest feedback. Four things it encodes. (1) `pg-core` needs `--only-explicit-features --features test,rust,stream`, the same set the test and clippy matrices use: cargo-semver-checks otherwise enables everything that doesn't look unstable, which pulls in `web` and hits its `compile_error!`. (2) `pg-core`'s `web,stream` surface is deliberately not checked. `Unsealer` has two `unseal` methods there on different instantiations (owned `self` in `client/web/mod.rs`, `&mut self` in `client/web/stream.rs`) and cargo-semver-checks 0.49 pairs them by name alone, so it reports `method_receiver_mut_ref_became_owned` against byte-identical source; `rust,stream` is clean because both receivers are owned there. (3) Any wasm32 run needs `RUSTFLAGS=--cap-lints=warn`, because the `--cap-lints allow` cargo-semver-checks sets silences the "dropping unsupported crate type" warnings cargo reads back when probing rustc, and cargo then dies with "output of --print=file-names missing". (4) `cargo-semver-checks` splits its non-zero exits: `100` is a semver violation, `101` is the tool or the build failing (unresolvable baseline rev, missing rustup target, registry fetch failure, compile error in the crate). Never treat "non-zero" as "breaking change" here, because the advice a semver gate prints is "declare the break", and on this repo that means a `!` in the PR title and a spurious major release of `pg-core`. `scripts/semver-checks-test.sh` pins that mapping; it stubs `cargo`, so it runs in well under a second and needs neither cargo-semver-checks nor a wasm32 toolchain. Run it after touching the gate. +- release-plz owns the version numbers, so the PR making a breaking change cannot bump the crate to match (bumping `pg-core` alone doesn't even resolve: `pg-cli` requires `^0.6.1`). What the semver gate accepts as the declaration is the conventional-commit `!` in the PR title, and only that; CI turns it into `SEMVER_RELEASE_TYPE=major`, which the script passes as `--release-type major`. A `BREAKING CHANGE:` footer in the PR body is not accepted and must not be: this repo's `squash_merge_commit_message` is `COMMIT_MESSAGES`, so the body never reaches the squashed commit, and release-plz reading a bare `fix(pg-core):` subject would cut a patch release of a break the gate had already waved through. Two consequences of the merge settings worth knowing when you declare a break. `squash_merge_commit_title` is `COMMIT_OR_PR_TITLE`, which is the PR title on a multi-commit PR but the commit's subject when the PR has exactly one commit — so on a single-commit PR put the `!` in the commit subject too, or the gate goes green off the PR title while release-plz cuts a patch. And `--release-type major` doesn't merely permit a bigger bump: every lint exists to demand a bump the declaration already grants, so all of them skip and the run checks nothing (`0 checks: 0 pass, 253 skip`) on both surfaces at once. A green gate on a `!` PR verified nothing; a `!` added for a pg-wasm break also passes any unrelated pg-core break in the same PR. - The Docker build (`Dockerfile`, `FROM rust:-slim`) pins an older or different Rust than the `Test workspace`/`Format workspace` jobs' `dtolnay/rust-toolchain@stable`. A change can pass every workspace test and still fail Docker Build on a type-inference difference that doesn't reproduce on host stable (e.g. a slice-element-type unification difference across rustc versions). Check the Dockerfile's current pin, and run `cargo build --profile edge --bin pg-pkg` locally before pushing any `Cargo.toml` dependency bump; for a true repro, build the Docker image. -- The `dobby-coder` GitHub App lacks `workflows: write` on this repo; any push touching `.github/workflows/*.yml` is rejected at the remote. Before treating a fix as blocked, check whether the same effect can be achieved in a pushable file (crate manifest, source, committed script); if a fix genuinely can only live in a workflow file, ship the pushable half and hand the maintainer ready-to-paste YAML in the PR body. +- The `dobby-coder` GitHub App lacks `workflows: write` on this repo; any push touching `.github/workflows/*.yml` is rejected at the remote. Before treating a fix as blocked, check whether the same effect can be achieved in a pushable file (crate manifest, source, committed script); if a fix genuinely can only live in a workflow file, ship the pushable half and hand the maintainer ready-to-paste YAML in the PR body. The block covers *merge* commits too, which is easy to miss: once a branch carries its own `build.yml` change (typically a maintainer applying such a patch onto it), a later `git merge origin/main` that has to touch `build.yml` produces a commit updating a workflow file, and the push is rejected even when the resolution is only "keep both new jobs". Nothing can be split out of a merge commit, so that merge has to be landed by a maintainer, or the App needs `workflows: write`. ## Dependencies diff --git a/scripts/semver-checks-test.sh b/scripts/semver-checks-test.sh new file mode 100755 index 0000000..b9dcacd --- /dev/null +++ b/scripts/semver-checks-test.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# +# Regression tests for scripts/semver-checks.sh, covering the exit-code +# contract: cargo-semver-checks answers 100 for a semver violation and 101 for +# the tool or the build failing, and only 100 may be reported as a breaking +# change. See the header of semver-checks.sh. +# +# `cargo` is stubbed, so this runs in about a second and needs neither +# cargo-semver-checks nor a wasm32 toolchain. +# +# Usage: +# scripts/semver-checks-test.sh +# +set -euo pipefail + +root=$(git rev-parse --show-toplevel) +cd "$root" + +gate="$root/scripts/semver-checks.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +stubs="$tmp/bin" +mkdir "$stubs" + +# Stub cargo: exits with STUB_EXIT_PG_CORE or STUB_EXIT_PG_WASM depending on the +# --manifest-path it was handed, and records its argv per crate so the tests can +# assert on what the gate passed through. +cat >"$stubs/cargo" <<'STUB' +#!/usr/bin/env bash +set -uo pipefail +crate=unknown +for arg in "$@"; do + case $arg in + pg-core/Cargo.toml) crate=pg_core ;; + pg-wasm/Cargo.toml) crate=pg_wasm ;; + esac +done +printf '%s\n' "$@" >"$STUB_ARGV_DIR/$crate.argv" +case $crate in + pg_core) exit "${STUB_EXIT_PG_CORE:-0}" ;; + pg_wasm) exit "${STUB_EXIT_PG_WASM:-0}" ;; +esac +exit 0 +STUB +chmod +x "$stubs/cargo" + +# The gate checks for cargo-semver-checks with `command -v` before running. +printf '#!/bin/sh\nexit 0\n' >"$stubs/cargo-semver-checks" +chmod +x "$stubs/cargo-semver-checks" + +failures=0 +argv_dir= + +# Takes VAR=VALUE arguments and hands them to the gate through `env`, so nothing +# leaks into the next case the way an assignment prefixed onto a function call +# can. +run_gate() { + argv_dir=$(mktemp -d "$tmp/argv.XXXXXX") + set +e + gate_out=$(env PATH="$stubs:$PATH" STUB_ARGV_DIR="$argv_dir" "$@" "$gate" 2>&1) + gate_status=$? + set -e +} + +check() { + local what=$1 want=$2 got=$3 + if [[ $want == "$got" ]]; then + echo " ok $what" + else + echo " FAIL $what: want ${want}, got ${got}" + failures=$((failures + 1)) + fi +} + +# Substring assertions take the haystack from $gate_out. +check_says() { + local what=$1 needle=$2 + if [[ $gate_out == *"$needle"* ]]; then + echo " ok $what" + else + echo " FAIL $what: output does not contain '${needle}'" + printf '%s\n' "$gate_out" | sed 's/^/ | /' + failures=$((failures + 1)) + fi +} + +check_silent_about() { + local what=$1 needle=$2 + if [[ $gate_out != *"$needle"* ]]; then + echo " ok $what" + else + echo " FAIL $what: output should not contain '${needle}'" + printf '%s\n' "$gate_out" | sed 's/^/ | /' + failures=$((failures + 1)) + fi +} + +echo "both surfaces clean" +run_gate +check "exit status" 0 "$gate_status" +check_says "reports success" "No undeclared breaking public-API changes." + +echo "pg-core reports a semver violation (100)" +run_gate STUB_EXIT_PG_CORE=100 +check "exit status" 1 "$gate_status" +check_says "names the crate" "Breaking public-API changes in: pg-core" + +echo "both surfaces report a semver violation (100)" +run_gate STUB_EXIT_PG_CORE=100 STUB_EXIT_PG_WASM=100 +check "exit status" 1 "$gate_status" +check_says "names both crates" "Breaking public-API changes in: pg-core pg-wasm" + +# The regression this file exists for. Before the fix both of these landed on +# the same exit 1 and the same "declare the break" advice, which on a +# release-plz repo means a spurious major release of pg-core. +echo "pg-core fails to build (101)" +run_gate STUB_EXIT_PG_CORE=101 +check "propagates the tool's exit code" 101 "$gate_status" +check_says "says it is not a semver break" "tool or build" +check_silent_about "does not claim a breaking change" "Breaking public-API changes" + +echo "pg-wasm baseline does not resolve (101)" +run_gate STUB_EXIT_PG_WASM=101 +check "propagates the tool's exit code" 101 "$gate_status" +check_silent_about "does not claim a breaking change" "Breaking public-API changes" + +# A build failure is a build failure whatever the declared release type, so the +# escape hatch must not turn it green. +echo "pg-wasm fails to build (101) with SEMVER_RELEASE_TYPE=major" +run_gate STUB_EXIT_PG_WASM=101 SEMVER_RELEASE_TYPE=major +check "propagates the tool's exit code" 101 "$gate_status" +check_silent_about "does not claim a breaking change" "Breaking public-API changes" + +# A build failure on the second surface must not throw away a real violation +# already found on the first: the author would fix the baseline, re-run, and only +# then learn about the break. +echo "pg-core violation (100) plus pg-wasm build failure (101)" +run_gate STUB_EXIT_PG_CORE=100 STUB_EXIT_PG_WASM=101 +check "propagates the tool's exit code" 101 "$gate_status" +check_says "still reports the recorded break" "Breaking public-API changes in: pg-core" +check_says "says the 101 is not a semver break" "tool or build" + +echo "an unrecognised exit code is not swallowed" +run_gate STUB_EXIT_PG_CORE=2 +check "propagates the tool's exit code" 2 "$gate_status" +check_silent_about "does not claim a breaking change" "Breaking public-API changes" + +echo "SEMVER_RELEASE_TYPE reaches both invocations" +run_gate SEMVER_RELEASE_TYPE=major +check "exit status" 0 "$gate_status" +for crate in pg_core pg_wasm; do + if grep -qx -- '--release-type' "$argv_dir/$crate.argv" && + grep -qx -- 'major' "$argv_dir/$crate.argv"; then + echo " ok $crate got --release-type major" + else + echo " FAIL $crate did not get --release-type major" + failures=$((failures + 1)) + fi +done + +echo "the default run passes no --release-type" +run_gate +check "exit status" 0 "$gate_status" +for crate in pg_core pg_wasm; do + if grep -qx -- '--release-type' "$argv_dir/$crate.argv"; then + echo " FAIL $crate got an unasked-for --release-type" + failures=$((failures + 1)) + else + echo " ok $crate got no --release-type" + fi +done + +echo "SEMVER_WASM_BASELINE reaches the pg-wasm invocation" +run_gate SEMVER_WASM_BASELINE=refs/heads/some-branch +check "exit status" 0 "$gate_status" +if grep -qx -- 'refs/heads/some-branch' "$argv_dir/pg_wasm.argv"; then + echo " ok pg-wasm got the baseline rev" +else + echo " FAIL pg-wasm did not get the baseline rev" + failures=$((failures + 1)) +fi + +# `cd "$(git rev-parse --show-toplevel)"` does not abort here: the substitution +# fails but `cd ""` succeeds, so the gate went on to check relative manifest +# paths against whatever the CWD happened to be. Assert on cargo never being +# reached, not just on the exit status, because the stub itself fails without a +# repo to record into. +echo "run from outside a git repository" +argv_dir=$(mktemp -d "$tmp/argv.XXXXXX") +set +e +outside_out=$(cd / && env PATH="$stubs:$PATH" STUB_ARGV_DIR="$argv_dir" "$gate" 2>&1) +outside_status=$? +set -e +check "exit status is non-zero" "yes" "$([[ $outside_status -ne 0 ]] && echo yes || echo no)" +if [[ -z $(ls -A "$argv_dir") ]]; then + echo " ok aborts before invoking cargo" +else + echo " FAIL ran cargo outside the repo: $(ls -A "$argv_dir" | tr '\n' ' ')" + printf '%s\n' "$outside_out" | sed 's/^/ | /' + failures=$((failures + 1)) +fi + +echo +if [[ $failures -gt 0 ]]; then + echo "${failures} assertion(s) failed" >&2 + exit 1 +fi +echo "all assertions passed" diff --git a/scripts/semver-checks.sh b/scripts/semver-checks.sh new file mode 100755 index 0000000..0cfe576 --- /dev/null +++ b/scripts/semver-checks.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# Public-API semver gate for the two crates external consumers build against: +# pg-core (crates.io) and pg-wasm (published to npm as @e4a/pg-wasm). +# +# A breaking change to either surface fails this script unless the change is +# declared as breaking. Versions in this repo are bumped by release-plz, not by +# the PR that makes the change, so the declaration is the `!` in the +# conventional-commit PR title, and only that: release-plz reads commit +# messages, and this repo squash-merges with squash_merge_commit_message = +# COMMIT_MESSAGES, so a `BREAKING CHANGE:` footer written in the PR body never +# reaches the commit release-plz reads. The footer is useful prose for the +# changelog; it is not the declaration. CI translates the title marker into +# SEMVER_RELEASE_TYPE=major; see .github/workflows/build.yml. +# +# Usage: +# scripts/semver-checks.sh +# +# Environment: +# SEMVER_RELEASE_TYPE major|minor|patch. Passed to --release-type. Note what +# `major` does: every cargo-semver-checks lint exists to +# demand a bump that a declared major already grants, so +# all of them skip and the run checks nothing (`0 checks: +# 0 pass, 253 skip`). It also reaches both invocations +# below, so a `!` declared for a pg-wasm break passes any +# unrelated pg-core break in the same PR as well. +# SEMVER_WASM_BASELINE Git revision pg-wasm is compared against. +# Default: origin/main. pg-wasm is not on crates.io, so +# there is no registry baseline to fetch. +# +# Exit codes: 1 for an undeclared breaking change, 127 if the tool is missing, +# and cargo-semver-checks' own code for anything else. It has two distinct +# non-zero exits and they must not be conflated. 100 is a semver violation. 101 +# is the tool or the build failing: an unresolvable baseline rev, a missing +# rustup target, a registry fetch failure, a compile error in the crate. Only +# 100 means "declare the break". Reporting a 101 that way would tell the author +# to add a `!` that makes release-plz cut a spurious major release, and it would +# not even clear the check, because --release-type major leaves a 101 non-zero. +# scripts/semver-checks-test.sh covers this mapping. +# +set -euo pipefail + +root=$(git rev-parse --show-toplevel) +cd "$root" + +release_type=() +if [[ -n "${SEMVER_RELEASE_TYPE:-}" ]]; then + release_type=(--release-type "$SEMVER_RELEASE_TYPE") +fi +wasm_baseline="${SEMVER_WASM_BASELINE:-origin/main}" + +if ! command -v cargo-semver-checks >/dev/null; then + echo "cargo-semver-checks is not installed: cargo install cargo-semver-checks --locked" >&2 + exit 127 +fi + +failed=() + +# Report the semver violations recorded so far, if any. +report_breaks() { + [[ ${#failed[@]} -gt 0 ]] || return 0 + echo >&2 + echo "Breaking public-API changes in: ${failed[*]}" >&2 + echo "Either keep the change additive, or declare the break: give the PR a" >&2 + echo "conventional-commit title with '!' (e.g. feat(pg-core)!: ...), which is" >&2 + echo "what release-plz reads to cut a major release." >&2 +} + +# Record a semver violation (exit 100) and carry on so both surfaces get +# reported; let anything else out with the tool's own exit code. +run_check() { + local name=$1 + shift + "$@" && return 0 + local ec=$? + if [[ $ec -eq 100 ]]; then + failed+=("$name") + return 0 + fi + # A violation already recorded for the other surface is a real break and has + # to be reported here too, or fixing the build failure costs a round trip to + # discover it. The tool-failure message goes last, because it is what the + # non-zero exit is about: no `!` clears it. + report_breaks + echo >&2 + echo "cargo-semver-checks failed on ${name} (exit ${ec}): tool or build" >&2 + echo "failure, not a semver break. Adding '!' to the PR title will not" >&2 + echo "clear this." >&2 + exit "$ec" +} + +# pg-core, native surface. The feature set is explicit for the same reason the +# test and clippy jobs spell it out: `web` is compile_error! off wasm32, so the +# feature heuristic cargo-semver-checks uses by default (everything except +# unstable-looking features) cannot build this crate. `test` is in the set +# because the `test` module it gates is public API that pg-compat and pg-wasm +# build against. +# +# pg-core's `web,stream` surface is deliberately NOT checked: on that feature +# combination `Unsealer` has two `unseal` methods on different instantiations +# (owned self in client/web/mod.rs, &mut self in client/web/stream.rs) and +# cargo-semver-checks 0.49 pairs them across versions by name alone, reporting +# method_receiver_mut_ref_became_owned against byte-identical source. pg-wasm +# below covers the surface those consumers actually call. +# +# `${release_type[@]+...}` rather than a bare `"${release_type[@]}"`: expanding +# an empty array aborts under `set -u` on bash < 4.4, which is macOS system bash. +echo "==> pg-core (native, features test,rust,stream)" +run_check pg-core \ + cargo semver-checks \ + --manifest-path pg-core/Cargo.toml \ + --only-explicit-features \ + --features test,rust,stream \ + ${release_type[@]+"${release_type[@]}"} + +# pg-wasm has to be built for wasm32, and cargo-semver-checks passes +# `--cap-lints allow` in RUSTFLAGS, which silences the "dropping unsupported +# crate type" warnings cargo reads back when it probes rustc for wasm32 target +# info. Cargo then aborts with "output of --print=file-names missing when +# learning about target-specific information from rustc". Setting RUSTFLAGS +# ourselves keeps cargo-semver-checks from adding its own cap and lets the +# probe through. +echo "==> pg-wasm (wasm32-unknown-unknown, baseline ${wasm_baseline})" +run_check pg-wasm \ + env RUSTFLAGS="--cap-lints=warn" cargo semver-checks \ + --manifest-path pg-wasm/Cargo.toml \ + --target wasm32-unknown-unknown \ + --baseline-rev "$wasm_baseline" \ + ${release_type[@]+"${release_type[@]}"} + +if [[ ${#failed[@]} -gt 0 ]]; then + report_breaks + exit 1 +fi + +echo +echo "No undeclared breaking public-API changes."