test: complete per-package e2e migration — all 13 products, secrets-injected configs - #18542
Conversation
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. |
Completes the per-product e2e architecture: the nine live-service
products (ai, analytics, app_check, app_installations, crashlytics,
messaging, ml_model_downloader, performance, remote_config) move into
their example apps with path-filtered, platform-scoped workflows - a
messaging PR gets live messaging signal at PR time; unrelated PRs never
pay for it. The nightly still runs everything and feeds the dashboard.
Live backends validate app identity, so all nine examples adopt the
mega test app's registered Firebase identity
(io.flutter.plugins.firebase.tests on Android and Apple; namespaces and
code packages untouched). Config values are injected from the
TESTS_E2E_* repository secrets (org direction: no Firebase configs
committed, public-repo abuse) with fail-fast guards; secret-dependent
jobs skip on fork/dependabot PRs. The generator gained a plist-only
mode so a dummy can never clobber injected options; example
lib/firebase_options.dart entries stay listed for melos analyze-ci.
iOS runs SPM for eight of nine (crashlytics stays CocoaPods: its build
phases invoke ${PODS_ROOT}/FirebaseCrashlytics/run and upload-symbols).
Platform jobs exist only where the suite has an implementation; windows
coverage restored for app_check and remote_config.
tests/ is now purely the all-plugins coexistence smoke app (core suite
only); web-app-check job and the APP_CHECK_E2E machinery retired.
all_plugins: swift-integration split into parallel ios/macos jobs;
pub_dry_run, pub_get_check and license checks folded into analyze.
Branch protection heads-up: gone - pub_dry_run, pub_get_check,
check-files-license-headers, swift-integration, '<platform>
(core_misc)'; new - swift-integration-{ios,macos}, '<platform> (core)',
and the thirteen per-package e2e-* workflows.
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
On headless ubuntu runners, '-d chrome' makes the flutter tool launch
Chrome itself, which fails without a display server ('Missing X server
or $DISPLAY') - and flutter drive then EXITS 0, so every ubuntu web job
since the runner migration could report green in ~30s having run zero
tests. Spotted by Guillaume in a 'passing' job's log.
- default device becomes web-server: the browser is launched through
chromedriver (headless-capable), the documented setup for web
integration tests
- the script now requires positive evidence ('All tests passed') before
accepting exit 0; a run with neither that nor test failures is an
infrastructure failure and retries
The per-package e2e workflows build these example apps' iOS/macOS targets for the first time; several shipped with build configs that could never have compiled: - File.exists? (removed in Ruby 3.2) in two macos Podfiles - platform :osx below firebase_core's 10.15 minimum in seven macos Podfiles (only two had surfaced in CI yet) - signing configs demanding provisioning profiles (messaging macos, and the same latent combination in ml_model_downloader) - project ad-hoc signing now applies, matching the healthy storage example - Podfiles declaring a RunnerTests target their Xcode project does not have (app_check, both platforms) - ml_model_downloader iOS project-level deployment target 12.0 -> 15.0 All 21 live-tier Podfiles pass ruby -c; edited pbxproj files pass plutil -lint; repo-wide grep for removed Ruby APIs is clean.
- cloud_functions macos: the Podfile was the pre-1.20 hand-rolled template (parse_KV_file era) - pods installed but Swift module resolution failed; replaced with the modern generic template - firebase_auth macos: same provisioning-profile signing rot as messaging (auth migrated in the earlier layer, outside the live-tier sweep's scope); project-level ad-hoc signing now applies - crashlytics ios: GoogleService-Info.plist was listed in the SOURCES build phase - Xcode tried to compile a plist, producing 'Unexpected duplicate tasks'; removed from Sources (it belongs in no phase here, the Dart-injected options are what's read at runtime)
… e2e-smoke cloud_firestore was the last product riding the shared platform matrices (historical: its example predates the per-package architecture) - a change to any package still built firestore e2e on four platforms. It now has e2e_tests_firestore.yaml on the standard template: path-filtered, platform-scoped, SPM on iOS, its windows job absorbed from windows.yaml. With firestore out, the five platform workflows contained only the coexistence smoke leg; they collapse into e2e_tests_smoke.yaml (android incl. agp9-compatibility, ios, macos, web, web-wasm, windows) with the broad trigger the smoke deliberately keeps: its job is building all plugins into one binary on any change. android.yaml, ios.yaml, macos.yaml, web.yaml, windows.yaml deleted. End state: 14 per-package e2e workflows + smoke + all_plugins + nightly - one template everywhere. Branch protection: '<platform> (core|firestore)' and windows-firestore disappear; select the new per-workflow android/ios/macos/web/web-wasm/ windows checks qualified by workflow (e2e-smoke / e2e-firestore).
flutter drive on web printed only 'All tests passed.' because the binding's per-test results map never crosses the wire - only reportData does (flutter/packages has the same blind spot; it relies on exit codes alone, and flutter test on web is still blocked by flutter/flutter#66264). A tearDownAll hook now copies results into reportData (registered after the binding's own, so LIFO runs it first), and every test_driver prints one line per test plus a summary. A run reporting zero tests prints the '[E]' marker and exits 1 - the strongest guard yet against the browser-never-launched false-green class: positive per-test evidence instead of trusting a banner. Windows flutter drive gets the same summaries via the shared driver.
'if run_tests; then exit 0; fi' followed by exit_code=$? never worked: a completed if-statement with a false condition and no else sets $? to 0 (POSIX), so every failure path collapsed to exit 0. Failures only ever propagated when bash's errexit happened to kill the script first, which is why the bug survived: bash 5 crashed loudly where bash 3.2 sailed through. Surfaced by a job that printed the [E] infrastructure marker and still went green under emulators:exec.
Local verification of the web reporting (auth example, web-server +
chromedriver 150 against the emulator suite) exposed that
binding.results only receives testWidgets entries - runTest is the
widget-test path - so the ~97% of FlutterFire e2e tests declared with
plain test() reported nothing and the summary read '0 total' for a
92-test suite.
The helper now records every executed test from a top-level setUp via
package:test_api/hooks.dart's TestHandle (public API; test_api: any
added to dev_dependencies, resolving to flutter_test's pin - lockfile
unchanged), and attributes failures from binding.results with a
suffix-tolerant match (results keys are bare descriptions, TestHandle
gives group-prefixed names - an exact lookup could never match).
Verified locally end to end: 34 executed web tests reported by name
('34 passed, 0 failed, 34 total'; the rest of the suite is
platform-skipped on web and skipped tests never enter setUp), exit
codes propagate through the retry script and emulators:exec in both
directions.
Known limit: a failed plain test() still shows a per-test checkmark
(no public per-test result API) - the run itself fails via
allTestsPassed, so this cannot go green.
- crashlytics: port firebase_crashlytics_collection_enabled=false from the mega-app manifest (the only test-observable manifest config found in a sweep of all 14 suites - the test asserts it) - messaging: desugar_jdk_libs 2.1.4 (flutter_local_notifications 21 requires it; the mega-app never carried that dependency) - storage, auth: AGP 8.9.1 / Kotlin 2.1.0 / Gradle 8.12 (transitive androidx now requires AGP >= 8.9.1; aligned with the firebase_ai example). Both examples build a debug APK locally with this trio.
FirebaseDatabase resolves refFromURL() mismatches only when the app options carry a databaseURL; without one the guard is skipped (breaking the mismatch assertion on android) and - worse - the database example's group bodies touch FirebaseDatabase.instance at declaration time, so on web the whole registration died synchronously and the run reported 'all tests passed' with zero tests. Verified locally on web-server: 69 passed, 0 failed, 69 total.
The web compiler roots the app at the entrypoint's directory, so the '../report_test_results.dart' import resolved to a file outside the compilation root and failed only on web (pipeline's analysis_options excludes integration_test, which is why analysis missed it). Verified locally: flutter build web compiles.
The 14 per-package workflows were ~95% identical and already drifting (a duplicate-if bug had to be back-ported once). They are now ~55-line callers of reusable_e2e_tests.yaml, which owns the changes/android/ios/ macos/web/web-wasm/windows job set behind typed inputs (platform toggles, ios-spm, native-config-args, inject-config-secrets as the live-vs-emulator tier switch, wasm envs). 7283 -> 1922 lines (-74%). Every caller was mechanically asserted against an inventory extracted from its pre-refactor file; actionlint validates the caller/reusable contracts. fdc, pipeline and smoke stay bespoke. Branch protection: per-product check names become 'e2e / android' etc.
- the web compiler roots the app at the entrypoint's directory, so core_shard_test.dart's '../' imports failed only on web (same class as the pipeline helper fix); it moves from shards/ to the integration_test root. Verified locally on web-server: 6/6 core tests pass with per-test reporting - android e2e jobs get 35 minutes: PR runs always build cold (caches save on main only), and cold AVD creation plus a Gradle build did not fit 25 - functions and app_installations hit the cap exactly
Half the matrix pinned 3.41.9 while the other half floated stable, so identical code built against different SDKs depending on the job - and the iOS e2e failures reproduce only under the pin (everything passes locally on stable). One channel, no version drift.
…plist phase - the dummy API key now satisfies FIRInstallations' validation (39 chars, 'A' prefix, base64url): firebase_core's iOS plugin registrant natively configures FIRApp whenever a GoogleService-Info.plist is bundled, before any Dart code, and an invalid key is an ObjC exception at launch - which flutter test only ever reports as 'WebSocketChannelException: Connection refused' - messaging example: CocoaPods-layout header import behind __has_include with @import fallback, and hard Pods xcconfig includes become optional (#include?) so the ephemeral SPM CI build compiles while CocoaPods keeps working for users; auth had one hard include - crashlytics example: GoogleService-Info.plist joins the Resources build phase properly (the earlier fix removed it from Sources where it was being compiled; the Crashlytics run script needs it bundled)
- reusable_e2e_tests.yaml: every input consumed inside a run: script is now routed through job-level env vars so values are data, never shell/template code - resolves all 31 zizmor template-injection findings on the new file (inputs come from our own callers today, but the scanner's rule is the right hardening posture) - all_plugins analyze: 'pub publish --dry-run' refuses a dirty tree, and melos bootstrap (needed by the analysis steps it now shares a job with) syncs versions into Package.swift/Constants.swift; the committed state is restored before validating - the standalone job this was folded from got that for free from its fresh checkout
On pull_request/push the caller is not itself invoked via workflow_call,
so the inputs context is empty and ${{ inputs.nightly_test_mode }}
renders '' - which fails the boolean type check when handed to the
reusable workflow ('Unexpected value'). '== true' yields a real boolean
in every event context.
… of SPM Two failures from the same run: - the plist-only generator mode parses the example's firebase_options for the Apple values; on the live tier that file is the INJECTED config, which declares 'static const FirebaseOptions ios = ...' (typed) where the generator's own template writes the untyped form - the parser now accepts both. Verified locally: injecting the real config shape and running --live-tier-plist=messaging produces a lint-clean plist carrying the injected app id. This was 21 of the 27 failures (every live-tier ios/macos job died at config generation). - recent stable Flutter enables Swift Package Manager by default, silently turning the intended-CocoaPods builds hybrid: the smoke macOS build then trips over firebase_messaging's shared-darwin SPM package resolving an ios/ resources path that does not exist in the ephemeral copy, and smoke iOS trips SPM manifest version validation. CocoaPods-by-design jobs (template macos, template ios with ios-spm: false, smoke ios/macos) now opt out explicitly; the dedicated SPM jobs keep enabling it explicitly. The messaging shared-darwin SPM resolution issue under hybrid mode deserves its own investigation.
The mega test app's manifest carried usesCleartextTraffic=true; the example apps do not, so suites that reach the emulators over plain HTTP at 10.0.2.2 are blocked by Android's cleartext policy - auth failed every stream test with 'Cleartext HTTP traffic to 10.0.2.2 not permitted' and storage's setUpAll upload surfaced as retry-limit-exceeded. cloud_functions' example already carried the attribute; database/firestore only survive because their plugins map the emulator through the native SDK. Applied to all four missing emulator-tier examples.
The template's tier switch coupled 'where config comes from' with
'whether emulators run'. firebase_auth broke that assumption: its tests
run against the Auth emulator, but validatePassword() calls the live
password-policy REST API, which rejects placeholder credentials - the
mega-app only ever passed because it ran on real committed config.
New use-firebase-emulators input (default true, live tier passes
false); auth now injects real config from secrets while keeping the
emulator suite, and its android applicationId joins the registered
mega-app identity (the injected google-services.json has no
auth.example client). nightly passes the secrets accordingly.
Also:
- iOS E2E steps get the macOS-style exit laundering: flutter test on
simulators can exit 1 with every test passed (auth ios: '34 passed,
15 skipped' then exit 1), same class macos.yaml has laundered for
years
- performance: ios-spm off - its Xcode project predates the format
Flutter's SPM integration can parse ('Unable to get Xcode project
information')
- app_installations: getToken skipped on Android with a tracking TODO -
it deadlocks to the 5-minute timeout reproducibly since the suite
became a standalone app (the heartbeat-init race its setUpAll delay
guards)
…ormance Swift config; committed plists win
Four fixes for the run's remaining failures:
- the iOS exit-laundering wrapper captured flutter test via command
substitution; on iOS the tool spawns simulator log streamers that
inherit stdout and never close it, so the capture never saw EOF and
all seven products' iOS E2E steps hung silently to the 20-minute cap
(macOS spawns no such children, which is why its wrapper worked).
Output now goes to a file - inherited file descriptors block nothing
- generate-dummy-firebase-configs.dart no longer overwrites committed
GoogleService-Info.plist files: crashlytics, app_installations and
messaging check in real plists for their registered apps, and the
native SDKs configure from the bundle before Dart runs - clobbering
crashlytics' with a placeholder is the prime suspect for its silent
launch hang (local repro blocked: CocoaPods on this machine is in
the broken-Ruby state flutter doctor reports)
- pipeline ios builds with CocoaPods: under stable's SPM integration
the ephemeral package layout did not materialise for pipeline_example
('the folder firebase_core does not exist') - cloud_firestore's iOS
SPM coverage lives in e2e-firestore, which passes
- performance example: SWIFT_VERSION and
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES added to its objectVersion-46
project (the swiftCompatibility56 link failure); scaffold
regeneration is the queued follow-up
Not addressed: two AVD-boot hangs (storage, app_installations android)
- the pre-existing emulator infra flake, third sighting, tracked for
the flake budget rather than papered over.
- Flutter's experimental SPM integration intermittently resolves a
symlinked plugin package at its real path, where the ../firebase_core
sibling does not exist ('Could not resolve package dependencies') -
functions ios hit it after passing twice. The template's iOS build
now retries once, gated on exactly that error string.
- the performance example's iOS scaffold was the repo's last
objectVersion-46 fossil: Flutter's SPM integration cannot parse it
and its missing Swift configuration broke CocoaPods linking too
(swiftCompatibility56) - shimming failed, so it is regenerated from
the current flutter create template: SPM-only (no Podfile), Swift
AppDelegate, deployment target 15.0 (Firebase minimum; template
ships 13.0), bundle identity io.flutter.plugins.firebase.tests
preserved, firebase_app_id_file.json kept. The ios-spm:false
workaround is gone - it rejoins SPM coverage. Verified locally:
builds under SPM in 40s. First concrete slice of the planned
SPM-only scaffold regeneration.
The Apple exit-laundering checked for the substring 'tests passed', which '0 tests passed, 1 failed.' happily contains - a genuinely failing iOS job (app failed to even load the suite) was published as green. Every wrapper now parses the runner's own tally numerically: failed > 0 or passed == 0 is a failure, no exceptions; a nonzero flutter exit with a clean tally (the simulator foreground quirk) is tolerated and says so. The zero-output startup hang (flutter test wedging before its first line, seen across android and ios on loaded runners) is bounded by a 15-minute perl alarm and retried once, keyed on the hang's signature: no tally at all. Android's exec one-liners stay honest reds for now - their retry needs the same restructuring and follows separately.
…ate license ignore - the tally fix exposed five macOS jobs failing on 'The log reader stopped unexpectedly' -> 'Unable to start the app on the device' - the known intermittent Apple launch race, previously laundered green. The wrappers now retry once on that signature too (it produces a tally, so the no-tally hang retry never caught it) - analysis runs through flutter_plugin_tools (the flutter/packages driver already powering the format job), with a custom-analysis config listing every package since FlutterFire manages options centrally; the tests/ app is analyzed alongside. Verified locally. publish-check was evaluated and rejected: it errors on already-published versions and requires AUTHORS files, neither of which fits this repo's release model - the pub dry-run stays on melos - the regenerated performance scaffold's SceneDelegate.swift (new in the modern Flutter template) joins the license-header ignore list next to AppDelegate/MainFlutterWindow. Verified locally with addlicense
The example built as 'Firebase Cloud Messaging Example.app'; flutter's integration_test attach machinery expects the standard Runner product and reported 'Unable to start the app on the device' - the app itself launches fine by hand (verified locally: manual simctl install+launch succeeds, no crash report). PRODUCT_NAME returns to $(TARGET_NAME) like every other example.
…launch fixes
- every example entrypoint guards Firebase.initializeApp with
Firebase.apps.isEmpty: the plugin registrant natively configures
[DEFAULT] from a bundled GoogleService-Info.plist before Dart runs,
and the explicit initializeApp then threw [core/duplicate-app] -
confirmed in CI for app_installations and crashlytics on macOS
(their committed plists carry the same registered apps, so the
native-configured instance is the right one to keep)
- messaging macOS: PRODUCT_NAME back to $(TARGET_NAME), same
attach-machinery fix as iOS
- iOS SPM builds get a --config-only pre-warm so the ephemeral package
layout exists before the first real resolution (the intermittent
'the folder firebase_core does not exist' race survived a
clean-and-retry; functions and performance hit it back to back)
- all_plugins analyze: full-history checkout + --base-branch, matching
the format job - flutter_plugin_tools resolves a git base even when
analyzing everything ('fatal: Not a valid object name main')
…unpin Xcode Dart's Firebase.apps cannot observe the natively-configured [DEFAULT] until the first platform-channel call, so the isEmpty guard always passed and the collision still threw inside initializeApp (confirmed: installations and crashlytics macOS failed identically with the guard in place). Catching duplicate-app and keeping the native instance is the only reliable form; the bundled plists carry the same registered apps, so the kept instance is correct. Xcode pins removed from smoke/fdc/pipeline/all_plugins - jobs use the runner image default (Firebase's minimum is 26.2, below any current default; the pinned 26.2 SwiftPM also canonicalizes the ephemeral symlink farm, the suspected root of the functions/performance SPM resolution failures).
…r no check
A job toggled off by an input still renders as a skipped check, so the
14 products carried ~25 permanently-skipped platform checks on every
PR (wasm x13, windows x9, web x2, macos x1). Each platform is now its
own reusable workflow (changes/android/ios/macos/web/windows, wasm as
an input on web) and each caller declares only the platform jobs its
product supports - an unsupported platform simply does not exist in
the recap, while changes-filter skips (diff cannot affect platform)
stay visible as intended.
Every job body was ported byte-identically from the monolith modulo an
enumerated allow-list (verified by scripted diff against HEAD): Xcode
pin dropped (image default; the pinned 26.2 SwiftPM canonicalizes the
ephemeral symlink farm), the ineffective SPM pre-warm removed, and
functions/performance take the CocoaPods path until default-Xcode runs
prove the SPM resolution clean.
Branch protection: check names become '<platform> / <platform>'
('web-wasm / web' for firestore's wasm leg); entries for unsupported
platforms must be deleted rather than renamed.
…mance iOS project The guard-v2 edit collapsed the whole try/catch onto one comment line, so initializeApp was never called. Web has no native [DEFAULT] fallback, so every web suite registered zero tests (Android/Apple survived on native auto-init). Verified locally: storage web 82/82 under emulators. flutter config --no-enable-swift-package-manager does not remove existing SPM integration from a project; the regenerated performance scaffold had it baked in, so the ios-spm:false retreat never reached CocoaPods. Removed the FlutterGeneratedPluginSwiftPackage references per the tool's own migration notes.
…the Android launch hang The committed example plists were placeholders, and two mechanisms turned them into failures now that the native SDKs configure [DEFAULT] from the bundle before Dart runs: - auth's plist carried the old invalid-shape 'dummy-api-key', which FIRInstallations aborts on at launch - the deterministic 'Unable to start the app on the device' on both Apple platforms. - installations/messaging/crashlytics carried valid-shape dummy values, so guard v2 kept a dummy-credential native app and every live-backend call (FIS getToken, FCM) failed. Deleted all of them; the generator already derives real plists from the injected options (inject runs before generate), auth just needed wiring into that path. performance iOS: the regenerated scaffold's pbxproj kept navigator file references to the plugin's local Swift package and the ephemeral FlutterFramework; xcodebuild resolves wrapper refs as packages, which is why de-SPM'ing the integration didn't stop 'ios/firebase_core does not exist'. Verified locally: resolution now returns empty. Android: flutter test against the AVD occasionally hangs at app launch with zero output and burned the whole 20-minute step. New wrapper mirrors the Apple one (alarm-bounded attempts, one retry, numeric tally). Apple reusables also dump recent crash reports on failure - the macOS launch deaths print nothing useful at the Flutter level. Pipeline iOS build timeout 25 -> 35 (cold SwiftPM fetch overran it).
…y in the step budget The crash reports from the new forensics step show the macOS launch deaths are taskgated kills: 'CODESIGNING - Taskgated Invalid Signature'. auth (applesignin), messaging (aps-environment) and remote_config (keychain-access-groups, empty) all carry restricted entitlements that require a provisioning profile CI's ad-hoc signing cannot provide. The mega tests app never had them and ran these suites green on macOS for years, so the examples drop them too. messaging/performance iOS were dying to arithmetic, not to a bug: the first attempt hangs at launch, the 900s alarm fires as designed, and the 20-minute step timeout then kills the retry mid-build. E2E steps get 40 minutes (two bounded attempts), jobs 60. installations has no macOS job anymore: every one of its e2e tests skips itself on macOS pending keychain-sharing work (#9538), so the job could only report '0 passed, 4 skipped', which the tally guard refuses.
…ests on macOS The pipeline pods build compiles gRPC-C++/BoringSSL from source, which alone overran a 35-minute budget; SPM pulls Google's precompiled grpc/abseil binaries. The resolution failure that forced CocoaPods here was the pinned Xcode 26.2 symlink canonicalization, and the unpinned e2e-firestore SPM job now proves the layout resolves. auth macOS launches since the entitlement fix and runs its suite; the two checkActionCode tests fail in createUserWithEmailAndPassword with [firebase_auth/keychain-error], the same keychain-sharing gap (#9538) that already skips 83 sibling tests on macOS - these two only escaped because their group's skip covers Windows.
…'s stale package refs; recycle wedged simulators - The macOS skips added to the two checkActionCode tests replaced the enclosing group's Windows skip (package:test metadata merging), which un-skipped them on Windows where checkActionCode is unimplemented. The per-test condition now covers both platforms. - messaging's example pbxproj carried the same three stale navigator wrapper refs that broke performance: Xcode resolves a wrapper ref to the plugin's real ios/ directory, where '../firebase_core' does not exist. Verified locally: resolution returns the full package set now. - A simulator that produces the zero-output launch hang stays wedged; the iOS retry burned its second alarm against the same device (observed twice in one job). The retry now shuts down, erases and reboots the simulator before the second attempt. - The pipeline build gets the same resolve-race retry the iOS reusable has had; its single attempt was the only unguarded SPM build left.
… the simulator around hangs The pipeline test step burned its whole budget on the zero-output launch hang (Xcode build done, then silence) with no bound, no retry and no recovery; it now runs the same alarm + simulator-recycle + retry + numeric-tally wrapper as reusable_e2e_ios.yaml. The hang does not reproduce locally: the full messaging suite passes on a real machine over the identical SPM path (pod deintegrate, real config). On the retry path both wrappers now dump the simulator's log for the Runner process before recycling, so the next CI hang shows what the app was doing instead of nothing.
…s; smoke android gets the retry wrapper The Flutter tool's SPM migration is what plants the poison: on every project without committed integration it adds navigator refs including one to the plugin's real ios/ directory, and xcodebuild resolving that ref dies on the plugin manifest's '../firebase_core' relative path. That is why pipeline failed resolution persistently while examples with committed integration never do. pipeline_example now commits the migration output minus those navigator refs (verified locally: full package set resolves), same end state as the stable examples. The simulator log captured around a hang shows the app alive and serving analytics traffic while waits forever - the failure is the tool's log reader missing the VM service URI, not the app or the device. So: three bounded attempts (900s cold, 600s warm), diagnostics after the first failure, simulator recycle only before the last attempt. smoke android still ran a bare flutter test inside the AVD action with a 20-minute budget; it now uses the same retry wrapper and 40 minutes as reusable_e2e_android.yaml.
… failures The only red left on the run was performance android dying in 'Pre-build APK' on 'Gradle threw an error while downloading artifacts from the network' - a transient repository hiccup with no retry anywhere in the path. Evidence-gated like the other retries: only that message triggers the second build, real failures still fail fast.
firestore windows died at 'ext.flutter.driver: (112) Service has disappeared' - the desktop app crashing partway through, an intermittent native crash the step had no answer for. Retry that exact shape once; test failures and [E] verdicts keep their first result.
Final layer of stack #18536. All remaining suites move to per-package, path-filtered, platform-scoped workflows; live-service products run at PR time only for changes to their own package (or core), injected from the new TESTS_E2E_* secrets with fork guards; app identity unified on the registered io.flutter.plugins.firebase.tests; tests/ becomes the coexistence smoke app; all_plugins restructured. Details in the commit message.