Background
We have a desktop end-to-end Nimbus enrollment integration test that spins up a local Experimenter stack and drives Firefox Desktop through a real enrollment cycle. We used to have equivalents for Fenix and iOS in experimenter/tests/integration/nimbus/{android,ios}/ but the CircleCI scaffolding that brokered those tests rotted — the jobs still exist in .circleci/config.yml:339-505 but are gated to filters: branches: only: [update_firefox_versions, fix-13658], both bot-only / stale, so they effectively never run. The Python + Kotlin test code itself is still intact.
Post-Oct-2025 monorepo consolidation also changed Fenix's build outputs (product flavors removed — app-fenix-x86_64-debug.apk → app-debug.apk, assembleFenixDebug → assembleDebug) and its home (mozilla-mobile/firefox-android → firefox-desktop-android/mobile/android/fenix/). The old moz-central.test-container.Dockerfile that cinnabar-cloned mozilla-central to build Fenix is obsolete.
What we're building
A GitHub Actions workflow that exercises the recipe JSON contract between Experimenter and Fenix without building Fenix from source or standing up a full Remote Settings pipeline.
Shape
- Download a publicly-indexed Fenix debug APK from TaskCluster (
signing-apk-fenix-debug).
- Bring up the Experimenter stack (Django + Postgres + nginx — no Kinto/Autograph/RS needed for this path).
- POST a test experiment via the existing
helpers.create_experiment() helper.
- GET the recipe JSON from
/api/v6/draft-experiments/{slug}/.
- Boot an Android emulator via
reactivecircus/android-emulator-runner.
adb install app-debug.apk.
nimbus-cli enroll <slug> --branch control --file recipe.json --preserve-targeting --preserve-bucketing --reset-app — pushes the recipe through the full SDK evaluation path (JEXL + bucketing + enrollment write).
nimbus-cli log-state → adb logcat -d | grep app-services-Nimbus → assert the expected slug | features | branch line is present with reason=Qualified.
Why --preserve-targeting --preserve-bucketing
Default nimbus-cli enroll rewrites the recipe: targeting → "true", bucketConfig → {start:0, count:10000}, target branch ratio=100, others ratio=0 — that's force-enrollment and we don't want it. With both preserve flags set, the recipe is pushed verbatim and evaluated normally against the emulator's real targeting context + bucketing hash. Same SDK code path as if the JSON had arrived via Remote Settings.
Why we skip the RS fetch path
The contract surface between Experimenter and Fenix is the recipe JSON (feature manifest + JEXL + branch structure). The RS fetch path is remote-settings-client + Autograph verification — third-party infra that neither team typically changes. Skipping it lets us avoid the large disk/compute cost of running the Autograph/Kinto containers and rebuilding Fenix for compile-time NIMBUS_ENDPOINT override.
What this covers
- Recipe JSON schema evolution
- Feature manifest emission + parsing
- JEXL targeting evaluation
- Bucketing hash + branch-ratio selection
- Nimbus SDK enrollment state writes + materialization
- Feature conflict checks
What this intentionally does not cover
- Remote Settings HTTP fetch
- Autograph signature verification
- RS collection metadata format
A separate pure-Python schema-validation test can fill those gaps if we want them later.
Trigger
Scheduled daily + workflow_dispatch. On schedule, check the TC index for a new signing-apk-fenix-debug task hash; if changed, bump a pinned hash env file in experimenter/tests/ (analogous to the existing firefox_fenix_release_build.env / firefox_fenix_beta_build.env pattern driven by scripts/external_integration_updater_script.sh).
Related context
- Task folder:
tasks/2026-04-17-fenix-integration-tests/
- Old CircleCI jobs to remove once this is green:
build_firefox_fenix, integration_nimbus_fenix_enrollment, create_mobile_recipes in .circleci/config.yml:339-505
- Old
moz-central.test-container.Dockerfile to delete
- iOS equivalent is out of scope for this issue — tracked separately if/when we pick it up
┆Issue is synchronized with this Jira Task
Background
We have a desktop end-to-end Nimbus enrollment integration test that spins up a local Experimenter stack and drives Firefox Desktop through a real enrollment cycle. We used to have equivalents for Fenix and iOS in
experimenter/tests/integration/nimbus/{android,ios}/but the CircleCI scaffolding that brokered those tests rotted — the jobs still exist in.circleci/config.yml:339-505but are gated tofilters: branches: only: [update_firefox_versions, fix-13658], both bot-only / stale, so they effectively never run. The Python + Kotlin test code itself is still intact.Post-Oct-2025 monorepo consolidation also changed Fenix's build outputs (product flavors removed —
app-fenix-x86_64-debug.apk→app-debug.apk,assembleFenixDebug→assembleDebug) and its home (mozilla-mobile/firefox-android→firefox-desktop-android/mobile/android/fenix/). The oldmoz-central.test-container.Dockerfilethat cinnabar-cloned mozilla-central to build Fenix is obsolete.What we're building
A GitHub Actions workflow that exercises the recipe JSON contract between Experimenter and Fenix without building Fenix from source or standing up a full Remote Settings pipeline.
Shape
signing-apk-fenix-debug).helpers.create_experiment()helper./api/v6/draft-experiments/{slug}/.reactivecircus/android-emulator-runner.adb install app-debug.apk.nimbus-cli enroll <slug> --branch control --file recipe.json --preserve-targeting --preserve-bucketing --reset-app— pushes the recipe through the full SDK evaluation path (JEXL + bucketing + enrollment write).nimbus-cli log-state→adb logcat -d | grep app-services-Nimbus→ assert the expectedslug | features | branchline is present withreason=Qualified.Why
--preserve-targeting --preserve-bucketingDefault
nimbus-cli enrollrewrites the recipe:targeting → "true",bucketConfig → {start:0, count:10000}, target branchratio=100, othersratio=0— that's force-enrollment and we don't want it. With both preserve flags set, the recipe is pushed verbatim and evaluated normally against the emulator's real targeting context + bucketing hash. Same SDK code path as if the JSON had arrived via Remote Settings.Why we skip the RS fetch path
The contract surface between Experimenter and Fenix is the recipe JSON (feature manifest + JEXL + branch structure). The RS fetch path is remote-settings-client + Autograph verification — third-party infra that neither team typically changes. Skipping it lets us avoid the large disk/compute cost of running the Autograph/Kinto containers and rebuilding Fenix for compile-time
NIMBUS_ENDPOINToverride.What this covers
What this intentionally does not cover
A separate pure-Python schema-validation test can fill those gaps if we want them later.
Trigger
Scheduled daily +
workflow_dispatch. On schedule, check the TC index for a newsigning-apk-fenix-debugtask hash; if changed, bump a pinned hash env file inexperimenter/tests/(analogous to the existingfirefox_fenix_release_build.env/firefox_fenix_beta_build.envpattern driven byscripts/external_integration_updater_script.sh).Related context
tasks/2026-04-17-fenix-integration-tests/build_firefox_fenix,integration_nimbus_fenix_enrollment,create_mobile_recipesin.circleci/config.yml:339-505moz-central.test-container.Dockerfileto delete┆Issue is synchronized with this Jira Task