Skip to content

feat(ota): verify main releases before promotion and gate delivery per platform - #3111

Merged
innolope-dev merged 22 commits into
devfrom
fix/ota-platform-floors
Sep 17, 2026
Merged

innolope-dev merged 22 commits into
devfrom
fix/ota-platform-floors

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Updating main automatically publishes a production OTA only after the release is built and verified independently for iOS and Android. The workflow has no manual dispatch path, so merging this PR into dev cannot publish an OTA; publishing begins only when the resulting code later reaches main.

Release behavior

  • Trigger only on pushes to main; manual OTA workflow runs are disabled.
  • Calculate the release version from current-release and reserve the same version for both platform bundles.
  • Upload separate iOS and Android bundle records with their own native compatibility floors.
  • Keep each candidate unavailable while validating its source commit, floor, platform fingerprint, and downloaded artifact.
  • Promote each platform atomically by assigning its production channel and setting rolloutEnabled: false in the same Capgo API mutation, then read the channel back and verify the final state.
  • Use the existing ios-mobile-release and android-mobile-release channels; no shared production channel or shared server floor is required.

Compatibility and recovery

  • Resolve floors against immutable native release tags, including same-version replacement baselines.
  • Emit the platform-aware v3 fingerprint format while continuing to verify older v2 replacement attestations.
  • Recover safely when parallel native release jobs race to create the shared .0 bundle: the losing job proceeds only if a second read proves the exact expected source, floor, and artifact.
  • Treat Capgo's disable_auto_update_to_metadata response as a native store-update requirement in the client updater.
  • Preserve the native-surface and branch guards added by the merged follow-up fixes on dev.

Validation

  • Focused Jest coverage: 9 suites, 270 tests passed.
  • pnpm typecheck passed.
  • Scoped ESLint and Prettier checks passed.
  • bash -n scripts/publish-native-ota.sh passed.
  • actionlint passed for the modified Android and OTA workflows.

No live Capgo settings were changed, no OTA was released, and this PR was not merged.

native-floor is scoped to the current major on purpose — a bundle's
--min-update-version has to sit in the same major band. Provenance is the
opposite question, and reusing the floor meant the first release of a new
major had no answer and skipped the check entirely: a lagging ref could build
2.1.0 without containing v1.6.0, a higher store version carrying older code.

New newest-native mode orders every v<major>.<build>.0 tag numerically on
(major, build) and both guards use it. Only a repository with no native
release at all now skips.
Two changes to the production OTA lane, both about the same thing: removing a
choice that was being made wrongly.

RELEASE TRIGGER. App Release OTA now runs on every push to `main`, and `main`
is the only ref it will ship from — the push trigger is branch-scoped and the
ref guard refuses a dispatch selected anywhere else. Merging to `main` IS the
decision to ship. The trigger it replaces, a human choosing a ref at dispatch
time, is what shipped bundle 1.6.1 from a `main` two days behind the v1.6.0
release: two refs both looked shippable and the older one was picked. Anything
not ready for every install now stops at `dev`, where App Staging OTA publishes
to a channel no production device sees.

Automatic means automatic. The deploy job's Production environment has no
protection rules today, so a merge reaches every install with nobody approving
it; required reviewers there restore a human gate with no workflow change, and
the file says so. One path still publishes a production bundle from a non-main
ref — a native release auto-publishes its matching bundle, and
release-native.yml accepts dev — left alone because releases are cut from dev
today, but flagged in the docs as a decision rather than a leftover.

PER-PLATFORM FLOORS. One release tag names two binaries, and the field does not
keep them in step. TestFlight has no auto-update, so iOS sat on 1.5.0 while
Android moved to 1.6.0 — and every native input that changed between those
releases was under android/. A version number cannot express that: read
numerically, bundle 1.6.x "needs a 1.6 binary" and the entire iOS population is
refused JS its binary runs perfectly. Silently, because the store row is hidden
while the App Store listing is not live, so there is not even a prompt to
explain why updates stopped.

ota-platform-floor.mjs answers it from the surface. Per platform it walks the
v<major>.<build>.0 tags newest-first while that platform's half of the
fingerprint is unchanged; the last release that still matches is the oldest
binary of that platform whose native contract is the one this tree was built
against. On this tree: android 1.6.0, ios 1.5.0.

`shared` is why this is not a path-prefix filter — capacitor.config.ts,
patches/ and the resolved plugin versions sit outside android/ and ios/ while
describing the native half of both. Every NATIVE_INPUTS entry now carries an
explicit platform and a test pins it, so a new input cannot default to the
lenient side. The walk stops at the first mismatch rather than reaching past
it: a native change made and then reverted does not make the binaries in
between able to run this JS.

The floors only widen delivery. The publish gate is untouched — a tree whose
surface differs from the newest release still fails check-native-ota-surface.
Capgo carries one min_update_version per bundle and one bundle serves both
platforms, so the channel gets the lower of the two and the on-device gate
applies each platform's own from the baked NEXT_PUBLIC_OTA_FLOOR_*; taking the
higher would have the server refuse the very population the floors exist to
keep served.

Two gaps are recorded in docs/NATIVE-RELEASE.md rather than papered over: the
floors describe the running bundle, since getLatest() carries no candidate
min_update_version, so across a native-release boundary the gate is permissive
and the server has to refuse (per-platform channels would retire the
approximation); and the native release lanes bake no floors, because their own
tag does not exist yet when they publish.

release-version.mjs grew a newest-native mode and an import guard so
ota-platform-floor can share its tag reader. realpath, not resolve: the suite
copies the script under os.tmpdir(), which on macOS reports /var/... against an
import.meta.url of /private/var/..., and a string compare left the CLI printing
nothing.
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 17, 2026 7:43am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ea145cca-59da-4c4f-8377-270b667fa323

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7997.19 → 7997.89 (+0.7)
Findings: 0 net (+0 new, -0 resolved)

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 7620 ran, 0 failed, 0 skipped, 2.4m

📊 Coverage (unit)

metric %
statements 78.7%
branches 65.8%
functions 72.8%
lines 79.8%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — changes requested

The per-platform floor is read from the running bundle, so affected pre-floor clients cannot receive the fix and floor-bearing clients can admit a later incompatible candidate unless an external server setting intervenes. The release runbook also retains the opposite trigger instructions.

