Integration guide · v0.1.0

Add appship to your project

From an app that has never been on the stores to a build in App Review. Plan for about an hour the first time, most of it spent in the Apple and Google consoles getting keys.

Early release. v0.1.0 is covered by tests and dry-runs but has not been run against the live stores yet. Every command that writes to a store supports --dry-run; start with read-only commands such as appship status.

1Requirements

You needNotes
Node.js 22 or newerappship is an npm package.
fastlaneappship runs fastlane for every store call. Install it the way the fastlane docs recommend for your machine.
macOS with XcodeOnly for iOS builds and uploads. Android-only projects also work on Linux.
Apple Developer Program99 USD a year.
Google Play Console account25 USD, one-time.
An app that builds a signed releaseappship does not sign for you. See Signing.

2Install

Install appship once per machine, globally. Nothing gets added to your app's package.json.

terminal
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

  1. In App Store Connect, open Users and Access → Integrations → App Store Connect API.
  2. Under Team Keys, click +. Name it appship and give it the App Manager role.
  3. Download AuthKey_XXXXXXXXXX.p8. Apple lets you download it only once.
  4. 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

  1. In Google Cloud Console, pick a project and enable the Google Play Android Developer API.
  2. Go to IAM & Admin → Service Accounts → Create service account. It needs no GCP role.
  3. Open the account, then Keys → Add key → JSON to download the key file.
  4. In Play Console, go to Users and permissions → Invite new users and invite the client_email from the JSON. Grant Release apps to testing tracks, Release to production and Manage store presence.
  5. 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.

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

terminal
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:

project layout
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:

release/release.yml
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:

terminal
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>/*.txt and release/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.png 512×512, featureGraphic.png 1024×500, and at least 2 images in phoneScreenshots/.

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.

appship doctor
▸ 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

terminal
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 from questionnaire.yml ready to copy.

On Google Play, follow the checklist to:

  1. Create the app in Play Console.
  2. Invite the service account to the app.
  3. Upload the first .aab by hand to Internal testing.
  4. 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:

terminal
appship metadata --android
appship upload
appship submit

8Every release after

Bump the version and build number in your project, then:

terminal
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:

terminal
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):

SecretValue
APPSHIP_ASC_KEY_IDKey ID
APPSHIP_ASC_ISSUER_IDIssuer ID
APPSHIP_ASC_KEYOutput of base64 -i AuthKey_XXX.p8
APPSHIP_PLAY_JSONOutput of base64 -i play-service-account.json
Signing secretsDepends 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:

.github/workflows/release.yml
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-run before 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 signingConfigs in Gradle, and turn on Play App Signing with the first upload. Keep the keystore out of git, for example in the gitignored release/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.

  1. Environment variables: APPSHIP_ASC_KEY_ID, APPSHIP_ASC_ISSUER_ID, APPSHIP_ASC_KEY_PATH or APPSHIP_ASC_KEY (contents, plain or base64), APPSHIP_APPLE_ID, APPSHIP_PLAY_JSON_PATH or APPSHIP_PLAY_JSON, and APPSHIP_PROFILE.
  2. release/release.yml: paths to files in the gitignored release/keys/. Key ID and Issuer ID are not secrets and can be committed.
  3. A profile: credentials.profile: my-company, pointing to ~/.appship/credentials/my-company/.
release/release.yml, without a profile
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

  1. Revoke it right away. iOS: App Store Connect → Integrations → Revoke. Android: Cloud Console → the service account → Keys → Delete.
  2. Create a new key and update your profile or CI secrets.
  3. Run git rm --cached <file>. The key stays in git history, which is why revoking it is the step that matters.