Files
shorebird-updater/shorebird_code_push
Eric Seidel 96fd32796e feat: stage 2 integration tests — fake patch server + golden path + rollback regression (#355)
Lands the fake HTTP server, the patch fixture pipeline, and the first
two scenarios that exercise the real FFI path end-to-end. Plus a test
that would have caught the patch-to-release rollback bug
(shorebirdtech/shorebird#3728).

Architecture follows the principles surfaced in review:

- Dart tests call the public `ShorebirdUpdater` API only — never the
  raw `Updater` FFI wrapper, never the engine API.
- Engine API stays inside the `library_test_hooks` Rust crate.
  `shorebird_test_init` constructs `AppParameters` + stub
  `FileCallbacks` internally so Dart never sees those types.
  `shorebird_test_simulate_successful_launch` wraps the
  start/success protocol so the Dart layer never knows there's a
  protocol — it just knows "the engine reported a successful boot."
- ffigen scans only the test_hooks header. The engine header
  (updater_engine.h) does not appear in the Dart bindings.
- A `TestEngine` Dart helper concentrates engine-side simulation in
  one place; the test bodies stay focused on `ShorebirdUpdater`.

Implementation choices worth flagging:

- FFI calls run via `Isolate.run`. Synchronous run blocks the main
  isolate, deadlocking against the in-isolate shelf server. The
  test's IsolateRun callback re-opens the cdylib (cheap: dlopen is
  ref-counted) and resets `Updater.bindings` in the sub-isolate
  because Dart isolates do not share static fields.
- `libapp_path` must be a real file on the desktop integration build:
  the non-Android non-iOS non-test `patch_base` reads it directly
  from disk. Tests that install a patch write the fixture's `base`
  bytes to `libapp.so` before init.
- Fake server kept minimal: shelf, no Range support, no auth, no
  concurrency knobs. Stage 3+ scenarios (download cutoff, hash
  mismatch loop, etc.) extend it as needed.

Three scenarios cover three reasons we wanted this suite:

1. `checkForUpdate returns upToDate when server has no patch` —
   baseline: confirms the harness boots cleanly and returns the
   expected enum.
2. `install a patch and boot from it` — golden path: check →
   update → simulateSuccessfulLaunch, then assert
   `readCurrentPatch` / `readNextPatch` / `checkForUpdate`
   transitions match the public API contract.
3. `checkForUpdate returns restartRequired after patch-to-release
   rollback` — regression for shorebirdtech/shorebird#3728. Pre-fix
   this returned `upToDate` and left no signal to prompt a restart.

Verified locally: 232 Rust unit tests + 44 Dart tests (41 existing
unit + 3 new integration) green; clippy/fmt/cspell clean.
2026-05-06 08:11:47 -07:00
..
2023-06-20 19:55:08 -04:00

Shorebird logo

Code Push

Discord

ci codecov License: MIT

Instantly push updates to your Flutter app without lengthy app store review cycles.

WebsiteDocsXYouTube

This Dart package communicates with the Shorebird Code Push Updater to:

  • Get the currently installed patch version
  • Check whether a new patch is available
  • Download new patches

Demo

Explore this interactive demo to learn more

Getting Started

If your Flutter app does not already use Shorebird, follow our Getting Started Guide to add code push to your app.

Installation

flutter pub add shorebird_code_push

Usage

Shorebird automatically checks for and downloads updates in the background. Most apps do not need this package. This package is for apps that want additional control, such as displaying update status to the user or prompting before downloading.

Important: checkForUpdate() and update() make network calls that may be slow. Avoid gating app startup on the result (e.g. awaiting in initState), as the app may appear stuck on the splash screen. Use .then() instead.

import 'package:shorebird_code_push/shorebird_code_push.dart';

void main() => runApp(const MyApp());

// [Other code here]

class _MyHomePageState extends State<MyHomePage> {
  final updater = ShorebirdUpdater();
  Patch? _currentPatch;
  bool _updateAvailable = false;

  @override
  void initState() {
    super.initState();

    // Read the current patch number (null if no patch is installed).
    updater.readCurrentPatch().then((patch) {
      setState(() => _currentPatch = patch);
    });

    // Check if an update is available to show in the UI.
    updater.checkForUpdate().then((status) {
      setState(() => _updateAvailable = status == UpdateStatus.outdated);
    });
  }

  // [Other code here]
}

See the example for a complete working app.

Tracks

Shorebird supports publishing patches to different tracks, which can be used to target different segments of your user base. See the percentage based rollout guide for implementation details.

You must first publish a patch to a specific track (patches are published to the stable track by default). To publish a patch to a different track, update your patch command to use the --track argument:

shorebird patch android --track beta

(We're just using Android for this example. Tracks are supported on all platforms).

To check for updates on a given track, pass an UpdateTrack to checkForUpdate (and update if you use it):

updater.checkForUpdate(track: UpdateTrack.beta);

You can also use custom track names. When creating a patch, specify a track name like this:

shorebird patch android --track my-custom-track

And:

updater.checkForUpdate(track: UpdateTrack('my-custom-track'));

Note: Updating to a specific track does not uninstall patches from other tracks. See #3484 for details.

Join us on Discord!

We have an active Discord server where you can ask questions and get help.

Contributing

See CONTRIBUTING.md.