Findings

  • BLOCKING · src/utils/ota-native-gate.ts:97 · The floor comes from the wrong bundle
    An affected iOS 1.5 install is necessarily running a pre-floor bundle today. It therefore falls back to the 1.6.x candidate version and rejects that candidate before download, so this new code and its baked iOS 1.5 floor can never bootstrap onto the population it is meant to unfreeze. After a floor-bearing bundle does land, the inverse is unsafe: a later candidate with a new native surface is checked against the stale floor in the running bundle and is accepted unless Capgo's out-of-band metadata strategy happens to reject it. Provide an explicit bootstrap path and an authoritative candidate/per-platform floor before replacing the candidate-version guard; cover both a pre-floor 1.5 -> 1.6 update and a floor-bearing 1.6 -> native-changing 1.7 candidate.

  • MINOR · docs/NATIVE-RELEASE.md:403 · The runbook still documents the old release trigger
    This automatic-main section is appended immediately after the same section says production is opt-in, never a push side effect, accepts dev/release/android-kyc, and requires a separate manual dispatch. An operator can therefore expect a main merge not to ship or attempt a dev dispatch that the new guard refuses. Replace the obsolete introduction and trigger table instead of leaving both policies in the release runbook.

  • MINOR · scripts/ota-platform-floor.mjs:112 · [claude-opus] --lowest sets Capgo's min_update_version for every install and has no test
    scripts/ota-platform-floor.mjs --lowest is the value the deploy job passes as NATIVE_FLOOR--min-update-version on the Capgo upload (.github/workflows/release-ota.yml:199, :285, :295). That single number is the server-side gate deciding which shipped binaries are served the production bundle — shared state at full population scale, which CONTRIBUTING.md line 519 makes a hard pre-merge test requirement.

The new suite scripts/__tests__/ota-platform-floor.test.js covers the per-platform resolver thoroughly (7 cases), but every helper in it invokes either the default two-line output or --platform; --lowest is never executed. The untested case is exactly the one the code comment at ota-platform-floor.mjs:106-111 names as the failure mode this PR exists to prevent: given floors android 1.6.0 / ios 1.5.0, --lowest must print 1.5.0, not 1.6.0 — picking the higher would have the server refuse the entire iOS population the floors were introduced to keep served, silently, since the App Store row is hidden. compareVersions sorting ascending and [0] being the permissive bound is correct as written; nothing pins it, so a later edit to the sort or the bound reintroduces the 1.6.1 incident with a green suite.

Fix: one case in the existing harness — build the android-only-change repo already used by 'lets an untouched platform keep its older binaries', run the script with --lowest, assert 1.5.0. A cross-major case (v1.6.0 + v2.1.0) would also pin compareVersions' major ordering.

Related, same file family: the new newest-native mode in scripts/release-version.mjs (now the provenance resolver for both release-ota.yml and release-native.yml) has no case in scripts/__tests__/release-version.test.js, while its sibling native-floor does.

Checked clean

  • Pinned head, base, trusted author, repository, and merge base matched the supplied review inputs.
  • Per-platform fingerprint classification and contiguous release-floor calculation resolve this tree to Android 1.6.0 and iOS 1.5.0.
  • Release provenance now selects the newest native tag across majors and both production workflows hold the shared release concurrency group.
  • Exact-head aggregate CI, unit, native-export, typecheck, lint, format, and CodeQL/analyze passed; advisory ds-shots was still running at the final check.
  • Focused local Jest execution was unavailable because the detached worktree has no installed dependencies; the release scripts themselves executed successfully against the pinned tree.
  • No earlier findings or trusted replies were supplied for this pull request, so there was nothing to dismiss or defer.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 1f3fe9276cf3 · Context: repo, engineering · Took 11m

Comment thread src/utils/ota-native-gate.ts Outdated
Comment thread docs/NATIVE-RELEASE.md Outdated
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🖼 Visual diff — ⚠️ no result for the latest push

The newest Tests run produced no diff report in any attempt: ds-shots was skipped, failed before the diff, or had no cached baseline.
The previous result on this comment no longer reflects the latest run, so it was cleared.

Fixture screenshots, no backend. Advisory — this check never blocks a merge.

…y running

Review caught that the floors were being read from the wrong bundle, in both
directions.

Forward: a candidate built after a native release has a higher floor than
anything the running JS knows about, so judging it against the baked constants
accepts JS the binary cannot execute — the exact failure the floors were added
to prevent, reintroduced one release later and dependent on a dashboard setting
nothing here can read.

Backward: an install running a bundle that has the gate but no floors judges
candidates by version alone. An iOS 1.5.0 device on bundle 1.6.2 therefore
refuses every 1.6.x, including the bundle that would have taught it about
floors, so the fix could never bootstrap onto the population it exists to
unfreeze.

Nothing in LatestVersion carries min_update_version, but `comment` is
round-tripped from the upload onto the getLatest() result — verified in both
native implementations — so the publish step writes the numbers there and
parseCandidateFloors reads them before the download. One exact marker, both
platforms or neither, plain X.Y.Z: a comment is otherwise a human string, and a
loose parse of one is how a commit message gets read as a version. No marker
falls back to comparing the candidate's version, which is the conservative
direction. The lane asserts the marker landed rather than assuming it —
--version-exists-ok makes a re-upload a no-op, and an earlier run's comment is
what the fleet would then read.

The baked constants stay for the one question they answer honestly:
runningBundleOutranksBinary(), "is this install running JS built for a native
contract it does not have", which is what the store row should reflect whether
or not a new bundle exists.

The backward half cannot be fixed over the air — a pre-floor install needs a new
binary — and that is recorded as a limit rather than papered over. It is also
the concrete argument for two production channels, one per platform: iOS would
have stayed on a 1.5.x line and never met a 1.6.x candidate.

Docs: the runbook stated both the old "opt-in per release, never a push side
effect" policy and the new automatic one, so an operator could expect a main
merge not to ship, or attempt a dev dispatch the guard refuses. The obsolete
intro and trigger table are replaced rather than appended to.
Writing the contract as an executable test found the hole immediately. The
bundle comment carries the commit subject as well as the floors, and the marker
regex was unanchored — so a subject reading

  fix: ota-floors: android=9.9.9 ios=9.9.9 was wrong

won over the real numbers the lane appends after it. A floor of 9.9.9 refuses
every bundle on every binary: fleet-wide OTA death by commit message. The
inverse is as bad — 0.0.0 accepts everything, including JS built against a
native surface the binary lacks.

The lane always appends its marker last, so anchoring the parse to the end of
the comment makes the lane's numbers the ones read and a subject unable to
forge a floor whatever it contains.

The test executes the contract rather than restating it: the workflow's own
COMMENT assignment builds the string through bash, the app's own FLOOR_MARKER
reads it back, and both are extracted from their files. Cases cover a
version-shaped subject, a subject naming the marker, quotes and brackets, a
multi-line message, and a well-formed marker that is not last. Reading the
workflow as text rather than parsed YAML because the suite has no yaml
dependency and the other workflow tests here assert the same way.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — changes requested

