How releases work in this monorepo: Changesets-driven, npm provenance, single-branch trunk.
A single long-lived main branch hosts shipped code. Feature work happens on short-lived branches off main, merges
via PR back to main. Releases are gated by Changesets, not by branch
merges.
Add one before merging any PR that touches packages/**. From the repo root:
pnpm changesetPick the affected packages, pick the bump type (patch / minor / major), and write a short user-facing summary. The
CLI writes a *.md file under .changeset/; commit it alongside your code change. CI fails the PR if packages/**
changed without a .changeset/*.md companion.
Examples don't need changesets — @inflowpayai/example-* is in the ignore list in .changeset/config.json.
The release workflow runs on every push to main. It uses the official changesets/action@v1:
- No pending changesets: workflow is a no-op.
- Pending changesets exist: the Changesets bot opens (or updates) a "chore(release): version packages" PR. The diff
shows the version bumps and
CHANGELOG.mdentries each changeset would produce. The maintainer reviews and merges. - Version Packages PR merges: workflow runs again, detects the applied versions, and calls
pnpm releasewhich runschangeset publish. Each bumped package is published to npm with provenance attestations (OIDC-backed).
The workflow's final step iterates steps.changesets.outputs.publishedPackages and queries
npm view <name>@<version> --json for each just-published version, asserting .dist.attestations is non-null. The
query is retried with backoff (six attempts across ~165s) because npm publish returns when the tarball reaches the npm
origin, but npm view reads through the registry CDN, which lags writes by a few seconds to a couple of minutes. Any
package landing without a provenance attestation after the retries fails the run. (We do not use npm audit signatures
for this — that command audits dependencies installed in node_modules, not the tarballs we just published, and it
can't see this repo's workspaces because they're declared in pnpm-workspace.yaml rather than
package.json#workspaces.)
Two scripts under scripts/ validate every publishable package against publish-time pitfalls and run automatically:
scripts/check-exports.mjs— walks each package'sexportsmap and confirms every referenced path resolves on disk after build, and that every conditional block liststypesfirst per the TypeScript dual-package handbook. Surfaced aspnpm check-exports.scripts/verify-publish.mjs— runspnpm pack --dry-runper publishable package and asserts each tarball containsdist/,README.md, andLICENSE; warns on accidental test-file inclusion. Surfaced aspnpm verify-publish.
Both run automatically in:
- CI on every PR and push (
.github/workflows/ci.yml) — between theBuildandTeststeps. A brokenexportsmap or a missingLICENSEfails CI before the PR can merge. - The release pipeline —
pnpm release(invoked bychangesets/action@v1when publishing) chainsturbo run build && pnpm check-exports && pnpm verify-publish && changeset publish, so a malformed tarball never reaches npm.
Run both locally before tagging a release or whenever publish-time troubleshooting is needed:
pnpm check-publish # builds, then runs check-exports + verify-publishThe combined check-publish script depends on a fresh pnpm build; the individual check-exports / verify-publish
scripts assume dist/ is already populated.
- Create the
@inflowpayaiscope on npmjs.com. Use a team-owned account, not an individual. - Bootstrap each package's npm record, then register Trusted Publishing. Trusted Publishing is configured per
package on the package's npmjs.com settings page, which only exists after the package has been published at least
once — see "First publish bootstrap" below for the procedure. Once each record points at this repo's
release.yml, the release workflow publishes via OIDC (permissions.id-token: write) with thenpm publish --provenancedefaults from npm 11+, and noNPM_TOKENsecret lives in repo settings. - Verify provenance after a CI publish:
pnpm view @inflowpayai/x402 --json | jq '.dist.attestations'
Trusted Publishing has a chicken-and-egg: a package's Trusted Publisher record can only be created after the package exists on npm. The first publish therefore runs from a developer machine with a short-lived granular token; every publish after that runs from CI under OIDC.
Source versions in packages/*/package.json are at 0.5.0. The bootstrap publishes those source versions directly with
no version bump — there are no pending changesets to apply.
- Gate the release workflow so it cannot race the local publish and fail with
E403. In.github/workflows/release.yml, flip the trigger fromon: push: branches: [main]toon: workflow_dispatch:. Commit and push. - Dry-run locally:
Every publishable package should report a non-empty tarball with
pnpm install --frozen-lockfile pnpm check-publish # builds, then check-exports + verify-publishdist/,README.md, andLICENSE. - Confirm clean changeset state:
pnpm changeset status --since=origin/main
- Mint a single-use granular access token on npmjs.com scoped to
@inflowpayai(read/write, 1-day expiry). Authenticate the local npm CLI and publish. Every package haspublishConfig.provenance: true, which fails without an OIDC context, so the bootstrap run overrides it:echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc NPM_CONFIG_PROVENANCE=false pnpm release
pnpm releaseisturbo run build && pnpm check-exports && pnpm verify-publish && changeset publish. With no pending changesets,changeset publishfinds each package's0.5.0source version newer than npm and publishes@inflowpayai/x402,@inflowpayai/x402-buyer, and@inflowpayai/x402-sellerwithout provenance attestations. - Register a Trusted Publisher on each package. For each of
@inflowpayai/x402,@inflowpayai/x402-buyer,@inflowpayai/x402-seller: openhttps://www.npmjs.com/package/<package>, go to the Settings tab → Trusted Publishers → Add. PublisherGitHub Actions, organizationinflowpayai, repositoryinflow-node, workflow filenamerelease.yml, environment blank. - Restore the release trigger: revert
release.ymltoon: push: branches: [main], commit, and push. The release workflow re-runs with no pending changesets and source versions matching npm, sochangeset publishis a no-op and the run goes green. The path is live for the next change. - Revoke the granular token on npm and strip the
_authToken=line from~/.npmrc. ConfirmSettings → Secrets and variables → Actionson GitHub has noNPM_TOKEN— Trusted Publishing makes one structurally unnecessary.
The first CI-driven publish with provenance happens on the next change. To prove the OIDC path end-to-end before relying
on it, add a no-op patch changeset for all three packages, merge it, then merge the resulting Version Packages PR — the
release workflow runs changeset publish under OIDC and each package page should show the Provenance: Signed and
verified badge. The workflow's final verify step (registry query of .dist.attestations per published version) fails
the run if any package lands without attestations.
From 0.5.1 onward, every change flows through the normal Changesets flow.
Today the three packages version independently. When an x402 protocol-level change requires synchronous bumps across the
whole tree (e.g. a PaymentRequirements shape change), use a linked group in .changeset/config.json:
Changesets will then bump every package in the group together. The current pre-1.x state intentionally leaves linked
empty — packages should be able to ship independent fixes.
For pre-release flows:
pnpm changeset pre enter alpha
# edit changesets as usual
git push
# … release workflow publishes @inflowpayai/x402@1.1.0-alpha.0, etc.
pnpm changeset pre exitalpha / beta / rc are conventions; any tag works.
Don't. Instead, publish a patch that supersedes the broken release. npm deprecate the broken version after the patch
is live so consumers see a warning on install:
pnpm exec npm deprecate @inflowpayai/x402-seller@1.2.3 \
"Broken release; use 1.2.4 or later (#issue-number)"- contributing.md — branch model, PR template, local test workflow.
- tooling.md — pnpm, Turborepo, Changesets reference.