--dry-run; start with read-only commands such as appship status.
1Requirements
| You need | Notes |
|---|---|
| Node.js 22 or newer | appship is an npm package. |
| fastlane | appship runs fastlane for every store call. Install it the way the fastlane docs recommend for your machine. |
| macOS with Xcode | Only for iOS builds and uploads. Android-only projects also work on Linux. |
| Apple Developer Program | 99 USD a year. |
| Google Play Console account | 25 USD, one-time. |
| An app that builds a signed release | appship does not sign for you. See Signing. |
2Install
Install appship once per machine, globally. Nothing gets added to your app's package.json.
npm i -g appship-ai
appship --version
The package is called appship-ai and installs the appship command. To try it without installing, run npx appship-ai --help.
3Get store keys
You do this once per developer account, not per project. Both keys can publish builds under your name, so never commit them to git.
iOS: App Store Connect API key
- In App Store Connect, open Users and Access → Integrations → App Store Connect API.
- Under Team Keys, click +. Name it
appshipand give it the App Manager role. - Download
AuthKey_XXXXXXXXXX.p8. Apple lets you download it only once. - Note the Key ID (10 characters, also in the file name) and the Issuer ID (the UUID above the key table).
An Apple ID (email) is optional. It is only needed to create the app record and upload App Privacy answers, because Apple's API key can't do either. It needs 2FA, so it never works in CI.
Android: Google Play service account
- In Google Cloud Console, pick a project and enable the Google Play Android Developer API.
- Go to IAM & Admin → Service Accounts → Create service account. It needs no GCP role.
- Open the account, then Keys → Add key → JSON to download the key file.
- In Play Console, go to Users and permissions → Invite new users and invite the
client_emailfrom the JSON. Grant Release apps to testing tracks, Release to production and Manage store presence. - Wait a few minutes for the permissions to apply. On a new account it can take up to 24 hours.
Save them as a profile
A profile copies the keys to ~/.appship/credentials/<name>/ with mode 600, so every project on this machine can share them. Run it without flags to be asked step by step.
appship credentials add my-company \
--asc-key ~/Downloads/AuthKey_ABC123DEFG.p8 \
--asc-key-id ABC123DEFG \
--asc-issuer-id 69a6de7a-0000-0000-0000-000000000000 \
--play-json ~/Downloads/play-service-account.json
appship credentials list
4Set up the project
Run init from the root of your app repo.
cd ~/Projects/my-app
appship init --profile my-company
init detects Flutter, React Native, Cocos Creator or native projects, reads your bundle id and package name, and fills in the build command and artifact path. It creates this folder and adds release/keys/* and release/.appship/ to .gitignore:
my-app/
└── release/
├── release.yml # ids, artifacts, build commands, tracks, credentials
├── questionnaire.yml # answers to store questions
├── ios/metadata/<locale>/*.txt
├── ios/screenshots/<locale>/*.png
├── android/metadata/<locale>/{*.txt, images/}
├── keys/ # gitignored; only if you don't use a profile
└── .appship/ # gitignored; scratch files for fastlane
Open release/release.yml and check artifact and build_command for each platform. For a Flutter app it looks like this:
app:
name: "My App"
primary_locale: en-US
ios:
bundle_id: com.example.myapp
artifact: "build/ios/ipa/*.ipa"
build_command: "flutter build ipa --release"
upload_screenshots: true
submit:
automatic_release: false # release as soon as Apple approves
phased_release: false # 7-day phased rollout
android:
package_name: com.example.myapp
artifact: "build/app/outputs/bundle/release/*.aab"
build_command: "flutter build appbundle --release"
track: internal # internal | alpha | beta | production
release_status: completed # draft while the app is unpublished
rollout: 1 # 0.2 = 20% staged rollout
credentials:
profile: my-company
Commit the whole release/ folder except keys/ and .appship/. Your repo gets no Fastfile: the release logic stays in appship, so updating appship updates every project.
5Fill in the listing
If the app is already live, pull the current listing instead of typing it:
appship metadata pull
Otherwise, replace every TODO in these files:
release/questionnaire.yml: age rating, export compliance, content rights, reviewer contact, App Privacy, and the Android App content answers.- Text:
release/ios/metadata/<locale>/*.txtandrelease/android/metadata/<locale>/*.txt. - iOS screenshots in
release/ios/screenshots/<locale>/: at least one iPhone 6.9" set (1320×2868) or 6.5" set (1284×2778). - Android images in
release/android/metadata/<locale>/images/:icon.png512×512,featureGraphic.png1024×500, and at least 2 images inphoneScreenshots/.
6Check with doctor
doctor checks tools, config, keys, artifacts, character limits, image sizes and placeholder text, and fails if a key file is tracked by git. Repeat until there are no ✗. A ! is a warning and does not block a release.
▸ Security
✓ No key files tracked by git
✓ .gitignore protects release/keys/
▸ iOS · store listing
✗ ios/metadata/en-US/subtitle.txt is 34 characters (max 30)
▸ Android · store listing
✓ Metadata OK for en-US
✗ 1 error(s), 0 warning(s)
7First release
appship build
appship first-release --create-app
- iOS: creates the app in App Store Connect (needs the Apple ID and may ask for a 2FA code), then pushes the listing, screenshots, age rating, review info and App Privacy.
- Android: Google Play has no API to create an app, so appship prints the steps instead.
- Both write
release/CHECKLIST.md: the console steps that must be done by hand, with your answers fromquestionnaire.ymlready to copy.
On Google Play, follow the checklist to:
- Create the app in Play Console.
- Invite the service account to the app.
- Upload the first
.aabby hand to Internal testing. - Fill in the App content section.
While the Android app has never been published, set release_status: draft in release.yml. New personal developer accounts also need a closed test with 12 testers for 14 days before production.
Then finish the first release:
appship metadata --android
appship upload
appship submit
8Every release after
Bump the version and build number in your project, then:
appship upload --build # build, then TestFlight + Google Play track (internal by default)
# test on TestFlight / the internal track
appship submit # iOS: App Review · Android: promote to production
appship status # review state and versions on both stores
For a staged rollout on Android:
appship submit --android --rollout 10%
appship promote --from production --to production --rollout 50%
appship promote --from production --to production --rollout 100%
Every command that writes to a store asks for confirmation first. Add --dry-run to see exactly what would be sent.
9Run it in CI
In CI, keys come from secrets through APPSHIP_* environment variables, and every write command needs --yes. Add these repository secrets (in GitHub: Settings → Secrets and variables → Actions):
| Secret | Value |
|---|---|
APPSHIP_ASC_KEY_ID | Key ID |
APPSHIP_ASC_ISSUER_ID | Issuer ID |
APPSHIP_ASC_KEY | Output of base64 -i AuthKey_XXX.p8 |
APPSHIP_PLAY_JSON | Output of base64 -i play-service-account.json |
| Signing secrets | Depends on how you sign, for example MATCH_PASSWORD for fastlane match, or an Android keystore in base64 |
The release steps of a GitHub Actions workflow:
env:
APPSHIP_ASC_KEY_ID: ${{ secrets.APPSHIP_ASC_KEY_ID }}
APPSHIP_ASC_ISSUER_ID: ${{ secrets.APPSHIP_ASC_ISSUER_ID }}
APPSHIP_ASC_KEY: ${{ secrets.APPSHIP_ASC_KEY }}
APPSHIP_PLAY_JSON: ${{ secrets.APPSHIP_PLAY_JSON }}
jobs:
release:
runs-on: macos-latest # iOS builds need macOS
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
# ...your toolchain, fastlane and signing setup...
- run: npm i -g appship-ai
- run: appship doctor --skip-artifacts
- run: appship upload --build --yes
- if: ${{ inputs.submit }}
run: appship submit --yes
The complete workflow, with Flutter setup, signing and a manual submit trigger, is in examples/github-actions.yml.
- Creating the iOS app and uploading App Privacy need the Apple ID with 2FA. Do those once on your machine, not in CI.
- To pin a fastlane version, use a Gemfile and set
APPSHIP_FASTLANE="bundle exec fastlane". - Try the pipeline end to end with
--dry-runbefore letting it push anything.
Reference
Signing
appship doesn't build or sign your app. It runs your build_command and picks up the signed artifact.
- iOS: the simplest setup is Automatic signing in Xcode (Signing & Capabilities). For teams and CI, use fastlane match.
- Android: create an upload keystore, configure
signingConfigsin Gradle, and turn on Play App Signing with the first upload. Keep the keystore out of git, for example in the gitignoredrelease/keys/.
Where keys are read from
For each value, the first source found wins. You can mix them, for example a profile on your machine and environment variables in CI.
- Environment variables:
APPSHIP_ASC_KEY_ID,APPSHIP_ASC_ISSUER_ID,APPSHIP_ASC_KEY_PATHorAPPSHIP_ASC_KEY(contents, plain or base64),APPSHIP_APPLE_ID,APPSHIP_PLAY_JSON_PATHorAPPSHIP_PLAY_JSON, andAPPSHIP_PROFILE. release/release.yml: paths to files in the gitignoredrelease/keys/. Key ID and Issuer ID are not secrets and can be committed.- A profile:
credentials.profile: my-company, pointing to~/.appship/credentials/my-company/.
credentials:
ios:
key_id: ABC123DEFG
issuer_id: 69a6de7a-0000-0000-0000-000000000000
key_path: release/keys/AuthKey_ABC123DEFG.p8
apple_id: you@example.com # optional
android:
json_key_path: release/keys/play-service-account.json
If a key gets committed
- Revoke it right away. iOS: App Store Connect → Integrations → Revoke. Android: Cloud Console → the service account → Keys → Delete.
- Create a new key and update your profile or CI secrets.
- Run
git rm --cached <file>. The key stays in git history, which is why revoking it is the step that matters.