CLI

sankofa patch

Ship a code-only OTA update on top of a base release — no store resubmission. Covers the Flutter auto-diff workflow (edit real Dart, ship only changed functions), the React Native JS-bundle path, and every flag.

sankofa patch ships a code-only update against an existing base release. No native rebuild, no App Store / Play Store resubmission — devices in the rollout fetch and apply the patch on next launch. Both iOS and Android are App Store + Play Store compliant.

bash
sankofa patch [platform] [options]

platform is ios or android. Omit it and the CLI prompts. As with release, the stack is auto-detected and the matching pipeline runs.

Flutter: the auto-diff workflow (default)

The Flutter path is the headline feature: you edit your real Dart code, and sankofa patch ships only the functions that changed. There is no patch file to write and no special entry point to maintain.

  1. Edit your real code

    Change a function body in your lib/ — fix a bug, tweak a string, adjust logic. Just edit the app the way you normally would.

  2. Run sankofa patch

    The CLI rebuilds your edited app, diffs it against the baseline captured by sankofa release, and lifts exactly the changed functions into a tiny patch module.

  3. Publish

    The module is signed (if you have a signing key), uploaded, and labelled <base>-patch.N. Devices in the rollout apply it on next launch via the Sankofa updater.

bash
# 1. …edit your Dart code…

# 2. Ship it — Sankofa diffs your edits and ships only what changed
sankofa patch android
sankofa patch ios                # App Store compliant

# Non-interactive with a staged rollout + note
sankofa patch android --publish --rollout 25 --description "fix crash on Pixel 6"

If nothing changed vs. the baseline, the CLI tells you there is nothing to ship and exits without publishing.

What you can and cannot patch

A patch carries Dart code changes only. The safety check refuses anything that changes AndroidManifest.xml, native assets, or adds new native bindings — those require a full sankofa release and a store submission. Practically:

  • Yes: changed logic and function bodies in your Dart code, bug fixes, copy tweaks, behavior changes reachable through your existing code.
  • No: new native plugins, changed Info.plist / AndroidManifest.xml, new bundled assets, engine upgrades, or a different app version. Cut a new release for those.

Engine must match the baseline

A patch built against engine X can only be applied to a baseline built against engine X, or the device cannot load it. The CLI cross-checks this before building and stops with an explicit engine mismatch message if your local engine differs from the baseline's. If you intentionally upgraded, cut a new release with sankofa engine upgrade first.

Selecting the baseline

With one baseline release the CLI picks it automatically. With several it shows an interactive picker. For headless / CI runs, name the baseline explicitly:

bash
# Pick the base release by label (or id) — required for CI / non-interactive runs
sankofa patch android --release v1.4.0 --publish

# Scope to the flavor the base release was built with
sankofa patch android --release v1.4.0 --flavor production --publish

Patches inherit the baseline's target_binary_version (so device targeting lines up) and are labelled <base>-patch.N, where N auto-increments.

Flavored apps

bash
sankofa patch android --flavor production -t lib/main_production.dart
sankofa patch ios      --flavor staging    -t lib/main_staging.dart

--flavor scopes which base release the patch targets (mirroring sankofa release --flavor); -t/--target is the entry point the rebuild + diff uses.

Legacy patch-file model

The default auto-diff path replaces the older model where you hand-wrote a single entry-point file. You can still opt into it:

bash
# Compile a single hand-written @pragma('dyn-module:entry-point') file
sankofa patch android --legacy-patch-file                       # default lib/sankofa_patch.dart
sankofa patch android --legacy-patch-file lib/my_patch.dart

This is limited compared to auto-diffing your real code — prefer the default unless you have a specific reason.

React Native: JS-bundle patch

React Native patches bundle your JavaScript + assets and ship them against a chosen base release. The CLI shows a picker of base releases, asks whether the change includes native code (which can't go OTA), then bundles and uploads.

bash
sankofa patch ios --publish --mandatory
sankofa patch android --rollout 50
sankofa patch ios --release v1.2.0 --publish   # skip the picker in CI

Full flag reference

Shared

--publishflag
Publish without the confirmation prompt. Required for CI.
--env <environment>live | test
Target environment. Prompts if omitted.
--rollout <percent>0–100default 100
Rollout percentage for this patch.
--mandatoryflag
Force devices to apply on next launch.
--description <desc>string
Patch note stored with the release row.
--release <labelOrId>string
Baseline release to patch. Skips the interactive picker — required for headless / CI patching.
--project <path>path
Project root. Defaults to auto-detect.
--dry-runflag
Build + run the safety check locally, but do NOT contact the server or upload.

Flutter

-t, --target <file>pathdefault lib/main.dart
App entry-point to rebuild + diff. Pass your flavored main (e.g. lib/main_prod.dart). You just edit real code; Sankofa diffs the rebuild and ships only changed functions.
--entry-file <file>path
Alias of -t/--target for the auto-diff rebuild. (For React Native, this is the JS entry file.)
--legacy-patch-file [file]pathdefault lib/sankofa_patch.dart
Opt into the legacy single-file model instead of auto-diffing real code. Compiles one hand-written @pragma('dyn-module:entry-point') file.
--flavor <name>string
Product flavor the base release was built with. Scopes which base release the patch targets.
--no-rebuildflag
Skip the app rebuild and reuse the last build under --output-dir. Advanced; only when you just built.
--output-dir <dir>pathdefault ./build
Directory for built artifacts.
--dart-define <KEY=VALUE>repeatable
dart-define passed to the rebuild. Repeat for multiple.
--engine-version <version>string
Override the detected engine version (rare).
--target-binary-version <semver>string
Host app version for a standalone base patch. Defaults to the baseline release's version.
--label <label>string
Standalone base-patch label override — used only when no baseline release exists.
--dynamic-interface <yaml>pathdefault ./sankofa/dynamic_interface.yaml
Dynamic-interface YAML path, used when present.
--engine-commit <sha>string
Sankofa Flutter engine commit baked into patch metadata.
--dart-version <semver>string
Dart SDK version baked into patch metadata.

Signing

If a project signing key exists on disk (created by sankofa keys generate), every patch is signed with it automatically and the device verifies the signature before applying. Without a key, patches are unsigned but still fully functional — generate a key when you want end-to-end signature verification.

After publishing

The command prints the patch ID, label (<base>-patch.N), runtime, engine, target version, rollout %, and whether it is mandatory. Devices in the rollout fetch and apply it on next launch through the Sankofa updater's pre-flight step at startup. Track adoption with sankofa status.

  • sankofa release — cut the base release a patch attaches to.
  • sankofa engine — inspect / match / upgrade the engine a patch is built against.
  • Troubleshooting — engine mismatch, no_matching_release, and the SANKOFA_SKIP_ENGINE_CHECK escape hatch.
Edit this page on GitHub