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

This commit is contained in:
Tony
2026-08-03 01:57:04 +08:00
11 changed files with 123 additions and 16 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ runs:
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
- name: Upload Coverage - name: Upload Coverage
uses: codecov/codecov-action@v6 uses: codecov/codecov-action@v7
with: with:
flags: ${{ steps.split.outputs.package_name }} flags: ${{ steps.split.outputs.package_name }}
token: ${{ inputs.codecov_token }} token: ${{ inputs.codecov_token }}
+1 -1
View File
@@ -82,7 +82,7 @@ runs:
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
- name: Upload Coverage - name: Upload Coverage
uses: codecov/codecov-action@v6 uses: codecov/codecov-action@v7
with: with:
# We use Codecov's carryforward flags to allow our PR testing to only # We use Codecov's carryforward flags to allow our PR testing to only
# run affected packages, but also allow our coverage information from # run affected packages, but also allow our coverage information from
@@ -10,7 +10,7 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: 📚 Git Checkout - name: 📚 Git Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: 🐦 Setup Flutter - name: 🐦 Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
+1 -1
View File
@@ -41,7 +41,7 @@ runs:
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
- name: Upload Coverage - name: Upload Coverage
uses: codecov/codecov-action@v6 uses: codecov/codecov-action@v7
with: with:
# We use Codecov's carryforward flags to allow our PR testing to only # We use Codecov's carryforward flags to allow our PR testing to only
# run affected packages, but also allow our coverage information from # run affected packages, but also allow our coverage information from
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
- name: Setup Flutter - name: Setup Flutter
@@ -64,7 +64,7 @@ jobs:
working-directory: ${{ inputs.package_path }} working-directory: ${{ inputs.package_path }}
run: flutter test --coverage run: flutter test --coverage
- if: inputs.has_unit_tests - if: inputs.has_unit_tests
uses: codecov/codecov-action@v6 uses: codecov/codecov-action@v7
with: with:
flags: ${{ inputs.package_name }} flags: ${{ inputs.package_name }}
working-directory: ${{ inputs.package_path }} working-directory: ${{ inputs.package_path }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
name: 🦀 Upload Artifacts name: 🦀 Upload Artifacts
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: taiki-e/upload-rust-binary-action@v1 - uses: taiki-e/upload-rust-binary-action@v1
with: with:
bin: "patch" bin: "patch"
+3 -3
View File
@@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: 📚 Git Checkout - name: 📚 Git Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- uses: dorny/paths-filter@v4 - uses: dorny/paths-filter@v4
name: Build Detection name: Build Detection
@@ -82,7 +82,7 @@ jobs:
steps: steps:
- name: 📚 Git Checkout - name: 📚 Git Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: 🦀 Build ${{ matrix.crate }} - name: 🦀 Build ${{ matrix.crate }}
uses: ./.github/actions/rust_crate uses: ./.github/actions/rust_crate
@@ -104,7 +104,7 @@ jobs:
steps: steps:
- name: 📚 Git Checkout - name: 📚 Git Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 📚 Git Checkout - name: 📚 Git Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
shorebird_code_push: ${{ steps.filter.outputs.shorebird_code_push }} shorebird_code_push: ${{ steps.filter.outputs.shorebird_code_push }}
shorebird_code_push_example: ${{ steps.filter.outputs.shorebird_code_push_example }} shorebird_code_push_example: ${{ steps.filter.outputs.shorebird_code_push_example }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
# Full history so dorny/paths-filter can diff on push events. # Full history so dorny/paths-filter can diff on push events.
fetch-depth: 0 fetch-depth: 0
@@ -65,7 +65,7 @@ jobs:
name: CSpell name: CSpell
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
- uses: streetsidesoftware/cspell-action@v8 - uses: streetsidesoftware/cspell-action@v8
+1 -1
View File
@@ -73,7 +73,7 @@ oslog = "0.2.0"
simple_logger = "5.0.0" simple_logger = "5.0.0"
[dev-dependencies] [dev-dependencies]
mockall = "0.14.0" mockall = "0.15.0"
mockito = "1.2.0" mockito = "1.2.0"
mock_instant = "0.6.0" mock_instant = "0.6.0"
# Gives #[serial] attribute for locking all of our shorebird_init # Gives #[serial] attribute for locking all of our shorebird_init
+109 -2
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| { let (client_id, current_patch_number) = with_state(|state| {
Ok(( Ok((
state.client_id().to_string(), state.client_id().to_string(),
state.currently_booting_patch().map(|p| p.number), state.running_patch().map(|p| p.number),
)) ))
})?; })?;
@@ -425,7 +425,7 @@ fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Resul
Ok(PatchCheckRequest::new( Ok(PatchCheckRequest::new(
&config, &config,
&state.client_id(), &state.client_id(),
state.currently_booting_patch().map(|p| p.number), state.running_patch().map(|p| p.number),
)) ))
})?; })?;
@@ -4087,3 +4087,110 @@ mod multi_engine_tests {
Ok(()) 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(())
}
}