diff --git a/.changeset/config.json b/.changeset/config.json index 291fc6c..b0ddac0 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -10,5 +10,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": ["examples"] } diff --git a/.changeset/sdk-authcapture-autocapture-wireformat.md b/.changeset/sdk-authcapture-autocapture-wireformat.md index f0009f3..3f7df26 100644 --- a/.changeset/sdk-authcapture-autocapture-wireformat.md +++ b/.changeset/sdk-authcapture-autocapture-wireformat.md @@ -4,17 +4,15 @@ "@x402r/cli": minor --- -authCapture wire format + autoCapture (PR 2/4 of authCapture migration). +authCapture wire format glue and autoCapture builder. **Breaking** - `@x402r/helpers` `forwardToArbiter` skips settlements whose scheme is not `'authCapture'` (was `'commerce'`). -- `@x402r/evm` peerDep widened to `>=0.2.0-alpha.0 <0.3.0` on `@x402r/core`, `@x402r/helpers`, `@x402r/cli`. -- `@x402r/core/payment` `toPaymentInfo()` now takes `PaymentInfoStruct` from `@x402r/evm` (was `EscrowPayload`). -- `@x402r/cli` switches `@x402r/evm` import from `registerCommerceEvmScheme` to `registerAuthCaptureEvmScheme`. +- `@x402r/helpers` and `@x402r/cli` widen `@x402r/evm` to `>=0.2.0-alpha.0 <0.3.0`. +- `@x402r/cli` switches from `registerCommerceEvmScheme` to `registerAuthCaptureEvmScheme`. **New** -- `x402rDefaults(input) → AuthCaptureExtra` from `@x402r/helpers`. Only `captureAuthorizer` is required. -- Wire-format type re-exports from `@x402r/helpers`: `AuthCaptureExtra`, `AuthCapturePayload`, `Eip3009Payload`, `Permit2Payload`, `PaymentInfoStruct` + payload type guards. -- New scenarios: `examples/scenarios/atomic-charge.ts` (atomic `payment.charge()`) and `partial-refund-flow.ts` (`capture(partial)` then `voidPayment()`). +- `x402rDefaults(input) → AuthCaptureExtra` from `@x402r/helpers` — only `captureAuthorizer` is required. +- Wire-format types re-exported from `@x402r/helpers`: `AuthCaptureExtra`, `AuthCapturePayload`, `Eip3009Payload`, `Permit2Payload`, `PaymentInfoStruct`, plus payload type guards. diff --git a/.changeset/sdk-authcapture-lift.md b/.changeset/sdk-authcapture-lift.md index 48f02c9..126701e 100644 --- a/.changeset/sdk-authcapture-lift.md +++ b/.changeset/sdk-authcapture-lift.md @@ -4,110 +4,44 @@ "@x402r/helpers": minor --- -Lift SDK to the authCapture contract surface (BackTrackCo/x402r-contracts#34, merged at `9786579`). +Lift the SDK to the authCapture contract surface. -Breaking changes — clean break, no shims: +**Breaking — operator methods** -**Operator method renames** -- `release()` → `capture()`. SDK function and on-chain method both renamed; no back-compat alias. -- `refundInEscrow(paymentInfo, amount, data)` → `voidPayment(paymentInfo, data?)`. The `amount` argument is dropped — the new `escrow.void()` is full-only and empties the entire authorization regardless of any partial value the caller intends. See migration note below. -- `refundPostEscrow()` → `refund()` on both SDK and contract. Renamed helpers: `approvePostEscrowRefund` → `approveRefundAllowance`, `getPostEscrowRefundAllowance` → `getRefundAllowance`. -- `feeRecipient` → `feeReceiver` on `OperatorConfig` (auto-derived from new ABI). `OperatorSlots` return shape from `getOperatorConfig()` also renamed to short-form fields (`authorizeHook`, `captureCondition`, `feeReceiver`, etc.). +- `release()` → `capture()` (SDK + on-chain). +- `refundInEscrow(paymentInfo, amount, data)` → `voidPayment(paymentInfo, data?)`. `void` is full-only and drops the `amount` argument; use partial capture + void remainder for the old partial-refund flow. +- `refundPostEscrow()` → `refund()`. Allowance helpers renamed: `approvePostEscrowRefund` → `approveRefundAllowance`, `getPostEscrowRefundAllowance` → `getRefundAllowance`. +- `OperatorConfig.feeRecipient` → `feeReceiver`. `OperatorSlots` fields renamed to `authorizeHook`, `captureCondition`, `feeReceiver`, etc. -**Plugin terminology** -- `recorder`/`Recorder` → `hook`/`Hook` everywhere (ABIs, factories, types, files, exports, addresses). -- `actions/recorder/` directory renamed to `actions/hook/`. Action functions: `getRecorderPaymentInfo` → `getHookPaymentInfo`, `getPayerPaymentsFromRecorder` → `getPayerPaymentsFromHook`, `getReceiverPaymentsFromRecorder` → `getReceiverPaymentsFromHook`. -- `recorderCombinator`/`recorderCombinatorFactory` → `hookCombinator`/`hookCombinatorFactory` in factory function names, types, and `factories.*` config field. -- `paymentIndexRecorder` → `paymentIndexRecorderHook`; `authorizationTimeRecorder` → `authorizationTimeRecorderHook`. (Tracks the `BackTrackCo/x402r-contracts#36` rename — the post-action slot needs to distinguish recorder-style hooks from arbitrary ones, so `RecorderHook` is back as a suffix.) -- `recorders` config field on `X402rChainConfig` → `hooks`; `RecorderSingletonAddresses` → `HookSingletonAddresses`; `getRecorderSingletons` → `getHookSingletons`. -- `recorderCombinatorCodehash` → `hookCombinatorCodehash`. -- `iRecorderAbi` → `iHookAbi` (and the rest of the recorder→hook ABI consts). +**Breaking — plugin terminology** -**Type renames** -- `ConditionConfig` (constructor's plugin-config arg) → `PluginConfig`. Field shape changed too: now `{authorize,charge,capture,void,refund} × {PreActionCondition, PostActionHook}` per action, `feeReceiver`, `feeCalculator`. Use named-field syntax — positional struct literals will misalign. +- `recorder` / `Recorder` → `hook` / `Hook` across ABIs, factories, types, exports, and addresses. +- `actions/recorder/` → `actions/hook/`. `getRecorderPaymentInfo` → `getHookPaymentInfo`. `getPayerPaymentsFromRecorder` → `getPayerPaymentsFromHook`. `getReceiverPaymentsFromRecorder` → `getReceiverPaymentsFromHook`. +- `recorderCombinator*` → `hookCombinator*`. `paymentIndexRecorder` → `paymentIndexRecorderHook`. `authorizationTimeRecorder` → `authorizationTimeRecorderHook`. +- `X402rChainConfig.recorders` → `hooks`. `RecorderSingletonAddresses` → `HookSingletonAddresses`. `getRecorderSingletons` → `getHookSingletons`. `iRecorderAbi` → `iHookAbi`. -**Slot getters** -- `AUTHORIZE_CONDITION` → `AUTHORIZE_PRE_ACTION_CONDITION`, `AUTHORIZE_RECORDER` → `AUTHORIZE_POST_ACTION_HOOK`, same pattern for charge/capture/void/refund slots. `FEE_RECIPIENT` → `FEE_RECEIVER`. `ConditionSlot` union updated. +**Breaking — types, slots, events, errors** -**Events** -- All five action events renamed to `Executed`: `AuthorizationCreated` → `AuthorizeExecuted`, `ReleaseExecuted` → `CaptureExecuted`, `RefundInEscrowExecuted` → `VoidExecuted`, `RefundPostEscrowExecuted` → `RefundExecuted`. -- `VoidExecuted` no longer carries an `amount` field. -- `FeesDistributed.arbiterAmount` → `operatorAmount`. +- `ConditionConfig` (constructor plugin-config arg) → `PluginConfig`. Shape is now `{authorize, charge, capture, void, refund} × {PreActionCondition, PostActionHook}` per action plus `feeReceiver` and `feeCalculator`. Use named-field syntax. +- Slot getters renamed: `AUTHORIZE_CONDITION` → `AUTHORIZE_PRE_ACTION_CONDITION`, `AUTHORIZE_RECORDER` → `AUTHORIZE_POST_ACTION_HOOK`. `FEE_RECIPIENT` → `FEE_RECEIVER`. +- Events renamed to `Executed`: `AuthorizationCreated` → `AuthorizeExecuted`, `ReleaseExecuted` → `CaptureExecuted`, `RefundInEscrowExecuted` → `VoidExecuted`, `RefundPostEscrowExecuted` → `RefundExecuted`. `VoidExecuted` no longer carries `amount`. `FeesDistributed.arbiterAmount` → `operatorAmount`. +- Errors: `ConditionNotMet` → `PreActionConditionNotMet`. -**Errors** -- `ConditionNotMet` → `PreActionConditionNotMet`. (`ReleaseLocked` → `CaptureLocked` flows through the new ABI.) +**Breaking — chains and addresses** -**Drops** -- `usdcTvlLimit` removed from canonical config + ABI exports + helpers re-export. Source remains in `x402r-contracts` for ad-hoc per-chain integrations; SDK no longer ships the canonical address. Marketplace/delivery-protection presets now wire `authorizePreActionCondition: zeroAddress` (was the TVL gate). -- `SKALE Base` chain (chainId `1187947933`) dropped from `x402rChains`. SKALE runs Shanghai EVM but the canonical commerce-payments bytecode targets Cancun (TSTORE/TLOAD via Solady's `ReentrancyGuardTransient`). CREATE2 binds bytecode, so Shanghai-recompiled bytecode lands at a different address than the canonical one — single-canonical-address is incompatible without shipping a chain-specific island. -- All chains except Base mainnet (8453) and Base Sepolia (84532) dropped from `x402rChains`. The SDK now points at the canonical `commerce-payments at v1.0.0` AuthCaptureEscrow, which lives on those two chains today; other EVMs return as the canonical primitives extend coverage. +- `x402rChains` reduced to Base mainnet (8453) and Base Sepolia (84532). Canonical addresses now point at the audited `commerce-payments` v1.0.0 deployment of `AuthCaptureEscrow`. +- `usdcTvlLimit` removed from the canonical config and helpers re-export. Marketplace and delivery-protection presets now wire `authorizePreActionCondition: zeroAddress`. -**Canonical addresses** -- `authCaptureEscrow`, `tokenCollector`, and the `commercePayments*` exports point at the canonical `commerce-payments at v1.0.0` deployment (`0xBdEA0D…420cff` / `0x0E3dF951…7A7757` / `0x992476B9…0aB26`). -- `protocolFeeConfig`, `conditions.*`, and the escrow-free entries of `factories.*` (`staticFeeCalculator`, `staticAddressCondition`, `andCondition`, `orCondition`, `notCondition`, `hookCombinator`, `signatureCondition`, `refundRequestEvidence`) live at salt namespace `x402r-canonical-v1::*` — unchanged from prior canonical deployments. -- `receiverRefundCollector` and the escrow-bound entries of `factories.*` (`paymentOperator`, `escrowPeriod`, `freeze`, `refundRequest`) plus `hooks.paymentIndexRecorderHook` are at the new `x402r-canonical-v1.0.1::*` salt namespace, rebound to the canonical escrow. `hooks.paymentIndexRecorderHook` lands at `0x358ECA14fFD51e63D2Bb8DDE3aBAA14f8D5274C3`. See `x402r-contracts/deployments/canonical-v1.0.1.json` for the full set + tx hashes. -- Owner / fee recipient on `ProtocolFeeConfig` is `0x773dBcB5BDb3Df8359ba4e42D7Ce7AE3fC9Ee235`; protocol-fee calculator is unset (`address(0)`), so `getProtocolFeeBps()` returns `0`. +**Breaking — query scoping** -**Workspace dev** -- `@x402r/sdk` and `@x402r/helpers` deps on `@x402r/core` switched to the `workspace:^` protocol (was `^0.2.0`) so local edits resolve through the workspace. `pnpm publish` substitutes the resolved version at publish time; consumer-facing `package.json` is unchanged. +- SDK `query.*` methods (`getPayerPayments`, `getReceiverPayments`, `getPayment`) auto-scope hook reads to the SDK's configured `operatorAddress`. Direct callers of `@x402r/core/actions/hook/*` must pass `operatorAddress` explicitly to opt in. `getPayerPayment` and `getReceiverPayment` narrow to `Promise`. ---- - -## Migration: partial in-escrow refund (R-25) +**Partial in-escrow refund migration** -The new `escrow.void()` is full-only — calling it empties the entire authorization in one transaction. Partial refunds while funds are still in escrow no longer have a single-call equivalent. The replacement is **partial capture** — capture only the amount the merchant intends to keep, then void the remainder back to the payer: +The old `refundInEscrow(paymentInfo, amount)` has no single-call replacement. Use partial capture + void remainder: ```ts -// Old (single call, no longer supported): -// await client.payment.refundInEscrow(paymentInfo, partialAmount) - -// New (two calls, no allowance / collector setup): const { capturableAmount } = await client.payment.getAmounts(paymentInfo) -const merchantAmount = capturableAmount - refundToPayer - -// 1. Capture only what the merchant keeps -// (decrements escrow.capturableAmount by merchantAmount) -await client.payment.capture(paymentInfo, merchantAmount, '0x') - -// 2. Void what's left — returns the remaining escrowed amount to the payer -// (atomically settles any pending RefundRequest via the voidPostActionHook) +await client.payment.capture(paymentInfo, capturableAmount - refundToPayer, '0x') await client.payment.voidPayment(paymentInfo) ``` - -This matches the canonical authCapture semantics: `capture` is incremental (can be called multiple times up to the cumulative authorized amount), and `void` zeros out the remaining `capturableAmount`. No `ReceiverRefundCollector` allowance, no merchant capital movement, no separate refund flow. - -For **post-capture refunds** (after the merchant has already captured and wants to refund a customer), use the post-escrow flow: - -```ts -// One-time setup: pre-stake an ERC-20 allowance on ReceiverRefundCollector -await client.payment.approveRefundAllowance(token, allowanceAmount) - -// Refund any amount up to the standing allowance -await client.payment.refund(paymentInfo, refundAmount, receiverRefundCollector, encodedData) -``` - -**Recovery if `voidPayment` doesn't land.** The partial-capture flow is two transactions. If the second tx (`voidPayment`) never executes — crash, gas exhaustion, key loss — the payer's remainder sits in escrow under the original authorization. Recovery is on-chain via `AuthCaptureEscrow.reclaim(paymentInfo)`, callable by the payer after `paymentInfo.refundExpiry`. The SDK does not currently ship a `payment.reclaim()` wrapper; call the contract directly via `walletClient.writeContract({ address: authCaptureEscrow, abi: authCaptureEscrowAbi, functionName: 'reclaim', args: [paymentInfo] })`. A typed wrapper is on the PR 2/4 backlog. - ---- - -## Cross-operator data scoping - -`PaymentIndexRecorderHook` is a chain singleton — one address per chain shared by every operator that routes through `HookCombinator`. The SDK's `query.*` methods (`getPayerPayments`, `getReceiverPayments`, `getPayment`) automatically scope hook reads to the SDK's configured `operatorAddress`, matching how `createEventProvider` already works. Multi-operator deployments no longer get mingled records by default. - -Direct callers of `@x402r/core/actions/hook/*` (`getPayerPaymentsFromHook`, `getReceiverPaymentsFromHook`, `getHookPaymentInfo`, `getPayerPayment`, `getReceiverPayment`) must pass `operatorAddress` explicitly to opt into filtering — by default these return unfiltered (mingled) data matching the contract behavior. - -Caveat: the on-chain `total` count returned by `getPayerPaymentsFromHook` and `getReceiverPaymentsFromHook` reflects the unfiltered count even when the returned `payments` array is filtered. Callers needing per-operator-accurate totals must paginate fully and count filtered results client-side. Per-operator-accurate totals would require contract changes (out of scope). - -Breaking change: `getPayerPayment` and `getReceiverPayment` (single-record-by-index reads) return type narrowed from `Promise` to `Promise` to surface mismatches when `operatorAddress` is set. - -`createHookProvider` signature changed: third arg is now `options?: { pageSize?, operatorAddress? }` instead of positional `pageSize?`. Internal-only — not exported from `@x402r/sdk`. - ---- - -## Out of scope (PR 2/3/4 per `x402r-notes/plans/AUTHCAPTURE_SDK_MIGRATION.md`) - -- `@x402r/helpers` scheme filter `'commerce' → 'authCapture'` (PR 2) -- `autoCapture` plumbing (PR 2) -- Permit2 support (PR 3) -- `x402rDefaults()` helper + new wire-format type re-exports (PR 4) -- Bumping `@x402r/evm` peerDep version (PR 2) diff --git a/.changeset/sdk-authcapture-permit2.md b/.changeset/sdk-authcapture-permit2.md index bf94fb2..b311c9b 100644 --- a/.changeset/sdk-authcapture-permit2.md +++ b/.changeset/sdk-authcapture-permit2.md @@ -4,11 +4,10 @@ "@x402r/cli": patch --- -Permit2 support (PR 3 of authCapture migration). +Add Permit2 payer-side helpers. **New** -- `@x402r/core/payment/permit2`: `signPermit2Authorization`, `createPermit2ApprovalTx`, `getPermit2AllowanceReadParams`, `PERMIT2_ADDRESS`. Parallels `signReceiveAuthorization` — returns `{collectorData, tokenCollector}` suitable for direct `payment.charge` / `payment.authorize`. `collectorData` is the raw 65-byte EOA signature (commerce-payments' Permit2PaymentCollector forwards it straight to `permit2.permitTransferFrom`). -- `@x402r/sdk`: re-exports the four `@x402r/core` Permit2 surfaces above. -- `@x402r/cli`: `--asset-transfer-method ` flag — filters `accepts[]` in addition to `--chain`. Errors on unknown value or empty match set with a `Malformed402Error` (exit code 2). -- New scenario: `examples/scenarios/permit2-charge.ts`. Demonstrates the one-time `ERC20.approve(PERMIT2, MAX)` step and an atomic Permit2 charge with balance-delta assertions. +- `@x402r/core/payment/permit2`: `signPermit2Authorization`, `createPermit2ApprovalTx`, `getPermit2AllowanceReadParams`, and the `PERMIT2_ADDRESS` constant. Returns `{collectorData, tokenCollector}` suitable for `payment.charge` / `payment.authorize`. +- `@x402r/sdk` re-exports the four Permit2 surfaces. +- `@x402r/cli` adds `--asset-transfer-method ` to filter `accepts[]` alongside `--chain`. Invalid value or empty match set errors with a `Malformed402Error` (exit code 2). diff --git a/.changeset/sdk-authcapture-reconstruct-payment-info.md b/.changeset/sdk-authcapture-reconstruct-payment-info.md index 3e8ab17..9608e58 100644 --- a/.changeset/sdk-authcapture-reconstruct-payment-info.md +++ b/.changeset/sdk-authcapture-reconstruct-payment-info.md @@ -3,20 +3,17 @@ "@x402r/helpers": minor --- -Add the `PaymentInfo` namespace in `@x402r/core` (re-exported from `@x402r/sdk`) and add `reconstructPaymentInfoWire` in `@x402r/helpers`. Together they bridge the authCapture wire format to the bigint shape SDK actions accept. +Add the `PaymentInfo` namespace and `reconstructPaymentInfoWire` to bridge the authCapture wire format to the bigint shape SDK actions accept. **New** -- `PaymentInfo` is now both a type and a namespace const in `@x402r/core` (re-exported from `@x402r/sdk`). Use `PaymentInfo.fromWire(wire)` to convert a JSON-form `PaymentInfoWire` to the bigint `PaymentInfo`, and `PaymentInfo.toWire(info)` for the reverse direction. The TypeScript "type + const sharing a name" pattern lets the same identifier serve both type annotations and value namespace access (same shape as built-in `Date`, `Buffer`, etc.). -- `PaymentInfoWire` type — derived from the contract ABI via the new `AbiPrimitiveToWire` type helper. Stays in sync with `PaymentInfo` at compile time whenever the ABI changes. -- `reconstructPaymentInfoWire(context)` in `@x402r/helpers` — builds the `PaymentInfoWire` JSON form from a verified `SettleResultContext`. Handles the 6 wire→struct field renames and the EIP-3009/Permit2 branch internally. +- `PaymentInfo` is now both a type and a namespace const in `@x402r/core` (re-exported from `@x402r/sdk`). Use `PaymentInfo.fromWire(wire)` to convert a JSON-form `PaymentInfoWire` to the bigint `PaymentInfo`, and `PaymentInfo.toWire(info)` for the reverse. +- `PaymentInfoWire` type in `@x402r/core` — derived from the contract ABI, stays in sync at compile time when the ABI changes. +- `reconstructPaymentInfoWire(context)` in `@x402r/helpers` — builds the wire JSON form from a verified `SettleResultContext`. +- `@x402r/core` no longer declares `@x402r/evm` as a peer dependency. Consumers using only `@x402r/core` (arbiters, standalone wallet code) no longer need `@x402r/evm` installed. **Breaking** -- **`@x402r/helpers`: `toPaymentInfo` removed.** Use `PaymentInfo.fromWire` from `@x402r/sdk` or `@x402r/core` instead — same conversion logic, new home. Arbiters and standalone workers can now drop the `@x402r/helpers` dep entirely; they only need `@x402r/sdk`. -- **`@x402r/helpers`: `reconstructPaymentInfoStruct` renamed to `reconstructPaymentInfoWire`.** Same function, return type renamed from `PaymentInfoStruct` to `PaymentInfoWire`. Mechanical search/replace migration. -- **`@x402r/helpers`: `forwardToArbiter`'s POST body field renamed from `paymentInfoStruct` to `paymentInfoWire`.** Arbiters consuming the helper's output should switch from `req.body.paymentInfoStruct` to `req.body.paymentInfoWire` and run it through `PaymentInfo.fromWire` to get bigints. The old `paymentPayload` field (legacy `commerce` scheme) is also gone. - -**Why these live where they do** - -`PaymentInfo` + converters live in `@x402r/core` because the conversion is scheme-agnostic; the wire type is ABI-derived in core, so there's no `@x402r/evm` dependency. `reconstructPaymentInfoWire` stays in `@x402r/helpers` because it encodes scheme-specific protocol logic (the wire→struct field renames and EIP-3009/Permit2 branching) that doesn't belong in core. +- `@x402r/helpers`: `toPaymentInfo` removed. Use `PaymentInfo.fromWire` from `@x402r/sdk` or `@x402r/core`. Arbiters and standalone workers can now drop the `@x402r/helpers` dependency entirely. +- `@x402r/helpers`: `reconstructPaymentInfoStruct` renamed to `reconstructPaymentInfoWire`. Return type renamed from `PaymentInfoStruct` to `PaymentInfoWire`. +- `@x402r/helpers`: `forwardToArbiter` POST body field renamed from `paymentInfoStruct` to `paymentInfoWire`. Arbiters should consume `req.body.paymentInfoWire` and run it through `PaymentInfo.fromWire`. diff --git a/.changeset/sdk-authcapture-topaymentinfo-relocation.md b/.changeset/sdk-authcapture-topaymentinfo-relocation.md deleted file mode 100644 index b3a60eb..0000000 --- a/.changeset/sdk-authcapture-topaymentinfo-relocation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@x402r/core": minor -"@x402r/helpers": minor ---- - -`toPaymentInfo` relocation (PR 4 of authCapture migration). - -**Breaking** - -- `@x402r/core`: removed `toPaymentInfo` and `ToPaymentInfoReturnType` exports. Wire-format conversion lives in `@x402r/helpers` now — import from `@x402r/helpers` instead. Also drops `@x402r/core`'s `@x402r/evm` peerDep since core no longer references wire-format types. - -**New** - -- `@x402r/helpers`: added `toPaymentInfo` and `ToPaymentInfoReturnType` exports. Converts the on-chain `PaymentInfoStruct` (string-encoded uints from the wire) to the runtime `PaymentInfo` (bigint) used by SDK action helpers. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4280163..708048c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,5 @@ name: Release -# Manually triggered only. Click "Run workflow" in the Actions UI when -# you want to either open/update the Version Packages PR (if there are -# pending changesets) or publish to npm (after the Version PR is merged). -# -# Changesets self-selects which mode to run via its `hasChangesets` -# output — one click does the right thing. -# -# Industry pattern: changesets/changesets uses the same two-job split. # See x402r-notes/sdk/RELEASING.md for the operator runbook. on: workflow_dispatch: @@ -46,10 +38,30 @@ jobs: with: egress-policy: audit + # Mint a GitHub App installation token for changesets/action. Required + # because PRs opened with the default GITHUB_TOKEN don't trigger + # downstream workflows (GitHub Actions anti-recursion), which would + # skip required CI on the Version Packages PR. App-token-opened PRs + # trigger workflows like a human-opened PR would. See + # x402r-notes/sdk/RELEASING.md. + - name: Mint release-bot installation token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - persist-credentials: false + # Persist the App installation token in local git config so the + # version-bump push to changeset-release/main authenticates. + # changesets/action pushes via git CLI under the hood; env + # GITHUB_TOKEN only flows to octokit and user scripts, so without + # token: + persisted credentials the push has no auth. + token: ${{ steps.app-token.outputs.token }} # Full history so changesets can generate complete changelogs. fetch-depth: 0 @@ -81,7 +93,7 @@ jobs: commit: "chore(release): version packages" title: "chore(release): version packages" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # Job 2: publish to npm. Only runs when the version job found NO # pending changesets (i.e., the Version PR was already merged on a @@ -118,10 +130,25 @@ jobs: with: egress-policy: audit + # Mint a GitHub App installation token so GitHub release entries + # created by changesets/action are attributed to the App rather + # than github-actions[bot]. npm publishing itself uses Trusted + # Publishing (OIDC) and does not consume this token. + - name: Mint release-bot installation token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - persist-credentials: false + # Same reason as version job: persisted App token is needed + # because `changeset publish` creates and pushes git tags via + # git CLI; without persisted credentials the tag push has no auth. + token: ${{ steps.app-token.outputs.token }} fetch-depth: 0 - name: Setup pnpm @@ -160,4 +187,4 @@ jobs: publish: pnpm exec changeset publish createGithubReleases: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}