ci(example): publish example app to Play Store internal track#347
ci(example): publish example app to Play Store internal track#347evan-masseau wants to merge 8 commits intofeat/example-appfrom
Conversation
e759edf to
85e0eb7
Compare
e0b8e13 to
a3d7d0a
Compare
85e0eb7 to
0c8357f
Compare
0c8357f to
7f6e898
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7f6e898. Configure here.
79a17be to
7d94eae
Compare
7d94eae to
00964ee
Compare
00964ee to
a27ed6b
Compare
a3d7d0a to
ac8ae77
Compare
a27ed6b to
beae0fa
Compare
beae0fa to
f40dbdc
Compare
92f82d4 to
ac8ae77
Compare
ac8ae77 to
bf39103
Compare
f40dbdc to
c1b80cf
Compare
bf39103 to
cad277c
Compare
cad277c to
b2fb2b6
Compare
c1b80cf to
0e694cd
Compare
b2fb2b6 to
0bd78ef
Compare
0e694cd to
2031397
Compare
Adds a GitHub Actions workflow to build and publish the React Native SDK example app (com.klaviyoreactnativesdkexample) to the Google Play internal track. Fires on SDK releases and manual workflow_dispatch. Includes Node 20 + Yarn 3 setup, JS bundle generation via the RN Gradle plugin (bundleRelease), signing with r0adkll/sign-android-release, and Slack notifications for both success and failure. Part of MAGE-464
0bd78ef to
ecf5cfa
Compare
2031397 to
158f110
Compare
#345) # Description Part 3 of 4 in the RN example-app overhaul chain for [MAGE-464](https://linear.app/klaviyo/issue/MAGE-464). Wires up native-level Firebase push integration on both iOS and Android using the JS-first init pattern. Both platforms compile and launch cleanly without Firebase configured (push features are disabled in that mode); full push flow lights up end-to-end when a Firebase config file is present. ## Due Diligence - [x] I have tested this on a simulator/emulator or a physical device, on iOS and Android (if applicable). - [ ] I have added sufficient unit/integration tests of my changes. - [ ] I have adjusted or added new test cases to team test docs, if applicable. - [x] I am confident these changes are implemented with feature parity across iOS and Android (if applicable). ## Release/Versioning Considerations - [x] `Patch` Contains internal changes or backwards-compatible bug fixes. - [ ] `Minor` Contains changes to the public API. - [ ] `Major` Contains **breaking** changes. - [ ] Contains readme or migration guide changes. - [ ] This is planned work for an upcoming release. ## Changelog / Code Overview ### iOS | File | Change | |------|--------| | `AppDelegate.mm` | Call `[FIRApp configure]` on launch (unconditional — stub plist is the zero-config default, documented in #346); preserve UN delegate, deep-link, universal-link, and silent-push handlers; retain `getLaunchOptionsWithURL` helper for RN #32350 cold-start workaround; keep native-init reference block commented | | `Podfile` | `$RNFirebaseAsStaticFramework = true` so RNFirebase links cleanly under `use_frameworks!` | | `Info.plist` | `UIBackgroundModes = [location, remote-notification]`; location usage strings; URL scheme registration | | `*.entitlements` | `aps-environment = development`, wired via `CODE_SIGN_ENTITLEMENTS` | | `project.pbxproj` | Bundle id normalized to `com.klaviyoreactnativesdkexample` (matches Firebase app id and Android `applicationId`); `GoogleService-Info.plist` file reference added so it's bundled when integrators drop it in | | `.github/workflows/ios-build.yml` | Stub `GoogleService-Info.plist` step so CI builds succeed without a real Firebase project (format-valid values so `FirebaseApp.configure()` succeeds at launch; benign backend-registration warning at runtime) | ### Android | File | Change | |------|--------| | `gradle.properties`, `local.properties.template`, `app/build.gradle` | Remove dead `initializeKlaviyoFromNative` / `publicApiKey` / `useNativeFirebase` gradle→BuildConfig plumbing (nothing reads it) | | `app/build.gradle` | Conditionally apply `com.google.gms.google-services` plugin on the presence of `app/google-services.json` — clean build without push configured | | `MainApplication.kt` | Commented reference block for native init; primary path stays in JS | ## Test Plan - [x] iOS: build with stub `GoogleService-Info.plist` (values documented in #346) — app launches, push section shows "Firebase not configured" UI - [x] iOS: build with real plist — `[FIRApp configure]` runs, permission request works (see known sim caveat below) - [x] Android: clean build without `google-services.json` — gms plugin not applied, app launches, push section shows "Firebase not configured" UI - [x] Android: clean build with real `google-services.json` — gms plugin applies, Firebase init succeeds, token populates ### Known iOS simulator caveat Apple has a documented regression on iOS 26.0 / 26.2 simulators where `didRegisterForRemoteNotificationsWithDeviceToken:` never fires (FB19400926, FB19404213; matches [rnfirebase/#8937](invertase/react-native-firebase#8937)). APNs token fetching works correctly on physical devices and iOS 18.x simulators. Setup is correct — Apple's sim is broken. ## Related Issues/Tickets Part of [MAGE-464](https://linear.app/klaviyo/issue/MAGE-464) **Chained PR series:** 1. theme + components (merged in #342) 2. JS layer: permission helpers, hooks, app shell (merged in #343) 3. **This PR** — native platform setup (iOS + Android Firebase push) 4. docs — #346 Also stacked alongside: #347 (CI Play Store publish workflow, branched off this PR). Follow-up: [MAGE-534](https://linear.app/klaviyo/issue/MAGE-534) — convert `AppDelegate.mm` to pure Swift. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
| on: | ||
| workflow_dispatch: | ||
| release: | ||
| types: [ published ] |
There was a problem hiding this comment.
I need to add a trigger here that I can use to test this on github actions, before merging to master... maybe a label?
Temporary push trigger scoped to this branch so the publish pipeline can be exercised end-to-end before merge — workflow_dispatch only fires from the default branch, so push is the only way to test from the PR. Remove before merging to master. Also aligns the runner with every other workflow in the repo (android-build.yml, ci.yml, doc-bot.yml all use ubuntu-24.04), addressing the Cursor Bugbot note. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0924305. Configure here.
| # is the only way to exercise the pipeline end-to-end against the PR. Remove | ||
| # this `push` trigger before merging to master. | ||
| push: | ||
| branches: [ecm/ci/android-example-play-publish] |
There was a problem hiding this comment.
Temporary push trigger left in workflow file
Medium Severity
A temporary push trigger on branch ecm/ci/android-example-play-publish is still present in the workflow, with an inline comment explicitly stating "Remove this push trigger before merging to master." If merged as-is, any push to that branch would trigger the full publish pipeline, potentially uploading unintended builds to the Play Store internal track. The PR description only mentions workflow_dispatch and release: published as intended triggers.
Reviewed by Cursor Bugbot for commit 0924305. Configure here.
When a `blocks` array is supplied, Slack treats top-level `text` as fallback only — for mobile push and accessibility — and never renders it in the message body. Without a header block the "✅ published" / "🚨 publish failed" title vanished from the Slack message, leaving just the metadata sections. Adds a `type: header` block (plain_text) at the top of both the success and failure payloads so the title is visible inline. Top-level `text` stays so the push notification fallback still reads correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The publish workflow was failing at Play upload with "signed with multiple
certificate chains" because Gradle was signing the release AAB with the
debug keystore (via the RN template's stock `release { signingConfig
signingConfigs.debug }` line) before the `r0adkll/sign-android-release`
step layered the real upload key on top. Play rejects AABs with more
than one signer.
Drops the debug signing config from the release buildType so bundleRelease
produces an unsigned AAB. The CI signing step then signs it exactly once
with the upload key from SIGNING_KEY. For local signed release builds,
pass signing via `-Pandroid.injected.signing.*` gradle properties — noted
inline.
Also bumps versionName to 2.4.0 to match the SDK version for the first
Play Store release.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…gner
Switches the publish workflow to sign the release AAB in a single Gradle
pass using `-Pandroid.injected.signing.*` properties. AGP produces a
properly v2/v3-signed AAB directly, so the separate `r0adkll/sign-
android-release` step is gone — fewer moving parts, no double-signing
risk, and one less abandoned Node-20 action emitting deprecation warnings.
Also reverts the previous build.gradle change that removed the release
signingConfig. With CI now doing its own signing via gradle properties
at build time, there's no need to break the RN template default of
`release { signingConfig signingConfigs.debug }` — that default keeps
local `./gradlew :app:bundleRelease` and `yarn android --mode release`
working out of the box for devs. CI's injected properties override
the buildType signingConfig anyway.
The keystore is decoded from the SIGNING_KEY secret to ${RUNNER_TEMP}
and cleaned up in an always() step as hygiene (the runner is ephemeral
but explicit cleanup is cheap insurance).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Prevents collision with the versionCode=1 AAB that was uploaded manually to the Play Store internal track to prove package name ownership. Play rejects duplicate versionCodes per package, so the next CI upload would fail without this. github.run_number is monotonic per-workflow-per-repo, so successive CI publishes will always produce a strictly-increasing versionCode. The static `versionCode 1` in build.gradle stays as the local dev default; AGP's `android.injected.version.code` property wins when set. Known limitation (same as sibling TestFlight workflow): manual uploads that bump the versionCode outside of CI can get ahead of run_number, which would then fail as "not greater than previously uploaded build". Fix if/when it happens by re-triggering CI enough times to overtake, or add a large offset to run_number. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
AGP's `android.injected.version.code` property is an IDE-oriented flag that isn't reliably honored by command-line Gradle builds — CI passed it but the resulting AAB still had versionCode=1, colliding with the manually-uploaded verification build in Play. Switches to a custom project property read explicitly in build.gradle: `-PreleaseVersionCode=N` → `Integer.parseInt(...findProperty(...))`. Locally verified: `-PreleaseVersionCode=42` produces AndroidManifest with `android:versionCode="42"`. Falls back to 1 when the property isn't set, preserving local dev behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Play rejects `status: completed` uploads to an app whose listing hasn't been published out of Draft. Switches to `status: draft` so the release lands on the internal track unpublished — manual promote in Console until the app listing is fully set up (content rating, data safety, etc.), then we flip back to `completed` for fully-automated publishes. Track stays `internal`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>


Summary
.github/workflows/publish-example-android.ymlto build and publish the React Native SDK example app (com.klaviyoreactnativesdkexample) to the Google Play internal trackrelease: published(mirrors the Android test app pattern) andworkflow_dispatchfor manual publishesMotivation
The RN SDK example app needs to be distributable via Play Store internal track for QA and stakeholder testing, matching what the Android test app already does. Branches off
ecm/example-app/4-native(PR #345) since the Play Store pipeline depends on that PR's Android Firebase + bundle-id setup.Parallel CI improvement: klaviyo-android-test-app — Slack notifications on publish, which brings the test app's publish workflow to parity with this one.
Required Secrets
Wire these up in repo Settings → Secrets and variables → Actions before the workflow will succeed:
GOOGLE_SERVICES_JSONgoogle-services.jsonfor FirebaseSIGNING_KEYALIASKEY_STORE_PASSWORDKEY_PASSWORDSERVICE_ACCOUNT_JSONKLAVIYO_EXAMPLE_API_KEYexample/.envat build time so the RN app initializes Klaviyo on launchSLACK_WEBHOOK_URLA guard step early in the workflow fails the run with a clear message if
KLAVIYO_EXAMPLE_API_KEYis unset — no silent fallback, no crashing builds on Play Store.Test plan
workflow_dispatchand confirm the AAB is uploaded to the Play Store internal trackPart of MAGE-464