Skip to content

ci(example): publish example app to Play Store internal track#347

Open
evan-masseau wants to merge 8 commits intofeat/example-appfrom
ecm/ci/android-example-play-publish
Open

ci(example): publish example app to Play Store internal track#347
evan-masseau wants to merge 8 commits intofeat/example-appfrom
ecm/ci/android-example-play-publish

Conversation

@evan-masseau
Copy link
Copy Markdown
Contributor

@evan-masseau evan-masseau commented Apr 20, 2026

Summary

  • Adds .github/workflows/publish-example-android.yml to build and publish the React Native SDK example app (com.klaviyoreactnativesdkexample) to the Google Play internal track
  • Triggers on release: published (mirrors the Android test app pattern) and workflow_dispatch for manual publishes
  • Handles Node 20 + Yarn 3 workspace setup, JS bundle generation via the RN Gradle plugin, AAB signing, and Play Store upload
  • Includes Slack notifications for both success and failure

Motivation

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:

Secret Description
GOOGLE_SERVICES_JSON Contents of google-services.json for Firebase
SIGNING_KEY Base64-encoded release keystore (upload key for Play App Signing)
ALIAS Key alias in the keystore
KEY_STORE_PASSWORD Keystore password
KEY_PASSWORD Key password
SERVICE_ACCOUNT_JSON Google Play service account JSON (plain text)
KLAVIYO_EXAMPLE_API_KEY Klaviyo public API key — written into example/.env at build time so the RN app initializes Klaviyo on launch
SLACK_WEBHOOK_URL Incoming webhook URL for Slack notifications

A guard step early in the workflow fails the run with a clear message if KLAVIYO_EXAMPLE_API_KEY is unset — no silent fallback, no crashing builds on Play Store.

Test plan

  • Wire up all required secrets in the repo
  • Trigger via workflow_dispatch and confirm the AAB is uploaded to the Play Store internal track
  • Confirm Slack notification fires in both success and failure cases
  • Verify the published app launches and initializes Klaviyo correctly on a test device (check for a Klaviyo event from the build's API key)

Part of MAGE-464

@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from e759edf to 85e0eb7 Compare April 20, 2026 22:13
@evan-masseau evan-masseau changed the base branch from master to ecm/example-app/4-native April 20, 2026 22:13
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch 3 times, most recently from e0b8e13 to a3d7d0a Compare April 21, 2026 00:20
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from 85e0eb7 to 0c8357f Compare April 21, 2026 00:26
Comment thread .github/workflows/publish-example-android.yml
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from 0c8357f to 7f6e898 Compare April 21, 2026 00:30
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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.

@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch 2 times, most recently from 79a17be to 7d94eae Compare April 21, 2026 00:38
@klaviyo klaviyo deleted a comment from cursor Bot Apr 21, 2026
Comment thread .github/workflows/publish-example-android.yml
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from 7d94eae to 00964ee Compare April 21, 2026 01:01
Comment thread .github/workflows/publish-example-android.yml
Comment thread .github/workflows/publish-example-android.yml Outdated
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from 00964ee to a27ed6b Compare April 21, 2026 01:18
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch from a3d7d0a to ac8ae77 Compare April 21, 2026 01:43
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from a27ed6b to beae0fa Compare April 21, 2026 02:27
Comment thread .github/workflows/publish-example-android.yml
Comment thread .github/workflows/publish-example-android.yml
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from beae0fa to f40dbdc Compare April 21, 2026 02:38
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch 2 times, most recently from 92f82d4 to ac8ae77 Compare April 21, 2026 02:56
@evan-masseau evan-masseau marked this pull request as ready for review April 21, 2026 11:38
@evan-masseau evan-masseau requested a review from a team as a code owner April 21, 2026 11:38
@klaviyoit klaviyoit requested a review from ajaysubra April 21, 2026 11:38
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch from ac8ae77 to bf39103 Compare April 21, 2026 16:54
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from f40dbdc to c1b80cf Compare April 21, 2026 17:06
@ab1470 ab1470 force-pushed the ecm/example-app/4-native branch from bf39103 to cad277c Compare April 21, 2026 17:16
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch from cad277c to b2fb2b6 Compare April 21, 2026 17:54
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from c1b80cf to 0e694cd Compare April 21, 2026 17:55
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch from b2fb2b6 to 0bd78ef Compare April 21, 2026 18:07
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from 0e694cd to 2031397 Compare April 21, 2026 18:07
Comment thread .github/workflows/publish-example-android.yml Outdated
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
@evan-masseau evan-masseau force-pushed the ecm/example-app/4-native branch from 0bd78ef to ecf5cfa Compare April 21, 2026 18:58
@evan-masseau evan-masseau force-pushed the ecm/ci/android-example-play-publish branch from 2031397 to 158f110 Compare April 21, 2026 18:58
evan-masseau added a commit that referenced this pull request Apr 21, 2026
#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)
Base automatically changed from ecm/example-app/4-native to feat/example-app April 21, 2026 22:41
on:
workflow_dispatch:
release:
types: [ published ]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0924305. Configure here.

evan-masseau and others added 6 commits April 21, 2026 22:27
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant