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
97 changes: 97 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ts>` (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-<crate>/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:<version>-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

Expand Down
Loading
Loading