@touqeerraza/rn-publisher
v0.0.3
Published
Interactive CLI to publish React Native apps to Firebase App Distribution, Google Play, and TestFlight.
Maintainers
Readme
@touqeerraza/rn-publisher
Interactive CLI to publish a React Native app to Firebase App Distribution, Google Play, and TestFlight.
It assumes a standard React Native layout (android/, ios/, release Gradle tasks, Xcode workspace). If your project uses custom flavors or paths, edit the generated config.
Requirements
- Node.js 20+
- A React Native app with Android and/or iOS native projects
- Tools for the destinations you use:
| Destination | Tool |
| ----------- | ---- |
| Firebase App Distribution | Firebase CLI (firebase login) |
| Google Play | fastlane (supply) |
| TestFlight | macOS + Xcode (xcodebuild, xcrun altool) |
Install
npm install -D @touqeerraza/rn-publisher
# or
yarn add -D @touqeerraza/rn-publisherOptional script in package.json:
{
"scripts": {
"publish:app": "rn-publisher"
}
}Quick start
From your React Native project root:
npx rn-publisher init
npx rn-publisher doctor
npx rn-publisherWhat init creates
| File | Tracked? | Purpose |
| ---- | -------- | ------- |
| rn-publisher.config.js | yes | Build tasks, artifact paths, Xcode settings |
| publish-secrets/keys.json | no | Destination credentials and IDs |
| publish-secrets/README.md | no | What to put in the secrets folder |
| .gitignore entries | yes | Ignores the secrets folder and release markers |
init always creates ./publish-secrets (gitignored). Change secretsDir / keysFile in the config if you need a different path.
It also fills values it can detect from a normal RN app:
- Android
applicationId→play.packageName - iOS bundle identifier →
apple.bundleId ios/*.xcworkspace→ios.workspaceandios.schemeandroid/app/build.gradleor.kts→android.versionCodeFile
You still need to add Firebase / Play / Apple credentials and an iOS ExportOptions plist.
Default config
Generated config matches the usual RN release layout:
/** @type {import('@touqeerraza/rn-publisher').Config} */
module.exports = {
secretsDir: "./publish-secrets",
keysFile: "./publish-secrets/keys.json",
environments: {
prod: {
label: "Production",
android: {
assembleTask: "assembleRelease",
bundleTask: "bundleRelease",
apkPath: "android/app/build/outputs/apk/release/app-release.apk",
aabPath: "android/app/build/outputs/bundle/release/app-release.aab",
versionCodeFile: "android/app/build.gradle",
},
ios: {
workspace: "ios/MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Release",
exportOptionsPlist: "ios/ExportOptions-Prod.plist",
projectDir: "ios",
},
},
},
platforms: {
firebase: { enabled: true },
play: { enabled: true },
ios: { enabled: true },
},
};| Field | Meaning |
| ----- | ------- |
| secretsDir | Folder for Play JSON and Apple .p8 files |
| keysFile | Path to destination credentials JSON |
| environments | One or more publish targets (menus use these keys) |
| android.assembleTask / bundleTask | Gradle tasks for APK / AAB |
| android.apkPath / aabPath | Expected outputs after those tasks |
| android.versionCodeFile | Gradle file that holds versionName / versionCode |
| android.keystoreProperties | Optional existence check for doctor only |
| ios.workspace / scheme / configuration | Archive settings |
| ios.exportOptionsPlist | Plist for -exportArchive (method = app-store) |
| ios.projectDir | Folder with the .xcodeproj (usually ios) |
| platforms.*.enabled | Hide a destination from the menu |
Add more environments by copying the prod block and matching keys in keys.json.
Keys file
Credentials live in publish-secrets/keys.json. Environment names must match environments in the config.
{
"prod": {
"firebase": {
"appId": "1:1234567890:android:abcdef",
"groups": "testers"
},
"play": {
"packageName": "com.example.app",
"serviceAccountJson": "./play-service-account.json",
"closedTrack": "alpha",
"releaseStatus": "draft"
},
"apple": {
"bundleId": "com.example.app",
"apiKeyId": "XXXXXXXXXX",
"apiIssuerId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"apiKeyP8": "./AuthKey_XXXXXXXXXX.p8"
}
}
}| Key | Used for |
| --- | -------- |
| firebase.appId | Firebase Android app id |
| firebase.groups | App Distribution tester group(s) |
| play.packageName | Play Console application id |
| play.serviceAccountJson | Service-account JSON under secretsDir |
| play.closedTrack | Closed-testing track name (default alpha) |
| play.releaseStatus | draft (default) or completed — Play release status for uploads |
| apple.bundleId | iOS bundle id |
| apple.apiKeyId / apiIssuerId / apiKeyP8 | App Store Connect API key (preferred) |
| apple.uploadEmail / appSpecificPassword | Alternative to API key |
Relative paths in keys resolve against secretsDir.
For Apple: if any API key field is set, all three are required. Otherwise both uploadEmail and appSpecificPassword are required.
How publish works
npx rn-publisherInteractive flow:
- Shows which destinations are available on this machine
- Pick an environment
- Pick destinations (Firebase / Play / TestFlight)
- Pick a Play track if Play is selected
- Edit or accept release notes
- Enter version name and version code separately for each selected destination
- Confirm, then build and upload
What runs per destination:
| Destination | Build | Upload |
| ----------- | ----- | ------ |
| Firebase | assembleTask → APK | firebase appdistribution:distribute |
| Google Play | bundleTask → AAB | fastlane supply (notes via changelog metadata) |
| TestFlight | xcodebuild archive + export | xcrun altool |
Versioning
Publish asks per destination before building (not one shared pair for everything):
| Destination | Prompt | Written to project? |
| ----------- | ------ | ------------------- |
| Firebase | Version name + code (defaults to current) | No — applied only for that APK build, then restored |
| Google Play | Version name + code (default code: current + 1) | Yes — versionName / versionCode in Gradle |
| TestFlight | Version name + build (default build: current + 1) | Yes — MARKETING_VERSION / CURRENT_PROJECT_VERSION |
Version name must look like 1.0 or 1.2.3.
iOS: Info.plist (app and every extension) should use $(MARKETING_VERSION) and $(CURRENT_PROJECT_VERSION), not hardcoded numbers. Otherwise the IPA keeps the old build and App Store Connect / extensions can reject the upload.
If Google Play or TestFlight fails before any store upload succeeds, those version file changes are reverted. Firebase never leaves version edits in the repo. If a store upload succeeds, Play/iOS changes are kept.
Destinations
Firebase App Distribution
1. App ID and tester group
- Open the Firebase Console → select your project
- Project settings (gear) → General → Your apps → select the Android app → copy App ID (
1:…:android:…) - App Distribution → Testers & groups → create or select a group → copy the group alias next to the group name
2. Fill keys.json
| Key | Value |
| --- | ----- |
| firebase.appId | Android App ID from Project settings |
| firebase.groups | Tester group alias from App Distribution |
3. Sign in on the publishing machine
firebase loginUse an account that can distribute builds for that Firebase project.
Google Play
1. Create a service account key
- Open the Google Cloud Console → select the project linked to Play
- IAM & Admin → Service Accounts → Create service account
- On the new account, open ⋮ → Manage keys → Add key → Create new key (JSON)
- Place the downloaded JSON file in
publish-secrets/ - Copy the service account email (e.g.
[email protected])
2. Invite the service account in Play Console
- Open Google Play Console → Users and permissions → Invite user
- Paste the service account email, select the app, and grant Releases and Store preferences (or equivalent release access)
- Accept the invite so the account can upload builds
3. Fill keys.json
| Key | Value |
| --- | ----- |
| play.packageName | Android application id (e.g. com.example.app) |
| play.serviceAccountJson | Path/name of the JSON key under publish-secrets/ (e.g. ./play-service-account.json) |
| play.releaseStatus | draft while the app is draft in Play Console; completed once completed releases are allowed |
| play.closedTrack | Optional; defaults to alpha for closed testing |
TestFlight
1. Create an App Store Connect API key
- App Store Connect → Users and Access → Integrations → App Store Connect API
- Click Generate API Key (or +), name it, and give it at least App Manager access
- Copy the Key ID and the page’s Issuer ID
- Download the
.p8file once (namedAuthKey_<KEY_ID>.p8) — Apple only shows the download once - Place that file in
publish-secrets/ - In
keys.json, set:
| Key | Value |
| --- | ----- |
| apple.apiKeyId | Key ID from App Store Connect |
| apple.apiIssuerId | Issuer ID from the same Integrations page |
| apple.apiKeyP8 | Path/name of the .p8 file under publish-secrets/ (e.g. ./AuthKey_XXXXXXXXXX.p8) |
| apple.bundleId | iOS bundle identifier (e.g. com.example.app) |
2. ExportOptions plist
- Create a plist under
ios/(e.g.ios/ExportOptions-Prod.plist) withmethod=app-storeand your Apple Team ID - Set
ios.exportOptionsPlistinrn-publisher.config.jsto that path
Example plist (ios/ExportOptions-Prod.plist):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>uploadSymbols</key>
<true/>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>Team ID: Apple Developer → Membership details → Team ID.
3. Versioning (required for uploads)
- In the app target’s Info.plist, use
$(MARKETING_VERSION)and$(CURRENT_PROJECT_VERSION)— not hardcoded numbers - Ensure the app and any extensions (e.g. notification service) share the same build number via Build Settings /
$(CURRENT_PROJECT_VERSION) - App Store Connect rejects duplicate
CFBundleVersion; bump the version code for each upload
Publishing requires macOS with Xcode. Run cd ios && pod install so ios.workspace resolves.
CI / flags
npx rn-publisher publish \
--env prod \
--platforms firebase,play \
--track closed \
--version-name 1.2.3 \
--version-code 42 \
--yes \
--commit| Flag | Meaning |
| ---- | ------- |
| --env | Environment key from config |
| --platforms | firebase, play, ios (comma-separated) |
| --track | Play: closed, open, or production |
| --notes | Release notes (skips git-generated notes) |
| --version-name | Default version name for each destination prompt (CI: applied per selected destination) |
| --version-code | Default Android versionCode / iOS build number (interactive Play/iOS default: current + 1; Firebase / CI without flags: current) |
| --yes / -y | Skip confirm |
| --commit | Commit version file changes as chore: release <env> |
Non-interactive runs must pass the required flags. Without --version-name / --version-code, CI keeps the current project versions.
Doctor
npx rn-publisher doctor
npx rn-publisher doctor --env prodChecks tools, config paths, and keys without publishing.
Troubleshooting
Failures include a suggestion. Common cases:
| Symptom | Fix |
| ------- | --- |
| Config not found | Run rn-publisher init in the app root |
| Keys still placeholders | Fill publish-secrets/keys.json |
| Play JSON missing | Put the file in secretsDir and set play.serviceAccountJson |
| Firebase not logged in | firebase login |
| fastlane missing | Install fastlane |
| TestFlight unavailable | Use macOS with Xcode |
| Workspace missing | cd ios && pod install, then check ios.workspace |
| Archive / export failed | Check ios/build/archive-*.log or export-*.log |
| TestFlight build already used | Use a higher version code; ASC rejects duplicate CFBundleVersion |
| Extension version mismatch | Match app + .appex build numbers ($(CURRENT_PROJECT_VERSION)) |
| Gradle signing failed | Fix Android signing in Gradle / CI |
License
MIT
