skip to content

Store Deployment

deliver and supply push builds, release notes, and metadata to App Store Connect and Google Play, including phased and staged rollouts. Expect a question about authenticating with API keys rather than a human account, since that is what makes unattended releases possible.

on this pageshow

explore

questions

5

How does a fastlane upload lane authenticate to App Store Connect and to Google Play without a human login?

level: juniorimportance: must knowfreq 78%

answer

  1. both stores want a machine credential
  2. no human account in the lane
  3. Apple issues an API key
  4. Google Play uses a service account
  5. json_key_data suits a string secret

basics

~20 s

Apple and Google use different credentials. upload_to_app_store takes an App Store Connect API key through api_key or api_key_path; upload_to_play_store takes a Google service-account JSON key through json_key or json_key_data. There is no shared credential model.

solid answer

~40 s

Each store gets a machine identity, and the two are unrelated. On Apple's side, `upload_to_app_store` accepts an **App Store Connect API key** — `api_key_path` points at a JSON file holding the key's identifiers and private key text, or `api_key` takes the same material as a hash already loaded in the lane. On Google's side, `upload_to_play_store` accepts a **service-account JSON key** — `json_key` for a file path, `json_key_data` for the file's contents as a string, which is what you want when the CI secret store holds strings rather than files. `upload_to_app_store` still offers `username` for an Apple ID, and that is the path to avoid: a human account drags a two-factor challenge and an expiring session into the pipeline. `upload_to_play_store` has no username option at all.

code

ruby · 16 lines
ruby
lane :android_release do
  upload_to_play_store(
    package_name: 'com.llamatrek.booking',
    json_key_data: ENV['PLAY_SERVICE_ACCOUNT_JSON'],
    aab: './build/llamatrek.aab',
    track: 'production'
  )
end

lane :ios_release do
  upload_to_app_store(
    app_identifier: 'com.llamatrek.booking',
    api_key_path: ENV['ASC_API_KEY_PATH'],
    ipa: './build/LlamaTrek.ipa'
  )
end

go deeper

for a junior

Recall the two names cleanly: App Store Connect API key for the Apple upload lane, Google service-account JSON key for the Play upload lane. Being able to say they are unrelated already answers the question.

for a middle

Explain the option pairs and when each is right: api_key_path versus api_key on Apple's side, json_key versus json_key_data on Google's, and why the in-memory form suits a string-only secret store.

for a senior

Demonstrate how you keep those secrets out of the repository and the logs, how you scope each identity, and how a rotation is planned rather than discovered during a release night.

for a principal

Own the estate view: who may issue store credentials, how they are scoped and rotated, and what the blast radius is when one leaks or is revoked across every pipeline that publishes.