P1, P2, and P6 are fixed at this head. P3-P5 and both supplied --lowest test findings remain: staged candidate metadata is still discarded, platform floors cross major bands, and deployment verification is not scoped to the uploaded bundle. Exact-head substantive CI is green.

Findings

  • BLOCKING · src/utils/capgo-updater.ts:112 · Candidate floors are lost before the staged-bundle gate
    On an iOS 1.5.0 install, getLatest() can admit bundle 1.6.3 here using its ios=1.5.0 candidate floor, but only the downloaded bundle id/version are queued. On the next launch, readStagedBundle() calls needsStoreUpdate(next.version) without the comment, falls back to 1.6.3, and disarms this compatible bundle, so floor-aware clients never apply the OTA. Persist the parsed floors with the staged bundle keyed by id and pass them into the second gate, while retaining the conservative fallback for legacy staged entries.

  • MAJOR · scripts/ota-platform-floor.mjs:58 · Platform floors still cross major-version bands
    allNativeReleases() is unfiltered, so with v2.1.0 newest and an unchanged iOS surface through v1.6.0 this loop returns an iOS floor of 1.6.0. The release-version contract requires a bundle's min_update_version to stay in the same major band; the candidate marker also makes a 1.6 binary accept a 2.1.x bundle across the deliberate app-generation boundary. Stop the scan when the newest/current major changes and add a cross-major regression case.

  • MAJOR · .github/workflows/release-ota.yml:339 · Floor verification is not scoped to the uploaded bundle
    On a re-run where --version-exists-ok leaves VERSION with a stale or missing comment, any older bundle in the global bundle list carrying the same common floors satisfies this grep. The step then succeeds although the served candidate falls back to version gating. Query or parse the exact $VERSION record first, then assert that record's comment contains the expected marker.

  • MINOR · scripts/ota-platform-floor.mjs:112 · The shared server-floor calculation still lacks regression coverage
    --lowest supplies production's single Capgo min_update_version, but the resolver tests exercise only per-platform/default output. Reversed platform floors or major ordering can therefore change server eligibility without failing the suite. Add CLI tests invoking --lowest with Android lower and iOS lower, plus the major-boundary case once fixed.

  • MINOR · scripts/ota-platform-floor.mjs:112 · The --lowest deployment contract remains untested
    release-ota.yml publishes every install through this output, yet no test invokes the --lowest CLI branch or asserts that the workflow passes it to --min-update-version. A refactor can leave the platform unit cases green while production uploads the wrong shared floor. Add an executed --lowest case and pin its workflow handoff.

  • MINOR · scripts/release-version.mjs:148 · [claude-opus] newest-native gates every production OTA and has no test
    newestNative() (scripts/release-version.mjs:148) and its newest-native mode are new in this PR and are now the sole input to the "Guard release provenance" step in both .github/workflows/release-ota.yml:93 and .github/workflows/release-native.yml:78. That guard decides whether a bundle may be published to the production channel, i.e. whether shared state reaching every install gets mutated.

scripts/tests/release-version.test.js covers native, ota, staging, native-floor and validate; a grep for newest-native across scripts/tests returns nothing, and allNativeReleases() is likewise unexercised except indirectly through ota-platform-floor.test.js.

The exact untested case is the one the mode was added for, per the comment at scripts/release-version.mjs:143-147: package.json has advanced to a major with no tag yet (say major 2) while the newest release tag is v1.6.0. native-floor throws there — which is why the old release-native.yml guard did exit 0 and skipped the check — whereas newest-native must return 1.6.0 so the guard still demands the ref contain it. Nothing pins that. A regression that made newest-native throw or return the current-major answer would silently restore the skip, and a lagging ref could build 2.1.0 without containing v1.6.0: a higher store version carrying older code, the TASK-21793 fleet-strand this guard exists to stop.

Fix: add a newest-native block to scripts/tests/release-version.test.js using the existing repo() helper — (a) package.json 2.0.0 with tags ['v1.0.0','v1.6.0'] resolves 1.6.0, (b) cross-major ordering, e.g. tags ['v1.6.0','v2.1.0'] resolves 2.1.0, and (c) a checkout with v* tags but no v<major>.<build>.0 tag exits non-zero.

  • MINOR · scripts/ota-platform-floor.mjs:105 · [claude-opus] --lowest sets Capgo's fleet-wide min_update_version and has no test
    Same issue as prior finding P7/P8, still open at f5dd3e4. scripts/ota-platform-floor.mjs:105 --lowest is the only consumer of compareVersions (scripts/ota-platform-floor.mjs:88), and its output flows to steps.ota_floors.outputs.lowestNATIVE_FLOOR--min-update-version on the bundle upload in .github/workflows/release-ota.yml:284. That value is server-side state applied to every install on the production channel.

scripts/tests/ota-platform-floor.test.js exercises the default two-line output, --platform, and three error paths; no case invokes --lowest. So the assertion the code's own comment makes — that the server gets the permissive bound, because picking the higher would have Capgo refuse the very iOS 1.5.0 population these floors exist to keep served — is unverified.

The exact untested case: the v1.6.0 shape already built by the first test in that file (android floor 1.6.0, ios floor 1.5.0) run through --lowest must print 1.5.0. Add the inverse (ios-only change → android 1.4.0, ios 1.5.0 → 1.5.0) so a sign flip in compareVersions cannot pass, and an equal-floors case. The helper is three lines given floorsFail's existing spawnSync shape.

Checked clean

  • Exact head, base, merge base, trusted author, and PR metadata matched the supplied values.
  • P1 is fixed: the initial candidate gate now parses an end-anchored floor marker from getLatest().comment, with an executed workflow/parser contract test.
  • P2 and P6 are fixed: the runbook and workflow consistently describe push-to-main production releases and a manual rerun path.
  • Release provenance across majors, branch/ref guards, native-input platform classification, and candidate-marker forgery resistance were inspected with no additional defect found.
  • Exact-head unit, typecheck, native-export, eslint, ds-lint, format, CodeQL/analyze, authorship, and aggregate CI checks passed; visual and preview jobs were still in progress.
  • A focused local Jest run was unavailable because this detached worktree has no installed Jest binary.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 2 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: f5dd3e40c430 · Context: repo · Took 12m

Comment thread src/utils/capgo-updater.ts
Comment thread scripts/ota-platform-floor.mjs
Comment thread .github/workflows/release-ota.yml Outdated
Comment thread scripts/ota-platform-floor.mjs
Comment thread scripts/ota-platform-floor.mjs
… boundary

Four defects from review, one of which made the whole thing a no-op.

The floors were lost across the launch boundary. A bundle is admitted at check
time, when the comment is in hand, but applied on a LATER launch — and the
plugin's queue carries only an id and a version, since BundleInfo has no
comment field. So the launch-time gate re-asked with nothing to answer from,
fell back to the version rule, and disarmed the bundle the check had just
approved: an iOS 1.5.0 install would download 1.6.3 and throw it away on every
launch, forever. The marker is now kept beside the staged id — one entry,
replaced on each stage, dropped with the queue, and a mismatched id reads as
"no floors", which is also what a bundle staged before this existed gets.

The floor scan crossed major bands. allNativeReleases() is unfiltered, so a 2.x
tree with an untouched iOS surface resolved an iOS floor of 1.6.0 — and since
the on-device comparison checks majors before builds, every 1.6 binary would
have accepted a 2.x bundle. A major is a deliberate app-generation break and
release-version.mjs keeps a floor inside one band; the scan now stops at the
boundary even where the surface matches.

The marker assertion was not scoped to the bundle it was asserting. A bare
search of the listing passes on any older bundle sharing the same floors, which
is exactly the re-run case: --version-exists-ok leaves the earlier comment in
place, the step goes green, and the served candidate silently falls back to
version gating. Now filtered to that version's own row.

And --lowest had no coverage despite being the single number every install's
eligibility goes through. Cases for either platform being lower, numeric build
ordering (1.9 vs 1.10), the major boundary, and a pin on the workflow handoff
to --min-update-version.

The end-to-end staged-apply case also caught that this suite never mocked
isIOSNative, so the real one answered false under jsdom and every case was
silently exercising the Android path.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

Candidate-floor persistence, major-band isolation, and --lowest coverage are fixed. Three findings remain: bundle verification still accepts prefix-matching versions, newest-native lacks direct regression coverage, and the runbook still contradicts the automatic production trigger.

Findings

  • MAJOR · .github/workflows/release-ota.yml:344 · Floor verification still accepts a different version row
    The new row filter is still a substring match. With VERSION=1.6.3, a bundle-list row for 1.6.30 carrying the expected marker passes both greps even when 1.6.3 has a stale or missing marker after --version-exists-ok. The workflow then reports success while the served candidate falls back to version gating. Parse the listing's version field, or match it with explicit field boundaries, before checking that exact row's comment.

  • MINOR · scripts/release-version.mjs:148 · newest-native still has no direct regression test — also flagged by claude-opus
    newest-native is the provenance input for both production release workflows, but release-version.test.js still never invokes this mode. The load-bearing case remains unpinned: package.json is major 2, only v1.6.0 exists, and the resolver must return 1.6.0 rather than fail or scope itself to major 2. Add direct success, cross-major ordering, and no-native-tag cases using the existing fixture helper.

  • MINOR · docs/NATIVE-RELEASE.md:386 · The runbook still calls every release workflow manual
    This added section correctly says that merging to main ships production OTA, but the earlier release instructions still say all three workflows require branch selection and that none is automatic (lines 216-240). An operator following that section can merge expecting a later dispatch after the OTA is already live. Update the earlier section to distinguish the manual native lanes from the automatic main OTA trigger.

Checked clean

  • Exact head, base, merge base, trusted author, and pull-request metadata matched the supplied values.
  • P1, P3, and P10 are fixed: the offered candidate marker is authoritative and is persisted by bundle id for the later staged-bundle gate.
  • P2 is fixed at its cited section; the separate earlier runbook contradiction remains as P13.
  • P4 and P11 are fixed: the floor walk stops at the newest major and has cross-major regression coverage.
  • P6, P7, P9, and P14 are fixed: --lowest has divergent-platform, numeric-order, major-boundary, and workflow-handoff coverage.
  • The remaining P5/P12 verification class was reproduced locally: target 1.6.3 passes against a 1.6.30 listing row with the expected marker.
  • Exact-head unit, typecheck, native-export, eslint, format, design lint, CodeQL/analyze, authorship, and aggregate CI checks passed; the visual screenshot job was still running at review time.
  • A focused local Jest run was unavailable because the detached worktree has no installed dependencies.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 599122d122ba · Context: repo · Took 11m

Comment thread .github/workflows/release-ota.yml Outdated
Comment thread scripts/release-version.mjs Outdated
Comment thread docs/NATIVE-RELEASE.md Outdated
…he major

Row match. The version filter was still a substring, so a bundle-list row for
1.6.30 satisfied it when 1.6.3 was what shipped — the re-run case again, where
--version-exists-ok leaves an earlier comment in place and the assertion goes
green while the served candidate falls back to version gating. Dots are escaped
and the boundaries exclude digits, dots and `-`, so 1.6.30, 11.6.3, 1.6.3.1 and
1.6.3-rc1 no longer answer for 1.6.3. The test executes the workflow's own
pipeline rather than a copy: an earlier draft that reassembled the whole
if-block failed its positive case while every negative passed vacuously, which
is the one way a test like this is worse than none.

newest-native had no direct coverage, and writing it found a hole neither
review named. `v*` is loose enough that a date-shaped tag matches the release
pattern exactly — v2026.02.0 would have become "the newest native release" and
made the provenance guard demand every ref contain a tag that never shipped a
binary. (main really does carry a v2026.02.26; only its third segment keeps it
out today.) Now bounded to majors at or below the one package.json declares,
which still answers for the case the mode exists for: package.json at major 2
with only v1.6.0 tagged returns 1.6.0 rather than failing.

Docs: §6 still said all three workflows are manual and none is automatic, so an
operator could merge to main expecting to dispatch later, after the OTA had
already shipped. The native lanes and the OTA lane are now described as what
they are.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

Most prior findings are fixed, but floor verification can still accept another bundle's row, and the new major bound lets a stale release ref ignore a newer-major native release.

Findings

  • MAJOR · .github/workflows/release-ota.yml:350 · Floor verification still searches the whole row
    With VERSION=1.6.3, the 1.6.3 row can lack the marker while an older 1.6.2 row carries it and has a commit subject such as fix 1.6.3 rollout. The first grep matches that exact version text in the subject and the second finds the marker, so verification passes the wrong bundle after --version-exists-ok. Parse the listing's version field/record and inspect the marker only on the record whose version equals VERSION.

  • MAJOR · scripts/release-version.mjs:156 · The provenance guard ignores newer-major releases
    After a legitimate v2.1.0 release, dispatching an accepted stale major-1 ref that contains v1.6.0 filters v2.1.0 out here. newest-native returns 1.6.0, the ancestry guard passes, and native resolves 1.7.0, allowing a new store build with older code—the exact stale-ref failure this guard exists to prevent. Keep provenance ordered across all genuine native-release tags; exclude date-shaped/non-release tags by validating their release attestation or tag metadata instead of bounding them to the current ref's package major.

