release: clean changesets and wire release-bot App for Version PR CI#174
Conversation
Fix .changeset/config.json ignore list: the prior "x402r-sdk" value matched no real package; switch to "examples" so the private workspace package stops being versioned into Version PRs. Rewrite the five sdk-authcapture-* changesets as consumer-facing release notes grounded in peer-changeset style. Removes internal migration code blocks, plan-file references, cross-repo PR refs, "Out of scope" sections, and PR-N-of-N markers. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The default GITHUB_TOKEN cannot trigger downstream workflows from PRs it opens (GitHub Actions anti-recursion). That meant the auto-generated Version Packages PR never fired required CI, blocking the alpha release behind a manual empty-commit workaround. Mint a GitHub App installation token in both the version and publish jobs via actions/create-github-app-token v3.2.0, and pass it as GITHUB_TOKEN to changesets/action. The App identity triggers downstream workflows like a human-opened PR, so required CI runs automatically. Requires repo variable RELEASE_APP_ID and secret RELEASE_APP_PRIVATE_KEY to be configured. Token scope minimized to contents:write (both jobs) and pull-requests:write (version job only). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Fix bug surfaced by industry-standards review: changesets/action and changeset publish push commits/tags via git CLI under the hood, not via octokit. With persist-credentials: false on actions/checkout and only env GITHUB_TOKEN set, the CLI push has no auth — env GITHUB_TOKEN flows only to octokit calls and user scripts, never to the git push subprocess. Pass the App installation token to actions/checkout in both version and publish jobs (token: input). Drop persist-credentials: false so the token is written to local git config and reused by the CLI push for the version-bump commit (version job) and tag push (publish job). Matches the pattern used by other small-monorepo projects that wire changesets/action with a GitHub App. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Review summaryChained review-sdk + industry-standards check on this PR. Critical (fixed in a583452)Bug: `changesets/action` and `changeset publish` push commits/tags via git CLI under the hood, not via octokit. With `persist-credentials: false` on `actions/checkout` and only env `GITHUB_TOKEN` set on the changesets step, the CLI push would have no auth — env `GITHUB_TOKEN` only flows to octokit calls and user scripts, not to the git push subprocess. The version job's commit push and the publish job's tag push would have failed at runtime. Fix: pass `token: ${{ steps.app-token.outputs.token }}` to `actions/checkout` in both jobs, drop `persist-credentials: false` so the App token persists in local git config and is used by the CLI push. Matches the pattern used by other small TS monorepos wiring `changesets/action` with a custom GitHub App. Verified clean
Below-threshold nits (not blocking)
Maintainer-only verifications
Follow-up`x402r-notes/sdk/RELEASING.md` doesn't yet document the App-token flow, the required repo variable + secret names, or the private-key rotation procedure. That belongs in the runbook update separately (direct-to-main per x402r-notes convention). |
A1igator
left a comment
There was a problem hiding this comment.
Industry-standards / upstream comparison
Compared the release wiring against upstream x402-foundation/x402 (which doesn't use Changesets but ships the same OIDC / Trusted-Publishing primitives) and the two reference changesets/action monorepos (viem, wagmi).
Verdict
Matches or exceeds the industry pattern.
Strengths over viem / wagmi
- All actions SHA-pinned. viem leaves
changesets/action@v1unpinned on its version job. step-security/harden-runnerwith egress audit. Neither viem nor wagmi run it.concurrency.cancel-in-progress: falseon the publish job. viem and wagmi default totrue, which can leave a half-published release.- Per-job permission minimization (publish drops
pull-requests:write). - Upfront
publint + attwgate (pnpm test:pkg) before any package publishes. viem and wagmi rely on per-packageprepublishOnly, which can leave a partial release if a late package fails.
Intentional divergences (justified)
- App installation token vs default
GITHUB_TOKEN. viem and wagmi use the default because their Version PR isn't gated by required CI. This repo's Version PR is gated, so the App-token path is thechangesets/action-recommended pattern for that scenario. workflow_dispatchonly (vspush: branches: [main]in viem / wagmi). More conservative; paired with the ref guard.- Two-job split vs a single self-selecting job. Granular permissions vs simplicity. Both valid.
Reuse from upstream x402-foundation/x402
Same checkout / pnpm / setup-node action SHAs, same [email protected] pin, same id-token: write + named-environment OIDC pattern, same if: github.ref == 'refs/heads/main' ref guard. Changesets adoption and the two-job split are additive on top of those primitives.
Nits (not blocking)
-
app-iddeprecated in favor ofclient-id.actions/create-github-app-tokenis moving the input name upstream; current value works today but emits a per-run warning. Worth renamingRELEASE_APP_IDtoRELEASE_APP_CLIENT_ID(different value: Client ID is a separate identifier on the App settings page) at the next App-config touch. -
Changeset verbosity.
sdk-authcapture-lift.mdis ~40 lines of bullets after the rewrite. The reference SDKs keep changesets to 1-3 sentences (wagmi'sbump-connect-evm-1-3-1.mdis one line). Defensible for an alpha-stage SDK where consumers need migration breadcrumbs; flagging as a style call, not a defect.
Approving
Workflow correctness verified by the prior review-sdk round (commit a583452 fixed the persist-credentials / CLI-push auth bug). No new findings above threshold.
Generated with Claude Code using review-sdk skill
vraspar
left a comment
There was a problem hiding this comment.
Follow-on read of the changesets-cleanup half of this PR plus an industry-norm audit of the release.yml restructure. Everything below is nits — nothing here blocks merge.
Changeset audit verdict
The 5 rewritten changesets sit at the upper-bound peer norm for rename-heavy minors on a typed SDK. Bolded **Breaking** / **New** sub-headers inside a single changeset are real peer practice for monorepos with substantive surface-area moves; the official changesets/action README example uses one-liners, but it's modeling a publish workflow, not changeset prose, and consumer-monorepo practice in the wider ecosystem (large viem/wagmi-style typed-SDK monorepos) skews longer when the change is rename-heavy. The 47-line sdk-authcapture-lift.md is long but proportionate to its fan-out — operator methods + plugin terminology + types/slots/events/errors + chains/addresses + query scoping. The 4-line ts migration code block in lift.md is appropriate because there's no single-call replacement for partial in-escrow refund; readers need the two-call pattern inline.
Five specific cuttable lines flagged below as inline anchors. None of them are wrong, they're just where the prose tips from describing the change into editorializing about it.
release.yml restructure verdict
The restructure diverges from the changesets/action README's "happy path" (one job + push: main + GITHUB_TOKEN) on three axes:
workflow_dispatchtrigger instead ofpush: main.- Two-job split gated on the
hasChangesetsoutput. - GitHub App installation token swap in place of
GITHUB_TOKEN.
Each divergence is independently community-standard and supported in the action's own docs — the hasChangesets output is explicitly documented as the hook for custom publish gating, and the App-token swap is the standard workaround for the well-known trigger-recursion problem where github-actions[bot]-opened PRs don't fire downstream workflows (so required CI would skip on the Version PR). The combination here is a security-conscious posture, not a whack one. The ref guard (if: github.ref == 'refs/heads/main') on workflow_dispatch is a real defense against dispatch from a stale feature branch minting a token against an unintended ref.
The app-id deprecation flagged in the prior review summary stands — app-id is the v2 input name and v3 prefers application_id, but app-id still works as an alias; not blocking.
On the inline comments
The cache: pnpm removal is defensible (privileged-runner-reading-untrusted-PR-cache is a real poisoning surface) but stricter than peer norm — most monorepo changesets workflows keep cache: pnpm and rely on cache-key scoping to mitigate. Flagging as a deliberate-vs-inherited check, not a fix request.
The 8-line workflow-level header comment plus the "Industry pattern: ..." parenthetical in release.yml lean cosmetic and could move to RELEASING.md — the workflow file is read line-by-line by the next operator on-call, and peer-project justification doesn't help them debug the job.
Audit gap to call out separately
Nobody has re-verified that the rename tables in the changesets actually match the current SDK exports (getRecorderPaymentInfo → getHookPaymentInfo, the recorderCombinator* → hookCombinator* family, the feeRecipient → feeReceiver slot rename, the AUTHORIZE_CONDITION → AUTHORIZE_PRE_ACTION_CONDITION getter family) or that the partial-refund snippet in sdk-authcapture-lift.md compiles against the current payment.capture / payment.voidPayment signatures. The prior review summary on this PR ran a structural check (front-matter preserved, linked-group invariant, no leftover plan-paths) but didn't run the snippets or grep the renames against the source tree. Worth running grep -r for each renamed symbol + tsc --noEmit against an example file before tagging 0.3.0-alpha.0.
Nothing here blocks merge.
| --- | ||
|
|
||
| Lift SDK to the authCapture contract surface (BackTrackCo/x402r-contracts#34, merged at `9786579`). | ||
| Lift the SDK to the authCapture contract surface. Clean break — no shims, no back-compat aliases. |
There was a problem hiding this comment.
Tone editorializing. Peer changesets describe the change, not their own posture toward it. Suggest cutting the sentence — the **Breaking** headers below already establish that this is a break.
| - `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 (see below). |
There was a problem hiding this comment.
Minor: cross-reference inside a 47-line entry. The migration code block at the bottom is findable without the parenthetical — (see below) is the kind of in-doc nav peers don't write. Suggest dropping the parenthetical.
| - `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` (same pattern for charge/capture/void/refund). `FEE_RECIPIENT` → `FEE_RECEIVER`. |
There was a problem hiding this comment.
This is the half-collapsed form. Either spell each of the four slot renames out (matches the recorder→hook bullet style above) or trust the reader and drop the parenthetical. The current state is the weakest version — readers either have to imagine the renames or skim past.
| - `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`. Handles the wire-to-struct field renames and the EIP-3009 / Permit2 branch internally. |
There was a problem hiding this comment.
Implementation detail — describes how the helper is internally branched. Consumers care what changed for them, not how the helper switches. Suggest cutting; the **New** bullet above already says what the helper does.
| - `@x402r/sdk`: re-exports the four `@x402r/core` Permit2 surfaces above. | ||
| - `@x402r/cli`: `--asset-transfer-method <eip3009|permit2>` 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. Parallels `signReceiveAuthorization` — returns `{collectorData, tokenCollector}` suitable for `payment.charge` / `payment.authorize`. `collectorData` is the raw 65-byte EOA signature. |
There was a problem hiding this comment.
Reference-doc detail bleeding into changelog. The Parallels signReceiveAuthorization framing and the 65-byte EOA-signature encoding belong in API docs. Suggest trimming to: list the four exports + that they return {collectorData, tokenCollector} suitable for payment.charge / payment.authorize. Drop the byte-layout.
| @@ -46,10 +46,30 @@ jobs: | |||
| with: | |||
There was a problem hiding this comment.
Re: top-of-file block comment (lines 3-11, not in this diff so anchoring to the next in-diff line). Mostly restates the operator runbook in x402r-notes/sdk/RELEASING.md. Suggest trimming to one line pointing at the runbook — the rest will drift out of sync the first time the runbook changes.
| @@ -46,10 +46,30 @@ jobs: | |||
| with: | |||
| egress-policy: audit | |||
There was a problem hiding this comment.
Re: line 10 — # Industry pattern: changesets/changesets uses the same two-job split. (not in this diff, anchoring nearby). Editorializing about peer projects inside YAML. The justification holds, but it belongs in the PR body or RELEASING.md, not in the workflow file where the next reader on-call has to evaluate it. Suggest moving.
| token: ${{ steps.app-token.outputs.token }} | ||
| # Full history so changesets can generate complete changelogs. | ||
| fetch-depth: 0 | ||
|
|
There was a problem hiding this comment.
Re: the cache: pnpm removal comment block (lines 79-83 in the version job, repeated 165-169 in the publish job — neither in this diff, anchoring to the closest in-diff line). Confirming this is a deliberate posture, not inherited copy-paste? The poisoning vector is real — privileged release runner deserializing a cache that ci.yml writes to from untrusted PR runs — but most monorepo changesets workflows keep cache: pnpm and rely on the cache-key scoping to mitigate. The ~10s cold-install cost is fine; just want to make sure the choice is intentional and that the same pattern is being applied to any other privileged workflows in the repo, not just this one.
Three correctness fixes flagged by an audit pass that grep'd each claimed rename against the current SDK source: - lift.md: drop the "ReleaseLocked → CaptureLocked" bullet. Neither error exists in the ABI or source; it was a fictitious rename. - autocapture-wireformat.md: drop @x402r/core from the @x402r/evm range-widening bullet. Core has no peerDependencies block at all. - autocapture-wireformat.md: drop the toPaymentInfo() bullet entirely. toPaymentInfo was removed in this same release (per reconstruct-payment-info.md); claiming a signature change is wrong. Plus restructure of the same release's relocation story: - Delete sdk-authcapture-topaymentinfo-relocation.md. It describes an intermediate "relocate from core to helpers" state that never reaches consumers — the companion changeset removes toPaymentInfo from helpers too. Net effect for users is captured in the reconstruct-payment-info entry. - reconstruct-payment-info.md: add a new bullet noting that @x402r/core no longer declares @x402r/evm as a peer dependency (folded in from the deleted file). Style trims from inline review comments (no semantic change): - lift.md: cut "Clean break — no shims, no back-compat aliases." editorial sentence; the Breaking headers establish it. - lift.md: drop "(see below)" cross-reference for the migration block. - lift.md: drop "(same pattern for charge/capture/void/refund)" parenthetical on slot getters; two concrete examples are enough. - permit2.md: drop "Parallels signReceiveAuthorization" framing and the "65-byte EOA signature" byte-layout — reference-doc territory. - reconstruct-payment-info.md: trim "Handles the wire-to-struct field renames and EIP-3009 / Permit2 branch internally" — implementation detail consumers don't care about. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Drop the 8-line top-of-file block comment that restated the operator runbook (when to dispatch, what each job does) and added a peer-project justification line. Both are kept in x402r-notes/sdk/RELEASING.md, which is the canonical reference. Peer-project justification belongs in PR history or the runbook, not in YAML where the next operator on-call has to skim it. Replace with a single line pointing at the runbook. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Re-review chain completeAddressed both reviews in two new commits.
|
SDK Review: Round 2Found 0 new issues since commit Resolved since last round:
Round-2 corrections I missed in round 1 (verified):
Release.yml trim ( Verified the rest of the rename claims still hold: Grep-audited 25 claimed renames against
Still open from round 1 (out of scope here): Self-correction: the round-1 approval shipped without grepping each claimed rename against Generated with Claude Code using review-sdk skill |
A1igator
left a comment
There was a problem hiding this comment.
Round-2 verified. Approving. See #174 (comment) for round-2 findings (all author corrections verified against SDK source; 0 new issues; round-1 approval dismissed by branch protection on the new commits).
The app-id input on actions/create-github-app-token is deprecated
upstream in favor of client-id. Works today, but emits a per-run
deprecation warning that will eventually become a hard error.
Switch both job invocations from:
app-id: ${{ vars.RELEASE_APP_ID }}
to:
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
The two inputs accept different values — Client ID is a string like
"Iv23li…" or "Iv1.…", distinct from the numeric App ID. Requires a
new repo variable RELEASE_APP_CLIENT_ID with the Client ID from the
App's General settings page. The old RELEASE_APP_ID variable can be
deleted after the next successful workflow dispatch.
x402r-notes/sdk/RELEASING.md updated in parallel to match (App-token
wiring section, troubleshooting entry, rotation procedure note).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A1igator
left a comment
There was a problem hiding this comment.
SDK Review: Round 3
Round-1 nit (app-id deprecation) resolved in ac86048f. 0 new issues.
Verified upstream at the pinned SHA (actions/create-github-app-token@bcd2ba49…, v3.2.0):
client-idis an accepted input onaction.yml(description: "GitHub App Client ID",required: false).app-idcarriesdeprecationMessage: "Use 'client-id' instead."Confirms the nit was real, not speculative.- Both job invocations (version + publish) switched. Variable name
RELEASE_APP_ID→RELEASE_APP_CLIENT_IDmatches the input rename.
Maintainer pre-merge step (blocking): set repo variable RELEASE_APP_CLIENT_ID to the Client ID from the App's General settings page (distinct value from the numeric App ID; Client ID strings start with Iv1. or Iv23li…). If RELEASE_APP_CLIENT_ID is unset on first dispatch after merge, the action fails with empty client-id and no token is minted. The old RELEASE_APP_ID variable can be deleted after the next successful dispatch.
Approving.
Generated with Claude Code using review-sdk skill
Summary
Two coupled changes that close the gap to a clean Version PR + auto-running CI.
Changeset cleanup
.changeset/config.json: switchignorefrom["x402r-sdk"](matched no real package) to["examples"]so the private workspace package stops being versioned into Version PRs.sdk-authcapture-*.mdchangesets as consumer-facing release notes. Removes internal migration code blocks, plan-file references, cross-repo PR refs, "Out of scope" sections, and PR-N-of-N markers.Release-bot App wiring in
release.ymlMint release-bot installation tokenstep in bothversionandpublishjobs, usingactions/create-github-app-tokenv3.2.0 with theclient-idinput.secrets.GITHUB_TOKENfor the App's installation token in bothchangesets/actioncalls.contents: write(both jobs) andpull-requests: write(version job only).Why
Closed Version PR #171 had two structural problems: it pulled
examples/files in (dead-letter ignore entry), and its body was a 500-line concatenation of internal migration prose. Both are fixed at the source.Separately, the prior
GITHUB_TOKENflow meant the Version PR was opened bygithub-actions[bot], which by GitHub's anti-recursion design doesn't trigger downstream workflows — so required CI never fired on the Version PR. Switching to a GitHub App installation token makes the PR appear from a non-github-actions[bot]identity, which triggers workflows like a human-opened PR.The required-CI ruleset bypass list stays empty — the App identity solves the trigger problem without bypass.
Pre-merge requirement
Repo configuration that must be in place before the next
workflow_dispatch:RELEASE_APP_CLIENT_ID(Client ID from the App's General settings page — a string starting withIv23liorIv1., distinct from the numeric App ID)RELEASE_APP_PRIVATE_KEY(App private key.pemcontent, unchanged from prior round)The previously-used
RELEASE_APP_IDvariable can be deleted after the next successful dispatch verifies the new wiring works. (Earlier round of this PR usedapp-id: ${{ vars.RELEASE_APP_ID }}; updated commitac86048switches toclient-id: ${{ vars.RELEASE_APP_CLIENT_ID }}to drop the upstream deprecation warning.)Updated after review
Two reviews landed (one APPROVED, one COMMENTED non-blocking) with 8 inline comments and an audit-gap concern that nobody had verified the rename tables match current SDK exports. Plus a separate forward-looking ask to fix the
app-iddeprecation now rather than later. Addressed via three new commits (fd722d4,0ca1126,ac86048).Audit-driven correctness fixes (grep'd each rename claim against the current SDK source):
lift.md: dropped fictitiousReleaseLocked → CaptureLockedbullet — neither error exists in the ABI or source.autocapture-wireformat.md: dropped@x402r/corefrom the@x402r/evmwidening bullet — core has nopeerDependenciesblock at all. Generalized phrasing to "widen@x402r/evmto ..." since@x402r/clihas it as a dep, not peerDep.autocapture-wireformat.md: deletedtoPaymentInfo()signature-change bullet —toPaymentInfowas actually removed entirely in this release, not signature-changed.sdk-authcapture-topaymentinfo-relocation.md— described an intermediate "relocate from core to helpers" state that didn't reach consumers (the companion changeset removes it from helpers too). Net effect for users is captured inreconstruct-payment-info.md, which now includes a bullet about@x402r/coreno longer declaring@x402r/evmas a peer dependency.Style trims per inline comments:
lift.md: cut "Clean break — no shims, no back-compat aliases." opening; dropped "(see below)" cross-reference; dropped "(same pattern for charge/capture/void/refund)" parenthetical on slot getters.permit2.md: trimmed "Parallels signReceiveAuthorization" framing and "65-byte EOA signature" byte-layout detail.reconstruct-payment-info.md: trimmed "Handles the wire-to-struct field renames and EIP-3009 / Permit2 branch internally" implementation detail.release.yml: replaced the 8-line top-of-file header (which restatedRELEASING.md) with a single-line pointer.Forward-looking fix in
ac86048(drops a per-run deprecation warning):release.yml: switched bothapp-id:inputs toclient-id:, using new variableRELEASE_APP_CLIENT_ID.actions/create-github-app-token'sapp-idinput is upstream-deprecated;client-idaccepts a different value (the App's Client ID, not the App ID — separate identifiers on the App settings page).x402r-notes/sdk/RELEASING.mdupdated onmainto match (variable name in the App-wiring section, troubleshooting check, rotation note).Verified by re-review:
lift.mdcompiles cleanly against currentpayment.getAmounts/payment.capture/payment.voidPaymentsignatures.cache: pnpmno-share posture is consistent across bothrelease.ymljobs. One residual exposure inci.yml'stestjob (Codecov OIDC + cache) has present mitigations (--ignore-scripts, narrow audience, no write to npm/contents). Acceptable as-is.packages/*/src/. Old names confirmed absent.app-id→client-idswap surgically two-line; no other variable references in the repo; deprecation-message upstream confirmed againstaction.yml.Deferred (not blocking, surfaced explicitly):
cache: pnpmfromci.yml'stestjob for full match to release.yml posture.Test plan
client-idinput verified against upstreamaction.ymland README; new variable name follows peer convention.RELEASE_APP_CLIENT_IDvariable in repo Settings.github-actions[bot]) andci.ymlfires automatically.0.3.0-alpha.0via npm Trusted Publishing.examples/files, no PR-N-of-N markers).RELEASE_APP_IDvariable.🤖 Generated with Claude Code