## Why a human login cannot drive an unattended release A store upload is an authenticated API call, and the only real question is whose identity it carries. If the release lane authenticates as a person, then everything that protects that person also blocks the pipeline: an interactive two-factor challenge, a session that expires, a rotated password, and eventually the fact that the person leaves the team. `upload_to_app_store` still accepts an Apple ID through `username`, and that is exactly the path that strands a llama-trekking booking app release overnight, waiting for a code nobody is awake to type. The fix on both stores is the same idea and two completely different implementations: issue a credential that belongs to the **automation** rather than to a human. There is **no shared credential model** — the formats, the option names and the issuing systems all differ. ## Apple: the App Store Connect API key `upload_to_app_store` (`deliver`) reads Apple credentials from these options: - `api_key_path` — a path to a JSON file that carries the key's identifiers together with the private key text. This is the usual CI form. - `api_key` — the same material as a hash already prepared inside the lane, so nothing has to be written to disk. - `username` — an Apple ID. The interactive path, kept for humans running the lane locally. - `team_id` and `team_name` — which team the upload belongs to, when the identity can see more than one. The key is a secret with a scope: it is issued with a role, so an upload can fail on permission rather than on authentication, and revoking or rotating it breaks every lane that uses it at once. ## Google: the service-account JSON key `upload_to_play_store` (`supply`) reads Google credentials from these options: - `json_key` — a path to the service-account JSON key file. - `json_key_data` — the contents of that file as a string, ideal when the credential arrives from an environment variable. - `package_name` — the Android application id the service account is acting on. Note the asymmetry that trips people up: the action has **no `username` option**. There is no human-account path on this side at all; a service account is the only identity Google Play's upload lane will use. ## The two credentials side by side | | iOS — `upload_to_app_store` | Android — `upload_to_play_store` | |---|---|---| | Credential kind | App Store Connect API key | Google service-account JSON key | | File option | `api_key_path` | `json_key` | | In-memory option | `api_key` | `json_key_data` | | Human fallback | `username` (an Apple ID) | none | | Scoping option | `team_id`, `team_name` | `package_name` | The same split shows up wherever fastlane talks to a store: Apple's tools take `api_key`/`api_key_path`, Google's take `json_key`/`json_key_data`. Neither key is convertible into the other, and a lane that ships both platforms needs both secrets. ## Handling the keys in CI 1. Keep both keys out of the repository. A committed service-account key or Apple private key is a store-publishing credential sitting in everyone's clone. 2. Prefer the in-memory option where one exists: `json_key_data` from an environment variable means the Play key never touches the job's disk. 3. When a file is unavoidable — Apple's `api_key_path` — write it from the secret at the start of the job and delete it at the end, inside the job's own workspace. 4. Rotate deliberately. Because one key serves every lane, plan the swap rather than discovering it when a release night fails. 5. Give the credential the narrowest role that still lets the upload succeed. ## Failure modes worth recognising - The lane hangs instead of failing: almost always an Apple ID `username` path asking for a two-factor code. - Authentication succeeds but the upload is refused: the identity is real but has not been granted access to that app, or its role is too narrow. - A key pasted into a single-line environment variable loses the line breaks of its private key text and no longer parses. - Someone points the Apple key at `json_key`, or the Google key at `api_key_path`, and gets an unhelpful parse error rather than a clear message. ## What a machine credential does not buy you It removes the human from the **login**, not from the release. Store review still happens on the stores' terms, and the declarations a submission requires still have to come from somewhere — either from options set on the action or from a person. Machine credentials make an unattended release possible; they do not make it automatic.

  • Your CI secret store holds only strings, not files. How do you give `upload_to_play_store` its Google service-account key?
    Pass the whole JSON blob as `json_key_data`, read from the environment, so the credential never touches disk. If some step genuinely needs a path, write the secret to a file inside the job workspace and delete it in a cleanup step. Never commit the key or its contents, and never echo it into build logs.
  • What does an App Store Connect API key not solve compared with an Apple ID login?
    It is still a scoped secret. The key is issued with a role, so an upload can fail on permission rather than on credentials, and revoking or rotating it breaks every lane that uses it at once. It also does not remove store review; it only removes the interactive login from the lane.
  • Can the same credential serve the iOS and the Android upload lane?
    No. An App Store Connect API key authenticates only to Apple through `api_key`/`api_key_path`, and a Google service-account JSON key only to Google Play through `json_key`/`json_key_data`. A pipeline that ships both platforms carries two independent secrets with two independent rotation stories.

An App Store Connect API key is a badge issued to the build machine itself. An Apple ID is a person's badge lent to the machine, and it is still the person who gets the two-factor prompt at three in the morning.

saying these in an interview costs you the question

  • Putting an Apple ID password into the CI job configuration
  • Thinking one credential covers App Store Connect and Google Play
  • Believing upload_to_play_store accepts a username and password
  • Committing the service-account JSON key into the repository
  • Saying two-factor prompts are fine because CI can retry
  • Assuming a machine credential removes store review
open as a page

In fastlane, which action uploads an iOS build to App Store Connect and which uploads an Android build to Google Play?

level: juniorimportance: should knowfreq 70%

basics

~10 s

fastlane's upload_to_app_store, nicknamed deliver, sends an iOS build and its listing metadata to App Store Connect. upload_to_play_store, nicknamed supply, sends an Android APK or AAB to Google Play. Both spellings work in a Fastfile.

open as a page

In fastlane, which upload_to_app_store and upload_to_play_store options release a llama-trekking booking app on iOS and Android with nobody at a console?

level: seniorimportance: should knowfreq 54%

basics

~20 s

On Apple's side upload_to_app_store needs submit_for_review, force to skip the confirmation prompt, and the submission and review information options; automatic_release, auto_release_date and phased_release control the release itself. On Google's side upload_to_play_store needs track, release_status and rollout.

open as a page

In fastlane, where do upload_to_app_store and upload_to_play_store read iOS and Android release notes from?

level: middleimportance: nice to knowfreq 42%

basics

~20 s

Both read a metadata folder set by metadata_path, but the layouts differ. upload_to_app_store reads a per-locale release_notes entry and also accepts the text inline as an option; upload_to_play_store reads per-locale changelog files named after the Android version code.

open as a page

In fastlane, which upload_to_app_store and upload_to_play_store options name an iOS or Android release version?

level: middleimportance: nice to knowfreq 46%

basics

~20 s

On Apple's side upload_to_app_store uses app_version and build_number to choose the App Store Connect version and the build attached to it. On Google's side upload_to_play_store uses version_name and the integer version_code that Play orders releases by.

open as a page