Checked clean

  • Exact head, base, merge base, trusted author, repository, and pull-request metadata matched the supplied inputs.
  • P1 and P3 are fixed: the offered candidate marker is authoritative and is persisted by staged bundle id for the later apply gate.
  • P2 and P10 are fixed: the release runbook now distinguishes manual native lanes from push-to-main production OTA.
  • P4, P6, P7, and P12 are fixed: platform floors stop at the newest major and --lowest has direct divergent-platform, numeric-order, major-boundary, and workflow-handoff coverage.
  • P5's broad global-marker match is fixed by version filtering; the remaining exact-row defect is reported under P8.
  • P9 and P11 are fixed as missing-test findings: newest-native now has direct success, cross-major, numeric-order, tag-exclusion, and failure cases.
  • Replayed the workflow matcher: a 1.6.2 row whose subject mentions 1.6.3 still satisfies verification for VERSION=1.6.3.
  • Reproduced the provenance regression in a temporary clone: with package major 1 and a non-ancestor v2.1.0 tag, newest-native returned v1.6.0, the selected ancestry check passed, and native resolved 1.7.0.
  • Exact-head aggregate CI, unit, native-export, typecheck, lint, format, design lint, CodeQL/analyze, authorship, and preview checks passed; ds-shots was still in progress.
  • The release scripts resolved the pinned tree to newest native 1.6.0, shared floor 1.5.0, Android floor 1.6.0, and iOS floor 1.5.0; focused local Jest was unavailable because the detached worktree has no installed Jest binary.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: c8d025e60cb7 · Context: repo · Took 10m

Comment thread .github/workflows/release-ota.yml Outdated
Comment thread scripts/release-version.mjs Outdated

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — changes requested

Request changes: the exact repository history makes the new replacement-aware floor resolver fail closed before any production OTA can build. The stale rollout documentation and promotion race are fixed.

Findings

  • BLOCKING · scripts/ota-platform-floor.mjs:74 · Keep existing replacement attestations verifiable
    P16 remains reachable with the repository's real release history. At this head, node scripts/ota-platform-floor.mjs --platform android follows this baseline lookup to the existing android-v1.5.0-replacement-189ecdcad7e9 tag and exits because its stored fingerprint no longer matches the digest recomputed by the split Android/iOS/shared manifest. The tagged tree has not changed; the fingerprint schema did. Since the resolve job runs this command before every automatic main OTA, the first main push cannot resolve floors or deploy. Version the fingerprint/attestation schema and verify existing v2 tags with their original digest rules, or provide another backwards-compatible migration, then cover a pre-split attestation in regression tests.

Checked clean

  • Confirmed the detached worktree HEAD, merge base, trusted author, dev base ref, and exact base/head SHAs.
  • Checked exact-head GitHub checks: aggregate CI, unit, native export, typecheck, formatting, lint, CodeQL, preview, and review checks completed successfully; the visual screenshot job was still running.
  • Checked all supplied prior findings against this head. P1-P15 remain repaired, P17's stale OTA rollout instruction is removed, and P18's production promotion race is closed by one rollout-disabling version mutation plus readback.
  • Executed the real per-platform floor resolver and native-surface guard against the repository tags. Both reject the existing Android replacement attestation after the fingerprint manifest schema change; the blocking release failure is reported above.
  • Reviewed replacement-tag ancestry and validation, platform fingerprint partitioning, floor traversal, candidate isolation, exact bundle verification, production mutation ordering, and the updated native release runbook.
  • JavaScript syntax checks and diff whitespace validation passed. Targeted Jest could not run because dependencies are absent from the detached worktree; exact-head unit CI is green, but its fixture creates attestations with the current schema and does not cover this historical tag migration.
  • Security, adversarial, and slop passes found no additional actionable defect beyond the release-blocking attestation compatibility failure.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion: did not run — claude-api_error. This review is one reviewer short.

Exact head: 73bd9a30c051 · Context: repo · Took 9m

Comment thread scripts/ota-platform-floor.mjs

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

Clean at the pinned head. All 18 supplied prior findings are fixed, including preservation of immutable v2 replacement attestations alongside the new platform-split v3 schema; no new evidence-backed defect was found.

Checked clean

  • Confirmed the detached worktree is clean at the exact supplied head, its merge base is the supplied dev SHA, and trusted pull-request metadata matches the requested repository, author, head, and base.
  • Rechecked candidate-floor admission, staged-floor persistence by bundle id, launch-time gating, and the running-bundle store-update signal.
  • Rechecked contiguous same-major platform-floor resolution, the shared --lowest contract, and attested same-version replacement baseline selection.
  • Recomputed the real immutable Android v1.5.0 replacement tag: its v2 digest matches 420de4c7daa9541a, its v3 digest differs, and current floors resolve to Android 1.6.0 and iOS/shared 1.5.0.
  • Rechecked platform-split dependency fingerprints and schema-aware v2/v3 replacement validation, including fail-closed ancestry, scope, and attestation checks.
  • Rechecked exact Capgo bundle identity and pagination, candidate-channel audience restrictions, source and artifact metadata, production rollout preflight, atomic rollout disablement during promotion, and post-promotion readback.
  • Rechecked main-only automatic OTA triggering, cross-major native-release provenance, release serialization, and the corresponding native-release runbook guidance.
  • Exact-head unit, native-export, typecheck, lint, formatting, CodeQL, and aggregate ci-success checks passed. A local focused Jest rerun was unavailable because the detached worktree has no installed Jest binary.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion: did not run — claude-api_error. This review is one reviewer short.

Exact head: ec67c340856f · Context: repo · Took 11m

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

All 18 supplied prior findings remain fixed at the merged head, but two minor merge regressions remain: obsolete single-bundle floor guidance and removal of the sticky mobile login action.

Findings

  • MINOR · scripts/ota-platform-floor.mjs:5 · Restore the per-platform floor contract
    Production now uploads separate iOS and Android records, each with its own min_update_version, but this new guidance says one bundle serves both platforms and presents the permissive --lowest value as the server contract. A maintainer following it could reintroduce the unsafe shared floor that the platform records removed. Restore the separate-record explanation and mark --lowest as diagnostic-only or remove the obsolete option.

  • MINOR · src/components/LandingPage/StickyMobileCTA.tsx:97 · Keep login in the sticky mobile CTA
    With migration disabled, scrolling past 300px now leaves the sticky bar with only the signup route; the existing /setup?step=login action was deleted to satisfy the overflow work. A returning user must scroll back to the hero to log in. Preserve the login action with a wrapping or stacked mobile layout and cover the migration-off sticky state.

Checked clean

  • Confirmed the detached worktree is clean at the exact supplied head, its merge base is the supplied dev SHA, and pull-request metadata matches the requested repository, trusted author, head, and base.
  • Rechecked candidate-floor admission, staged-floor persistence by bundle id, launch-time gating, and the running-bundle store-update signal.
  • Rechecked contiguous same-major platform-floor resolution, per-platform server floors, and attested same-version replacement baseline selection.
  • Recomputed the real immutable Android v1.5.0 replacement tag: its v2 digest matches 420de4c7daa9541a, and current floors resolve to Android 1.6.0, iOS 1.5.0, and diagnostic lowest 1.5.0.
  • Rechecked platform-split dependency fingerprints and schema-aware v2/v3 replacement validation, including fail-closed ancestry, scope, and attestation checks.
  • Rechecked exact Capgo bundle identity and pagination, candidate-channel restrictions, source and artifact metadata, production rollout preflight, atomic rollout disablement during promotion, and post-promotion readback.
  • Rechecked main-only automatic OTA triggering, cross-major native-release provenance, release serialization, native release publication, and the corresponding runbook guidance.
  • Reviewed the per-locale overflow detector, synthetic detector coverage, route and fixture coverage, responsive typography fixes, and the mobile CTA behavior.
  • Exact-head unit, native-export, typecheck, lint, formatting, design, CodeQL, overflow/visual, analyze, preview, and aggregate ci-success checks passed. A local focused Jest rerun was unavailable because the detached worktree has no installed Jest binary.

Security review: did not run — the model reasoned past its deadline without writing an answer. This review is one reviewer short.

Third opinion: did not run — claude-api_error. This review is one reviewer short.

Exact head: 9960719dc620 · Context: repo · Took 19m

Comment thread scripts/ota-platform-floor.mjs Outdated
Comment thread src/components/LandingPage/StickyMobileCTA.tsx

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

The OTA release, floor, fingerprint, and promotion fixes are coherent, but two previously raised minor UX/documentation defects remain at this head.

Findings

  • MINOR · src/components/LandingPage/StickyMobileCTA.tsx:97 · Keep login in the sticky mobile CTA
    Once a mobile visitor scrolls 300px, the persistent bar exposes only Download or Sign up; the Log in route remains above the fold. A returning web user reading farther down the page must scroll back to authenticate. Restore a secondary /setup?step=login link alongside the sticky action and cover both migration states.

Inline anchors unavailable for 1 finding(s); the findings remain in this summary.

Answered by a human, not raised again

These are still present as far as this review can tell. You resolved the thread or deferred the fix, so Chip files them instead of repeating them.

  • MINOR · docs/NATIVE-RELEASE.md:386 · Remove the remaining manual OTA instruction — no task filed, please file one

Checked clean

  • Exact head, supplied base, trusted author, and PR metadata match; reviewed the final 18-file diff.
  • Per-platform floor scanning, major boundaries, replacement baselines, v2/v3 fingerprints, dependency classification, and the --lowest diagnostic contract.
  • Candidate isolation, exact bundle source/floor/artifact checks, production channel policy validation, atomic rollout-disabled promotion, and concurrent native-upload recovery.
  • Candidate, staged, and running-bundle native gates plus Capgo metadata-rejection handling on device.
  • Eleven focused OTA/provider suites passed locally (298 tests); the landing CTA suite could not resolve next-intl from the detached dependency setup.
  • Exact-head Code analysis and Preview deploy passed; the GitHub Tests workflow was still pending during review.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion: did not run — claude-api_error. This review is one reviewer short.

Exact head: e9f591c9492d · Context: repo · Took 16m

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

Clean at the exact head. The remaining runbook defect is fixed by distinguishing the manually dispatched native bootstrap bundle from the automatic production OTA lane, and no new defect survives the correctness, security, adversarial, or slop passes.

Checked clean

  • Verified the detached worktree head, supplied base SHA and merge base, trusted author, base ref, and effective diff.
  • Rechecked candidate and staged floor propagation, exact bundle metadata verification, platform-specific server floors, and running-bundle store gating.
  • Rechecked native release provenance, major-version boundaries, replacement-baseline schema compatibility, platform dependency partitioning, and the diagnostic-only --lowest contract.
  • Rechecked candidate isolation and atomic production promotion, including active or unreadable rollout state and exact post-mutation readback.
  • Rechecked the release runbook for stale manual production OTA and staged-rollout instructions; remaining manual wording applies only to native releases or staging.
  • Recomputed the real v2 Android replacement attestation as 420de4c7daa9541a and resolved current platform floors as Android 1.6.0 and iOS 1.5.0.
  • Exact-head ci-success, unit, native-export, typecheck, lint, format, and CodeQL checks passed; ds-shots was still in progress. Focused local Jest was unavailable because this detached worktree has no installed dependencies.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion: did not run — claude-api_error. This review is one reviewer short.

Exact head: baabafcded9e · Context: repo · Took 19m

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — changes requested

One blocking deployment-order race remains: a superseded main SHA can still be promoted. The supplied prior OTA findings are fixed or no longer apply at this head.

Findings

  • BLOCKING · .github/workflows/release-ota.yml:19 · Do not promote a superseded main SHA
    Push A can start this workflow and still be building when push B updates or reverts main. Because the shared concurrency group keeps the running job alive and no step checks that GITHUB_SHA still equals the current main head, A proceeds to promote its now-superseded bundles while B waits for it to finish. Production then serves code that main has already reverted. Keep promotion non-cancelable, but add a latest-main SHA guard after the build and immediately before the first production mutation, with a regression test for a stale push run.

