Compare commits

...

6 Commits

Author SHA1 Message Date
Tony 479de106d7 Merge remote-tracking branch 'upstream/main'
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🦀 Build ${{ matrix.crate }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / shorebird_code_push (push) Has been cancelled
Shorebird CI / shorebird_code_push_example (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
2026-08-03 01:57:04 +08:00
dependabot[bot] 1f85c4ab1e chore(deps): update mockall requirement (#365)
Updates the requirements on [mockall](https://github.com/asomers/mockall) to permit the latest version.

Updates `mockall` to 0.15.0
- [Changelog](https://github.com/asomers/mockall/blob/master/CHANGELOG.md)
- [Commits](https://github.com/asomers/mockall/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: mockall
  dependency-version: 0.15.0
  dependency-type: direct:production
  dependency-group: library-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 18:30:09 -07:00
Mac dd213f923c fix: populate current_patch_number from running_patch, not the boot breadcrumb (#363)
* fix: send running_patch as current_patch_number on patch check

Both patch-check construction sites read current_patch_number from
state.currently_booting_patch(), a transient boot-handshake breadcrumb
that record_boot_success() clears to None before any Dart-initiated
patch check fires. The field was therefore omitted from the wire for
essentially the entire fleet.

Use state.running_patch() instead: the session-scoped accessor set at
report_launch_start, kept for the whole session, surviving server-driven
rollbacks. This is the same value the FFI symbol
shorebird_current_boot_patch_number already exposes.

* test: assert patch-check request carries running_patch as current_patch_number

Covers both construction sites (check_for_downloadable_update and update).
Reproduces the steady state where the bug manifested — patch booted,
currently_booting_patch cleared by record_boot_success — and asserts the
request still carries the running patch number. Verified to fail (field
omitted, captured as -1) against the pre-fix currently_booting_patch() code.

* test: cover all changed lines in patch-check regression test

Codecov's patch gate flagged 13 uncovered lines in the new test (78.7%
patch coverage), none of them the fix: the no-op download/report
closures never fired, and multi-line assert failure messages are only
reached when an assertion panics.

Swap the closures for the existing UNEXPECTED_DOWNLOAD/UNEXPECTED_REPORT
placeholders (also asserting that path is never hit), collapse the
assertions to single lines with the sentinel legend moved to comments,
and tighten the module doc. Changed lines now at 100% patch coverage;
fmt, clippy, and the full suite stay green.
2026-06-29 18:27:15 -06:00
Tony 6e9aab2ce7 Point updater docs at GitHub Flutter fork
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🦀 Build ${{ matrix.crate }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / shorebird_code_push (push) Has been cancelled
Shorebird CI / shorebird_code_push_example (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
2026-06-26 01:40:25 +08:00
Tony 5d4e9c3396 refactor: improve comments for clarity and update privacy references
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🦀 Build ${{ matrix.crate }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / shorebird_code_push (push) Has been cancelled
Shorebird CI / shorebird_code_push_example (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-06-25 15:50:24 +08:00
dependabot[bot] 83df31b60e chore(deps): bump the gh-deps group across 5 directories with 2 updates (#362)
Bumps the gh-deps group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [codecov/codecov-action](https://github.com/codecov/codecov-action).
Bumps the gh-deps group with 1 update in the /.github/actions/dart_package directory: [codecov/codecov-action](https://github.com/codecov/codecov-action).
Bumps the gh-deps group with 1 update in the /.github/actions/flutter_package directory: [codecov/codecov-action](https://github.com/codecov/codecov-action).
Bumps the gh-deps group with 1 update in the /.github/actions/publish_flutter_package directory: [actions/checkout](https://github.com/actions/checkout).
Bumps the gh-deps group with 1 update in the /.github/actions/rust_crate directory: [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gh-deps
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gh-deps
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gh-deps
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gh-deps
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gh-deps
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gh-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 15:45:05 -07:00
19 changed files with 161 additions and 38 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ runs:
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
- name: Upload Coverage
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
flags: ${{ steps.split.outputs.package_name }}
token: ${{ inputs.codecov_token }}
+1 -1
View File
@@ -82,7 +82,7 @@ runs:
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
- name: Upload Coverage
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
# We use Codecov's carryforward flags to allow our PR testing to only
# run affected packages, but also allow our coverage information from
@@ -10,7 +10,7 @@ runs:
using: "composite"
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 🐦 Setup Flutter
uses: subosito/flutter-action@v2
+1 -1
View File
@@ -41,7 +41,7 @@ runs:
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
- name: Upload Coverage
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
# We use Codecov's carryforward flags to allow our PR testing to only
# run affected packages, but also allow our coverage information from
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Setup Flutter
@@ -64,7 +64,7 @@ jobs:
working-directory: ${{ inputs.package_path }}
run: flutter test --coverage
- if: inputs.has_unit_tests
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
flags: ${{ inputs.package_name }}
working-directory: ${{ inputs.package_path }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
name: 🦀 Upload Artifacts
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: taiki-e/upload-rust-binary-action@v1
with:
bin: "patch"
+3 -3
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- uses: dorny/paths-filter@v4
name: Build Detection
@@ -82,7 +82,7 @@ jobs:
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 🦀 Build ${{ matrix.crate }}
uses: ./.github/actions/rust_crate
@@ -104,7 +104,7 @@ jobs:
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
shorebird_code_push: ${{ steps.filter.outputs.shorebird_code_push }}
shorebird_code_push_example: ${{ steps.filter.outputs.shorebird_code_push_example }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
# Full history so dorny/paths-filter can diff on push events.
fetch-depth: 0
@@ -65,7 +65,7 @@ jobs:
name: CSpell
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: streetsidesoftware/cspell-action@v8
+1 -1
View File
@@ -73,7 +73,7 @@ oslog = "0.2.0"
simple_logger = "5.0.0"
[dev-dependencies]
mockall = "0.14.0"
mockall = "0.15.0"
mockito = "1.2.0"
mock_instant = "0.6.0"
# Gives #[serial] attribute for locking all of our shorebird_init
+1 -2
View File
@@ -89,8 +89,7 @@ bool shorebird_check_for_downloadable_update(const char *c_channel);
* to their own stable account/device identifier. The updater persists this
* value in state.json after a successful call.
*/
SHOREBIRD_EXPORT
bool shorebird_set_device_id_override(const char *c_device_id);
SHOREBIRD_EXPORT bool shorebird_set_device_id_override(const char *c_device_id);
/**
* Synchronously download an update on the first non-null channel of:
+3 -2
View File
@@ -169,7 +169,8 @@ pub(crate) fn open_base_lib(apks_dir: &Path, lib_name: &str) -> anyhow::Result<B
// https://chromium.googlesource.com/chromium/src/base/+/a5ca5def0453df367b9c42e9817a33d2a21e75fe/android/java/src/org/chromium/base/library_loader/Linker.java
// Previously I tried reading libapp.so from from the AssetManager, but
// it does show the lib/ directory in the list of assets.
// https://github.com/shorebirdtech/updater/pull/6
// Historical updater context: this avoids reading libapp.so via
// AssetManager.
// Ideally we would do this apk reading from the C++ side and keep the rust
// portable, but we have a zip library here, and don't on the C++ side.
@@ -245,7 +246,7 @@ pub fn libapp_path_from_settings(original_libapp_paths: &[String]) -> Result<Pat
// path to the libapp.so file. This is true for the current engine, but
// may not be true in the future. Better would be for the engine to
// pass us the path to the base.apk.
// https://github.com/shorebirdtech/shorebird/issues/283
// This is fragile because Flutter passes multiple libapp paths here.
// This is where the paths are set today:
// First path is "libapp.so" (for dlopen), second is a full path:
// https://github.com/flutter/engine/blob/a7c9cc58a71c5850be0215ab1997db92cc5e8d3e/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java#L264
+1 -1
View File
@@ -15,7 +15,7 @@
// buckets. They are not `extern "C"`, so cbindgen never emits them.
//
// Engine-side usage lives at `engine/src/flutter/shell/common/shorebird/updater.cc`
// in the Shorebird Flutter monorepo: <https://github.com/shorebirdtech/flutter>.
// in the open Flutter fork: <https://github.com/tony-cloud/flutter>.
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
+1 -1
View File
@@ -72,7 +72,7 @@ pub struct UpdaterState {
struct SerializedState {
/// Stable per-install ID. Survives release-version changes; only
/// reset when the app is uninstalled. Used for analytics.
/// <https://shorebird.dev/privacy/>
/// Treat this as privacy-sensitive update-server metadata.
client_id: String,
/// The release version this cache corresponds to. Mismatch with the
/// app's reported release version triggers a wipe of all per-release
+24 -6
View File
@@ -13,12 +13,7 @@ use std::sync::Mutex;
// cbindgen looks for const, ignore these so it doesn't warn about them.
/// cbindgen:ignore
#[cfg(test)]
const DEFAULT_BASE_URL: &str = "DEFAULT_BASE_URL should be mocked using mockito::Server";
/// cbindgen:ignore
#[cfg(not(test))]
const DEFAULT_BASE_URL: &str = "https://api.shorebird.dev";
const DEFAULT_BASE_URL: &str = "http://localhost:8080";
/// cbindgen:ignore
const DEFAULT_CHANNEL: &str = "stable";
@@ -279,6 +274,29 @@ mod tests {
Ok(())
}
// These tests are serial because they modify global state.
#[serial]
#[test]
fn set_config_defaults_to_open_self_hosted_server() -> Result<()> {
testing_reset_config();
let mut yaml = fake_yaml();
yaml.base_url = None;
set_config(
fake_app_config(),
Box::new(FakeExternalFileProvider {}),
"first_path".into(),
&yaml,
NetworkHooks::default(),
)?;
let config = super::with_config(|config| Ok(config.clone())).unwrap();
assert_eq!(config.base_url, super::DEFAULT_BASE_URL);
Ok(())
}
// These tests are serial because they modify global state.
#[serial]
#[test]
+2 -3
View File
@@ -46,9 +46,8 @@ impl<'de> Deserialize<'de> for EventType {
}
}
}
/// Any edits to this struct should be made carefully and in accordance
/// with our privacy policy:
/// <https://docs.shorebird.dev/privacy>
/// Any edits to this struct should be made carefully and in accordance with
/// the configured update server's privacy policy.
/// An event that is sent to the server when a patch is successfully installed.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PatchEvent {
+3 -5
View File
@@ -219,17 +219,15 @@ pub struct Patch {
pub hash_signature: Option<String>,
}
/// Any edits to this struct should be made carefully and in accordance
/// with our privacy policy:
/// <https://docs.shorebird.dev/privacy>
/// Any edits to this struct should be made carefully and in accordance with
/// the configured update server's privacy policy.
/// The request body for the patch check endpoint.
#[derive(Debug, Serialize)]
pub struct PatchCheckRequest {
/// The Shorebird app_id built into the shorebird.yaml in the app.
/// app_ids are unique to each app and are used to identify the app
/// within Shorebird's system (similar to a bundle identifier). They
/// within the update server (similar to a bundle identifier). They
/// are not secret and are safe to share publicly.
/// <https://docs.shorebird.dev/concepts>
pub app_id: String,
/// The Shorebird channel built into the shorebird.yaml in the app.
/// This is not currently used, but intended for future use to allow
+111 -3
View File
@@ -289,7 +289,7 @@ pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result<bo
let (client_id, current_patch_number) = with_state(|state| {
Ok((
state.client_id().to_string(),
state.currently_booting_patch().map(|p| p.number),
state.running_patch().map(|p| p.number),
))
})?;
@@ -338,7 +338,8 @@ fn check_hash(path: &Path, expected_string: &str) -> anyhow::Result<()> {
// This is a common error for developers. We could avoid it entirely
// by sending the hash of `libapp.so` to the server and having the
// server only send updates when the hash matches.
// https://github.com/shorebirdtech/updater/issues/56
// A hash mismatch usually means the same version number was reused with a
// different app binary.
if !hash_matches {
bail!(
"Update rejected: hash mismatch. Update was downloaded but \
@@ -424,7 +425,7 @@ fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Resul
Ok(PatchCheckRequest::new(
&config,
&state.client_id(),
state.currently_booting_patch().map(|p| p.number),
state.running_patch().map(|p| p.number),
))
})?;
@@ -4086,3 +4087,110 @@ mod multi_engine_tests {
Ok(())
}
}
/// Regression tests asserting the patch-check request's `current_patch_number`
/// carries the running patch, not the boot breadcrumb (`currently_booting_patch`)
/// which `report_launch_success` clears before any patch check fires. Covers
/// both construction sites: `check_for_downloadable_update` and `update`.
#[cfg(test)]
mod patch_check_current_patch_number_tests {
use anyhow::Result;
use serial_test::serial;
use std::sync::atomic::{AtomicI64, Ordering};
use tempfile::TempDir;
use crate::{
check_for_downloadable_update,
network::{
testing_set_network_hooks, PatchCheckResponse, UNEXPECTED_DOWNLOAD, UNEXPECTED_REPORT,
},
report_launch_start, report_launch_success,
test_utils::install_fake_patch,
update,
updater::tests::init_for_testing,
with_state,
};
/// `current_patch_number` was `None` on the wire (the bug we're guarding).
const FIELD_OMITTED: i64 = -1;
/// The patch-check hook never ran.
const HOOK_NOT_CALLED: i64 = i64::MIN;
/// Last `current_patch_number` seen by the patch-check hook. Shared by
/// both tests; safe because they are `#[serial]` (only one runs at a
/// time) and `arrange_capturing_hooks` resets it before each check.
static CAPTURED: AtomicI64 = AtomicI64::new(HOOK_NOT_CALLED);
/// Installs patch 1 and completes a full boot, then asserts the steady
/// state in which patch checks actually run: the boot breadcrumb is
/// cleared, but the process is still running patch 1. Reading
/// `currently_booting_patch` here yields `None` — that is precisely the
/// regression this guards against.
fn boot_patch_one(tmp_dir: &TempDir) -> Result<()> {
init_for_testing(tmp_dir, None);
install_fake_patch(1)?;
report_launch_start()?;
report_launch_success()?;
with_state(|state| {
// Steady state: the boot breadcrumb is cleared, yet patch 1 is
// still the running patch.
assert!(state.currently_booting_patch().is_none());
assert_eq!(state.running_patch().map(|p| p.number), Some(1));
Ok(())
})
}
/// Installs network hooks whose patch-check leg records the request's
/// `current_patch_number` into `CAPTURED` (`None` -> `FIELD_OMITTED`) and
/// reports no available update.
fn arrange_capturing_hooks() {
CAPTURED.store(HOOK_NOT_CALLED, Ordering::SeqCst);
testing_set_network_hooks(
|_url, request| {
CAPTURED.store(
request
.current_patch_number
.map(|n| n as i64)
.unwrap_or(FIELD_OMITTED),
Ordering::SeqCst,
);
Ok(PatchCheckResponse {
patch_available: false,
patch: None,
rolled_back_patch_numbers: None,
})
},
// No update is offered, so neither hook should ever fire.
UNEXPECTED_DOWNLOAD,
UNEXPECTED_REPORT,
);
}
#[serial]
#[test]
fn check_for_downloadable_update_sends_running_patch() -> Result<()> {
let tmp_dir = TempDir::new().unwrap();
boot_patch_one(&tmp_dir)?;
arrange_capturing_hooks();
check_for_downloadable_update(None)?;
// 1 = running patch sent; -1 = field omitted (the bug); i64::MIN = hook never ran.
assert_eq!(CAPTURED.load(Ordering::SeqCst), 1);
Ok(())
}
#[serial]
#[test]
fn update_sends_running_patch() -> Result<()> {
let tmp_dir = TempDir::new().unwrap();
boot_patch_one(&tmp_dir)?;
arrange_capturing_hooks();
update(None)?;
// 1 = running patch sent; -1 = field omitted (the bug); i64::MIN = hook never ran.
assert_eq!(CAPTURED.load(Ordering::SeqCst), 1);
Ok(())
}
}
+1 -1
View File
@@ -23,4 +23,4 @@ For an Updater function to be visible to the Dart code, it must:
`dart run ffigen`. ffigen reads only `updater_dart.h`, so engine-only
symbols are not bound.
1. Android specific: be listed in
https://github.com/shorebirdtech/flutter/blob/shorebird/dev/engine/src/flutter/shell/platform/android/android_exports.lst
https://github.com/tony-cloud/flutter/blob/tonycloud/dev/engine/src/flutter/shell/platform/android/android_exports.lst