Skip to content
Open
171 changes: 171 additions & 0 deletions .github/workflows/publish-example-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Publish the React Native SDK example app to Google Play Console internal track.
#
# Triggers:
# - workflow_dispatch: Manual publish (preferred — SDK releases don't map 1:1 to example app publishes)
# - release: published: Fires on every SDK release to keep the example app in sync
#
# Required secrets (set in repo Settings → Secrets and variables → Actions):
# GOOGLE_SERVICES_JSON - Contents of google-services.json for Firebase
# SIGNING_KEY - Base64-encoded release keystore
# 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 for the example app build.
# Required: workflow fails early if unset (guard step after checkout).

name: Publish Example App to Play Store Internal Track

on:
workflow_dispatch:
release:
types: [published]
# Temporary: run on every push to this branch so we can iterate on the workflow
# before merge. `workflow_dispatch` only works from the default branch, so this
# 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.


jobs:
deploy:
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v4

- name: Verify KLAVIYO_EXAMPLE_API_KEY secret is set
run: |
if [ -z "${KLAVIYO_EXAMPLE_API_KEY}" ]; then
echo "::error::KLAVIYO_EXAMPLE_API_KEY secret is not set. The workflow cannot proceed — the build would produce an app that crashes at launch."
exit 1
fi
shell: bash
env:
KLAVIYO_EXAMPLE_API_KEY: ${{ secrets.KLAVIYO_EXAMPLE_API_KEY }}

# Node + Yarn 3 + workspace deps. Required because the React Native Gradle plugin
# invokes the Metro bundler during bundleRelease to generate the JS bundle. Installs
# the example workspace too, making react-native and its CLI available to Gradle.
- name: Setup
uses: ./.github/actions/setup

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-package: jdk
Comment thread
cursor[bot] marked this conversation as resolved.

- name: Add Google Services from Secrets
run: 'echo "$GOOGLE_SERVICES_JSON" > ./example/android/app/google-services.json'
shell: bash
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}

# Write the Klaviyo public API key into example/.env so react-native-dotenv
# can expose it to the JS layer as KLAVIYO_API_KEY (imported from '@env').
# The guard step above ensures the secret is set before we reach this point.
- name: Write .env with Klaviyo API key
run: echo "KLAVIYO_API_KEY=${KLAVIYO_EXAMPLE_API_KEY}" > example/.env
env:
KLAVIYO_EXAMPLE_API_KEY: ${{ secrets.KLAVIYO_EXAMPLE_API_KEY }}
Comment thread
evan-masseau marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

# Decode the base64-encoded upload keystore from secrets into a file that
# the Gradle signing config can point at. The runner is ephemeral so the
# plaintext .jks is torn down with the VM, but we also remove it at the
# end of the job as hygiene.
- name: Decode upload keystore
run: echo "$SIGNING_KEY" | base64 -d > "${RUNNER_TEMP}/upload.jks"
env:
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}

# bundleRelease triggers react-native bundle automatically via the RN Gradle plugin.
# We cd into the android directory so that relative paths in settings.gradle resolve correctly.
#
# Signing is done inline via `-Pandroid.injected.signing.*` gradle properties so AGP
# produces a properly v2/v3-signed AAB in a single pass — no separate re-sign step,
# no risk of double-signing (Play rejects AABs with multiple signer certificates).
# versionCode is overridden per-run via github.run_number so successive
# CI uploads never collide on Play's strictly-increasing versionCode rule.
# Read in build.gradle as `project.findProperty("versionCode")` — AGP's
# `android.injected.version.code` is an IDE flag and isn't reliably
# honored by command-line Gradle builds.
# Caveat: manual Play uploads that bump versionCode outside CI can get
# ahead of run_number — if that happens, bump CI past the manual number
# (re-trigger the workflow N times, or add an offset).
- name: Assemble Release Bundle
run: |
cd example/android && ./gradlew :app:bundleRelease \
-PreleaseVersionCode=${{ github.run_number }} \
-Pandroid.injected.signing.store.file="${RUNNER_TEMP}/upload.jks" \
-Pandroid.injected.signing.store.password="${KEY_STORE_PASSWORD}" \
-Pandroid.injected.signing.key.alias="${ALIAS}" \
-Pandroid.injected.signing.key.password="${KEY_PASSWORD}"
env:
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
ALIAS: ${{ secrets.ALIAS }}
# Prevent Metro from trying to start a dev server during the build
CI: true
Comment thread
evan-masseau marked this conversation as resolved.

# `status: draft` — the release lands on the internal track unpublished
# so you can manually promote it in Play Console. Required while the app
# listing itself is still in Draft state (no published version yet);
# flip to `completed` once the listing is fully configured for release.
- name: Deploy to Internal Track
uses: r0adkll/[email protected]
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.klaviyoreactnativesdkexample
releaseFiles: example/android/app/build/outputs/bundle/release/app-release.aab
track: internal
status: draft

- name: Clean up keystore
if: always()
run: rm -f "${RUNNER_TEMP}/upload.jks"

- name: Notify Slack on success
if: success()
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "✅ RN SDK Example App published to Play Store internal track"
blocks:
- type: "header"
text:
type: "plain_text"
text: "✅ RN SDK Example App published to Play Store internal track"
- type: "section"
text:
type: "mrkdwn"
text: "*Repository:* ${{ github.repository }}\n*Workflow:* ${{ github.workflow }}\n*Release:* `${{ github.event.release.tag_name || 'manual' }}`\n*Commit:* `${{ github.sha }}`\n*Author:* ${{ github.actor }}"
- type: "section"
text:
type: "mrkdwn"
text: "*Workflow Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"

- name: Notify Slack on failure
if: failure()
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "🚨 RN SDK Example App Play Store publish failed"
blocks:
- type: "header"
text:
type: "plain_text"
text: "🚨 RN SDK Example App Play Store publish failed"
- type: "section"
text:
type: "mrkdwn"
text: "*Repository:* ${{ github.repository }}\n*Workflow:* ${{ github.workflow }}\n*Release:* `${{ github.event.release.tag_name || 'manual' }}`\n*Commit:* `${{ github.sha }}`\n*Author:* ${{ github.actor }}"
- type: "section"
text:
type: "mrkdwn"
text: "*Workflow Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
7 changes: 5 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ android {
applicationId "com.klaviyoreactnativesdkexample"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
// versionCode is overridable at build time via `-PreleaseVersionCode=N`
// so CI can inject a monotonic value from github.run_number. Local
// builds default to 1.
versionCode Integer.parseInt(project.findProperty("releaseVersionCode")?.toString() ?: "1")
versionName "2.4.0"
manifestPlaceholders = [usesCleartextTraffic: "false"]
}
signingConfigs {
Expand Down
Loading