Checked clean

  • Confirmed HEAD, the supplied base SHA, and the merge base exactly match the dispatched review target.
  • P1 fixed: candidate and staged gates use the offered bundle's floor metadata rather than the running bundle's floor.
  • P2 fixed: the runbook documents the automatic main-only OTA trigger.
  • P3 fixed: candidate floors are persisted before staging and restored for the later launch-time gate.
  • P4 fixed: platform-floor scanning stops at the native major boundary.
  • P5 fixed: uploaded floors are verified on the exact named bundle record.
  • P6 fixed: shared server-floor behavior has direct platform-floor regression coverage.
  • P7 fixed: the diagnostic --lowest contract has direct regression coverage and is excluded from production workflow use.
  • P8 fixed: bundle verification matches the exact version object rather than accepting another version row.
  • P9 fixed: newest-native has direct provenance regression coverage, including cross-major cases.
  • P10 fixed: the runbook distinguishes automatic OTA releases from manual native releases.
  • P11 fixed: floor verification parses structured bundle fields instead of searching an arbitrary row string.
  • P12 fixed: newest-native provenance considers attested releases across majors.
  • P13 fixed: Android and iOS runtime dependencies advance only their own platform floor while shared plugins remain shared.
  • P14 fixed: active or unreadable rollouts fail closed, and promotion sets the stable bundle plus rolloutEnabled=false in one mutation with readback.
  • P15 fixed: native startup independently evaluates the running bundle floor and exposes store-update-required state.
  • P16 fixed: platform floors resolve and validate attested same-version replacement baselines.
  • P17 fixed: the stale staged-rollout instruction now describes direct stable promotion with rollout disabled.
  • P18 fixed: immutable v2 replacement attestations retain their legacy fingerprint calculation while new attestations use v3.
  • P19 fixed: production uses separate platform bundle records and documents --lowest as diagnostic-only.
  • P20 no longer applies: StickyMobileCTA is absent from the effective base-to-head diff and its inherited behavior is outside this PR.
  • P21 fixed: the remaining sentence now refers only to manually dispatched native bootstrap publication, not manual OTA publication.
  • Exact-head GitHub checks showed the two review checks successful and manual jobs skipped; no standard build or test gate was present on this SHA.
  • Ran 11 focused suites covering release guards, floors, fingerprints, replacement attestations, updater gating, and friendly errors: 327 tests passed.
  • git diff --check, shell and Node syntax checks, scoped Prettier, and scoped ESLint passed.
  • A full typecheck was not representative in the isolated test copy because generated Next declarations and the content submodule were not initialized; the changed TypeScript paths compiled through their focused Jest suites.
  • Confirmed the backend sibling emits XCHAIN_WITHDRAW_DISABLED and the live Lexicon permits the withdrawal, transfer, chain, and Arbitrum terminology used by the new localized copy.
  • Confirmed from Capgo's public channel API contract that one POST can update the stable version and set rolloutEnabled=false, followed by readback.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: a1942dd23ecb · Context: repo, backend sibling, product Lexicon, Capgo API docs · Took 26m

Comment thread .github/workflows/release-ota.yml

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — changes requested

The OTA floor, attestation, rollout, and client-gate fixes hold, but the superseded-main race remains between the final ref check and the Capgo promotions.

Findings

  • BLOCKING · .github/workflows/release-ota.yml:282 · Close the final superseded-main promotion window
    Because cancel-in-progress remains false, a second push can advance refs/heads/main after this git ls-remote returns. The old run then crosses the GitHub step boundary and promote-production publishes both stale bundles while the newer run is only queued, so a superseded SHA still reaches production and can remain there if the queued run later fails. Put the current-main check inside the promotion step immediately before each Capgo mutation, and combine it with a latest-wins or release-lock mechanism that prevents a newer main push from superseding a run during that check-to-mutation interval.

  • MINOR · src/utils/friendly-error.utils.tsx:257 · [claude-opus] Per-account cross-chain disable is not in product truth
    This PR ships user-visible copy for a new account state: "Withdrawals to other networks are limited on your account…" / "Transfers to other networks are limited on your account. Contact support for help." (src/i18n/app/messages/en.json and the three other locales), driven by XCHAIN_WITHDRAW_DISABLED. The backend lever is real — peanut-api-ts/src/rhino/provision-gate.ts:119-131 returns it, and mono/ops/scripts/withdrawals/xchain-withdraw-policy.ts is the ops tool that sets it.

product/networks.md:350 is the source of truth and knows only one account-level lever for cross-chain: the count cap (10/h, 20/24h, 30/30d) plus "a user who hits it sees 'You reached the limit for withdrawals to other networks' and can ask support to raise it (ops script ops/scripts/withdrawals/set-xchain-withdraw-limit-exempt.ts)". Two things now disagree with the code: (a) there is no documented state where cross-chain is off with no expiry and no raise path, which is exactly what the new copy tells the user; (b) the script it names does not exist in mono/ops/scripts/withdrawals/ (that directory has xchain-withdraw-policy.ts). The doc is the wrong side here — the code and ops tooling are shipped.

Consequence is concrete for support: a user quoting the new "limited on your account" copy will be told, per product truth, that support can raise their limit, when the account has actually been policy-disabled and raising the cap changes nothing.

Fix: update product/networks.md §limits to record the per-account cross-chain disable (no expiry, Arbitrum still unlimited, set/cleared via ops/scripts/withdrawals/xchain-withdraw-policy.ts) alongside the existing count cap, and correct the stale exempt-script filename.

Checked clean

  • Confirmed detached HEAD c45fd36, trusted author innolope-dev, PR 3111, dev base fdc1683, and the supplied merge base.
  • Reviewed the complete effective diff across automatic main releases, candidate upload and verification, production mutation and readback, native fingerprint schemas, replacement baselines, per-platform floors, client-side floor gating, documentation, and the cross-chain disabled-error mapping.
  • Rechecked every supplied prior finding: P1-P19 are fixed at this head, P20 is absent from the effective diff, and P21 remains through the final check-to-promotion race.
  • All 209 focused release-contract tests and 185 focused client/error tests passed; current repository floors resolve to Android 1.6.0 and iOS 1.5.0, and the real v2 replacement attestation recomputes its stored fingerprint.
  • The exact-head GitHub review check is green; git diff --check and publish-native-ota shell syntax pass. actionlint was unavailable locally.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: c45fd36dfbc5 · Context: repo, sibling, ci · Took 15m

Comment thread .github/workflows/release-ota.yml Outdated

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

No findings.

Checked and not raised again

  • MINOR · src/utils/friendly-error.utils.tsx:257 · [claude-opus] Per-account cross-chain disable is not in product truth — this review checked it and does not believe it. No task filed.

