Skip to content

feat(cli): unified cross-platform patch numbering#3738

Open
easymac wants to merge 3 commits intomainfrom
mc/patch-unification
Open

feat(cli): unified cross-platform patch numbering#3738
easymac wants to merge 3 commits intomainfrom
mc/patch-unification

Conversation

@easymac
Copy link
Copy Markdown
Contributor

@easymac easymac commented May 4, 2026

Summary

Make POST /v1/apps/{appId}/patches idempotent on a user-supplied correlation key so cross-platform builds resolve to one patch row instead of one per platform.

Customer outcome: a developer who shipped "patch 3" sees the same number on iOS and Android — in their analytics, in-app, and in their release notes. The dominant CI flow (iOS bot publishes, Android bot publishes minutes later, no shared state between them) collapses to one patch as long as both bots pass the same --patch-id.

Protocol — shorebird_code_push_protocol

  • CreatePatchRequest gains optional clientPatchId (wire field client_patch_id). toJson always emits the key (null when unset) so server parsing doesn't have to special-case absence.
  • CreatePatchResponse (new field) echoes clientPatchId back so callers can verify whether an idempotent re-use occurred.
  • ReleasePatch carries clientPatchId so existing GET endpoints surface the correlation key for admin/console rendering.

Client — shorebird_code_push_client

  • CodePushClient.createPatch accepts and threads clientPatchId, and now returns CreatePatchResponse instead of Patch so callers can read the echoed key.
  • Empty-string clientPatchId is normalized to null at the client boundary — a caller that passes an unexpanded template variable or empty flag shouldn't land on the idempotent path keyed on '' and inherit a stranger's patch. Done at the client rather than the protocol so the protocol layer remains a faithful round-trip.

CLI — shorebird_cli

  • New --patch-id=<string> flag on shorebird patch. Common usage: --patch-id=${{ github.sha }} or any stable token (hotfix-login, a CI run ID, etc.).
  • Multi-platform invocations (--platforms=ios,android) auto-generate a UUID and share it across every per-platform fan-out — single-invocation is just cross-invocation-with-an-internal-ID.
  • Append-after-promotion confirmation: when an idempotent hit lands on a patch already promoted to stable, the new platform's artifacts go live to that platform's stable users immediately. In interactive mode the CLI prompts before continuing; in CI it proceeds silently (the iOS-publishes-then-Android-completes flow is the canonical use case and prompting would deadlock).
  • Suppresses the prompt when the same run promoted the patch (e.g. the first platform of a single --platforms ios,android invocation — the user already opted in by passing both platforms together).
  • Success line: ✅ Published Patch 3 (iOS, Android) instead of one log per platform; degenerate cases where platforms land on different numbers each get their own line.

Backwards compatibility

  • Missing client_patch_id deserializes to null. Old clients that don't set the field fall through the existing per-call create path on the server.
  • Old servers that don't recognize the field ignore it on deserialization; the CLI flag becomes a no-op until the server side ships, with no error surface.

This is the protocol + CLI half of unified patch numbering. The server-side migration and idempotent createPatch live in the private parent repo and are gated behind the same client_patch_id correlation key.

Test plan

  • dart test packages/shorebird_code_push_protocol
  • dart test packages/shorebird_code_push_client
  • dart test packages/shorebird_cli
  • Manual: shorebird patch android --patch-id=test-sha against a server with the matching server-side change deployed
  • Manual: shorebird patch --platforms=ios,android end-to-end (single invocation; auto-generated UUID)
  • Manual: simulate cross-invocation by running two shorebird patch calls minutes apart with the same --patch-id
  • Manual: append-after-promotion in interactive TTY (expect prompt) and in CI/non-TTY (expect silent proceed)

Make `POST /v1/apps/{appId}/patches` idempotent on a user-supplied
correlation key so cross-platform builds resolve to one patch row
instead of one per platform.

Customer outcome: a developer who shipped "patch 3" sees the same
number on iOS and Android — in their analytics, in-app, and in their
release notes. The dominant CI flow (iOS bot publishes, Android bot
publishes minutes later, no shared state between them) collapses to
one patch as long as both bots pass the same `--patch-id`.

## Protocol — shorebird_code_push_protocol

- `CreatePatchRequest` gains optional `clientPatchId` (wire field
  `client_patch_id`). `toJson` always emits the key (null when unset)
  so server parsing doesn't have to special-case absence.
- `CreatePatchResponse` (new field) echoes `clientPatchId` back so
  callers can verify whether an idempotent re-use occurred.
- `ReleasePatch` carries `clientPatchId` so existing GET endpoints
  surface the correlation key for admin/console rendering.

## Client — shorebird_code_push_client

- `CodePushClient.createPatch` accepts and threads `clientPatchId`,
  and now returns `CreatePatchResponse` instead of `Patch` so callers
  can read the echoed key.
- Empty-string `clientPatchId` is normalized to null at the client
  boundary — a caller that passes an unexpanded template variable or
  empty flag shouldn't land on the idempotent path keyed on `''` and
  inherit a stranger's patch. Done at the client rather than the
  protocol so the protocol layer remains a faithful round-trip.

## CLI — shorebird_cli

- New `--patch-id=<string>` flag on `shorebird patch`. Common usage:
  `--patch-id=\${{ github.sha }}` or any stable token
  (`hotfix-login`, a CI run ID, etc.).
- Multi-platform invocations (`--platforms=ios,android`) auto-generate
  a UUID and share it across every per-platform fan-out — single-
  invocation is just cross-invocation-with-an-internal-ID.
- Append-after-promotion confirmation: when an idempotent hit lands
  on a patch already promoted to stable, the new platform's artifacts
  go live to that platform's stable users immediately. In interactive
  mode the CLI prompts before continuing; in CI it proceeds silently
  (the iOS-publishes-then-Android-completes flow is the canonical
  use case and prompting would deadlock).
- Suppresses the prompt when the same run promoted the patch (e.g.
  the first platform of a single `--platforms ios,android` invocation
  — the user already opted in by passing both platforms together).
- Success line: `✅ Published Patch 3 (iOS, Android)` instead of one
  log per platform; degenerate cases where platforms land on
  different numbers each get their own line.

## Backwards compatibility

- Missing `client_patch_id` deserializes to null. Old clients that
  don't set the field fall through the existing per-call create path
  on the server.
- Old servers that don't recognize the field ignore it on
  deserialization; the CLI flag becomes a no-op until the server
  side ships, with no error surface.

This is the protocol + CLI half of unified patch numbering. The
server-side migration and idempotent createPatch live in the parent
repo and are gated behind the same `client_patch_id` correlation key.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

easymac added 2 commits May 4, 2026 16:03
CI was failing on `dart format --set-exit-if-changed`. The cascade
literals in the unified-success log tests exceeded 80 columns and
need to be split across multiple lines.
@easymac easymac marked this pull request as ready for review May 5, 2026 17:41
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