Checked clean

  • Confirmed the detached worktree HEAD and merge base match the supplied SHA pair, and the trusted PR metadata matches repository peanutprotocol/peanut-ui, author innolope-dev, and base dev.
  • Rechecked prior findings P1-P20 at this head: the native-floor source, staged candidate floors, exact bundle verification, major-band provenance, platform-scoped dependency fingerprints, rollout-safe promotion, running-floor UI wiring, replacement attestations, release documentation, and regression coverage are present; StickyMobileCTA is unchanged from the supplied base.
  • P21 is fixed: the workflow re-reads refs/heads/main after the native build and aborts before candidate publication when the run SHA is superseded.
  • P22 is fixed: the main-SHA guard now runs inside the platform loop immediately before each independent Capgo production mutation.
  • Verified the production mutation validates channel policy, atomically selects the exact platform bundle with rollout disabled, and reads back both channel state and exact artifact metadata.
  • Verified the immutable Android v2 replacement tag recomputes its attested fingerprint, while current platform floors resolve to Android 1.6.0 and iOS 1.5.0 and remain within the newest major.
  • Checked the cross-chain-disabled UI mapping against the live Lexicon and the current peanut-api-ts sibling: the Lexicon does not define this implementation detail, while the API exposes and tests XCHAIN_WITHDRAW_DISABLED for both withdrawal and request-payment paths.
  • Exact-head CI review is green. Local syntax checks and bash -n passed; actionlint and Jest were unavailable in the dependency-free detached worktree, so those gates were not duplicated locally.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: ca869ae940c3 · Context: repo, ci, peanut-api-ts sibling, Notion Lexicon · Took 14m

# Conflicts:
#	scripts/__tests__/capgo-release-guard.test.js
#	scripts/__tests__/ota-platform-floor.test.js
#	scripts/ota-platform-floor.mjs
#	scripts/publish-native-ota.sh
#	src/utils/capgo-updater.ts

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

No findings at this head; the supplied OTA release-safety findings are fixed or inapplicable.

Findings

  • MINOR · scripts/tests/release-branch-guards.test.js:48 · [claude-opus] The new stale-main promotion gate is only asserted by substring matching
    guard_current_main (.github/workflows/release-ota.yml:283-290) is the control that decides whether a bundle built from a superseded main SHA reaches every production install — it is the fix for P21/P22. It is never executed by a test. The only coverage is scripts/tests/release-branch-guards.test.js:48-63, which greps the YAML for literal strings, including the whitespace-sensitive 'guard_current_main\n # Bundle selection'. That same revision deletes the promotion() harness (scripts/tests/capgo-release-guard.test.js, old lines 319-416) that used to extract the real promotion shell from the workflow and run it, so the loop's actual behaviour now has no executable test at all.

Untested cases, named exactly: (1) git ls-remote reports a main SHA different from EXPECTED_MAIN_SHA before the first platform → the step must exit non-zero with zero Capgo mutations; (2) main advances after ios-mobile-release is promoted but before the android iteration → the loop must abort, leaving android on its previous bundle rather than promoting. Both are the exact sequences the PR exists to prevent, and both currently pass CI on string presence alone — a renamed env var, a reindented comment, or the guard moved into a subshell where exit 1 only kills the subshell would keep the assertions green while the gate is inert.

Fix: restore an executable harness in the shape of the deleted one — extract the run: block of "Promote verified bundles to platform production", stub node/git so git ls-remote returns a differing SHA, and assert the step exits 1 and that promote-production was invoked zero times (case 1) and exactly once (case 2). The promote-production mode itself is already well covered by the three new unit tests; this is specifically the shell gate around it.

Checked clean

  • Validated the exact head, base, and merge base in the supplied detached worktree.
  • Rechecked the supplied prior findings against current code; P1-P19 are fixed and P21-P22 are fixed by the post-build and per-platform pre-promotion main-SHA guards.
  • Verified production promotion atomically selects the bundle with rollout disabled, then reads back the complete channel policy and exact artifact.
  • Verified platform-specific dependency floors, replacement-baseline selection, v2/v3 attestation compatibility, native upload race recovery, and client store-update gating.
  • Recomputed the real v2 replacement fingerprint as 420de4c7daa9541a and resolved the Android platform floor to 1.6.0.
  • Ran 342 focused release, fingerprint, floor, updater, and provider tests successfully; diff checks and script syntax checks also passed.
  • Required CI aggregate, unit, native export, typecheck, lint, format, and workflow checks pass at the exact head; three locale screenshot captures were still running when checked.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: c44121c1a7a1 · Context: repo, product · Took 18m

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

Clean re-review: the prior release-safety findings are fixed, the two out-of-diff advisory findings are dismissed, and no actionable defect remains at the exact head.

Checked clean

  • Pinned detached checkout, trusted author, PR metadata, dev base ref, exact base SHA, exact head SHA, and merge base all match the supplied review target.
  • Prior P1, P3, and P15 are fixed: candidate floors survive staging, admission uses the offered bundle's floor, and the provider independently surfaces an incompatible running bundle.
  • Prior P4, P6, P7, P13, P16, P18, and P19 are fixed: floor resolution stays within a major, covers shared and diagnostic contracts, isolates platform runtime dependencies, honors replacement baselines, and preserves v2 attestations while emitting v3.
  • Prior P5, P8, and P11 are fixed: bundle verification selects the exact platform-suffixed version record and validates its structured floor, source, and artifact fields rather than searching unrelated rows or text.
  • Prior P9 and P12 are fixed: newest-native has direct regression coverage and provenance resolves the newest attested native release across major versions.
  • Prior P14 is fixed: production promotion assigns the verified bundle and disables rollout in one Capgo mutation, then reads back the exclusive channel state.
  • Prior P2, P10, and P17 are fixed: the runbook documents automatic main-triggered OTA publication, manual native lanes, platform records, and direct stable promotion with rollout disabled.
  • Prior P21 and P22 are fixed: the workflow rechecks the live main ref after the build and immediately before each platform mutation, aborting before the first or second promotion when main advances.
  • Prior P23 is fixed: the stale-main tests now execute the extracted promotion shell and assert that no Capgo mutation occurs before a stale first promotion and only one occurs when main advances between platforms.
  • Focused exact-head validation passed: 9 Jest suites and 278 tests, including release branch guards, Capgo release guards, platform floors, fingerprint schemas, replacement attestations, release versions, and updater behavior.
  • Required exact-head CI is green; git diff whitespace validation, shell syntax, and actionlint are clean. Optional visual capture jobs were still running after the aggregate ci-success gate completed.
  • The live Lexicon contains no per-account cross-chain-disable definition, but the sibling API contract explicitly emits XCHAIN_WITHDRAW_DISABLED with the same account-specific behavior and copy mapped by the UI.
  • The real iOS floor resolver currently rejects unreleased native drift at both the supplied base and head; this fail-closed state is pre-existing and unchanged by the PR.
  • Correctness, security, adversarial, and slop passes found no additional actionable secret-exposure, injection, promotion-order, compatibility, concurrency, dead-code, or architecture-drift issue in the changed behavior.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 4854a2b7ae74 · Context: repo, product, api · Took 15m

@innolope-dev
innolope-dev merged commit 84275a2 into dev Sep 17, 2026
31 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant