Guide

Magic Deploy Setup — Google Play

Where to find the service account JSON, package name, keystore, and track LingCode needs to upload your Android app to Google Play.

What you're filling in

When you click Magic Deploy in LingCode and choose Google Play, you'll see up to seven fields. The first three are required; the four signing fields are only needed if your build.gradle doesn't already configure a release signing config.

FieldWhat it isWhere to get it
Service account JSONPrivate key file Google generates when you create a service account.Play Console → Setup → API access → Create service account → Download JSON
Package nameYour app's applicationId, e.g. com.example.app.Must match the applicationId in app/build.gradle.
TrackWhere the build lands: internal, alpha, beta, or production.Pick inside LingCode. Defaults to internal.
Keystore fileA .jks or .keystore file that signs the release AAB.Generated once with keytool (or already exists in your project).
Store passwordPassword for the keystore file itself.Set when you ran keytool -genkeypair.
Key aliasName of the key inside the keystore, e.g. release.Set when you ran keytool -genkeypair.
Key passwordPassword for that specific key.Set when you ran keytool -genkeypair (often the same as the store password).

Two in-app shortcuts save you time. Magic Deploy parses the service-account JSON as a preflight check — if the file is malformed or missing client_email / private_key, you'll know in a second instead of waiting for the OAuth phase to fail opaquely. And the Auto-bump versionCode checkbox next to the fields rewrites versionCode in app/build.gradle before each build, which eliminates the most common second-deploy failure ("Version code X has already been used"). The toggle is off by default — tick it the first time you deploy.

Why does Google use service accounts instead of API keys? A plain API key is a single secret that lets the bearer do anything the account can do — if it leaks, you rotate it and pray. Google's service accounts are real identities in Google Cloud's IAM system: you grant them narrow permissions (e.g. "upload builds to this one app, on internal testing only") and revoke or regenerate their keys independently. The JWTOAuth 2.0 flow LingCode runs with them trades that JWT for a one-hour access token — so even if the token leaks, it expires fast. Unfamiliar term? See the glossary.

Before you start — one-time Play Console setup

Three things have to exist in Google Play Console before a service account can upload anything. Skipping any of them will result in confusing errors later.

  1. A Google Play developer account. Register at play.google.com/console/signup. There is a one-time $25 fee. Use the Google account you want to own the app long-term — transferring ownership later is possible but painful.
  2. An app record in Play Console. In Play Console, click Create app, fill in the name, default language, app vs. game, and free vs. paid. The package name you enter here must match the applicationId in app/build.gradle exactly and can never be changed afterward.
  3. At least one AAB uploaded by hand. Play refuses service-account uploads until a human has uploaded at least one signed build. Build a signed AAB locally (./gradlew bundleRelease) and drag-drop it into Play Console → Testing → Internal testing → Create new release, once. You don't have to release it — just uploading unlocks the API path.

Skipping the first manual upload is the #1 cause of "everything works but nothing shows up" bugs. The API will happily accept the upload and commit the edit, and you'll still see no build in Play Console. Upload once by hand first.

Your Android project must have these settings

A correct Play Console side is useless if ./gradlew bundleRelease doesn't produce the AAB LingCode expects. Verify each of these before the first deploy.

applicationId in app/build.gradle

This is Android's equivalent of a bundle identifier, and it must match the package name you typed in Play Console exactly. Once the app is live, this value can never change — Google treats a different applicationId as a completely different app.

android {
    defaultConfig {
        applicationId "com.example.app"
        // …
    }
}

versionCode and versionName

Both live in defaultConfig. versionCode is an integer that Play uses to order uploads; versionName is the human-facing string.

defaultConfig {
    versionCode 42        // must be greater than every previous upload
    versionName "1.2.3"   // shown to users in the Play Store
}

Every upload must bump versionCode. Re-uploading the same versionCode is the single most common reason a second deploy fails silently — Play rejects it server-side with "Version code X has already been used."

minSdkVersion and compileSdkVersion

Play Console enforces a targetSdkVersion floor that rises yearly (typically API 33+ today). If your project's targetSdkVersion is below the current floor, the upload is accepted but the release is blocked from rollout. Check developer.android.com/google/play/requirements/target-sdk for the current minimum.

A bundleRelease gradle task

LingCode runs ./gradlew bundleRelease and expects the output at app/build/outputs/bundle/release/app-release.aab. This works out of the box for standard Android Studio projects. If you renamed the app module, if you use product flavors, or if you have a multi-module project where the uploadable AAB is produced elsewhere, you'll need to adjust:

A Gradle wrapper (gradlew)

LingCode prefers ./gradlew over the system gradle. If your project doesn't have one, add one:

gradle wrapper --gradle-version 8.5

(Pick whatever version your project is known to build with.) Using the wrapper insulates you from whatever gradle happens to be on your PATH and matches what CI / teammates use.

Step 1 — Create a service account and JSON key

The service account is what LingCode authenticates as when it talks to Google Play. You create it inside Play Console — not inside the Google Cloud Console, even though service accounts are a Cloud concept.

  1. Sign in at play.google.com/console.
  2. In the left sidebar, click Setup → API access.
  3. Under Service accounts, click Create new service account. Play Console opens a dialog linking to Google Cloud; follow it, create the service account (any name — "LingCode Magic Deploy" is fine), then come back to the Play Console tab and click Done.
  4. Back on the API access page, find your new service account in the list and click Manage Play Console permissions (or Grant access).
  5. On the App permissions tab, click Add app and pick the app you registered earlier.
  6. On the Account permissions tab, enable at least Release apps to testing tracks and Release apps to production. Save.
  7. Back in Google Cloud Console, open the service account's Keys tab, click Add key → Create new key → JSON, and download the file. Save it somewhere you back up.

In LingCode, click Browse… next to Service account JSON and pick the downloaded file.

The JSON key file is the equivalent of a password. Don't commit it to git, don't paste it in Slack. If it leaks, delete the key from the Cloud Console — the service account keeps working, it just gets a new key.

Step 2 — Pick a release track

The Track dropdown decides where Play puts the upload. LingCode defaults to internal, which is the fastest and safest choice while you're testing Magic Deploy itself.

You can always promote a build from internal to a higher track later from inside Play Console without re-uploading.

Step 3 — Configure release signing

Google Play only accepts signed AABs. You have two ways to get there — pick whichever fits your existing workflow.

Option A — Sign inside build.gradle (recommended if you already have a keystore)

Add a signingConfigs.release block to your app/build.gradle:

android {
    signingConfigs {
        release {
            storeFile file("/absolute/path/to/release.keystore")
            storePassword "…"
            keyAlias "release"
            keyPassword "…"
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            // …
        }
    }
}

If you go this route, leave the four keystore fields in LingCode blank. LingCode just runs ./gradlew bundleRelease and lets gradle read the signing config from the build file.

Option B — Let LingCode inject the keystore at build time

If you don't want to hard-code passwords into build.gradle, have your build file read them from gradle properties:

android {
    signingConfigs {
        release {
            storeFile file(project.findProperty("RELEASE_STORE_FILE") ?: "release.keystore")
            storePassword project.findProperty("RELEASE_STORE_PASSWORD") ?: ""
            keyAlias project.findProperty("RELEASE_KEY_ALIAS") ?: ""
            keyPassword project.findProperty("RELEASE_KEY_PASSWORD") ?: ""
        }
    }
    buildTypes {
        release { signingConfig signingConfigs.release }
    }
}

Then fill in all four keystore fields in LingCode. At build time LingCode passes them to gradle as -PRELEASE_STORE_FILE=…, -PRELEASE_STORE_PASSWORD=…, -PRELEASE_KEY_ALIAS=…, -PRELEASE_KEY_PASSWORD=…. The values never leave your Mac except as arguments to the local gradle process.

Don't have a keystore yet?

Generate one with the JDK's keytool:

keytool -genkeypair -v \
  -keystore release.keystore \
  -alias release \
  -keyalg RSA -keysize 2048 \
  -validity 10000

Save the file somewhere you back up. If you lose this keystore you can never update the app on Play again — Google uses its signature to prove new versions are really "from you." Back it up in at least two places.

Step 4 — Click Deploy

With the service account JSON, package name, and signing all in place, the Deploy to Google Play button activates. LingCode then:

On success the build shows up in Play Console → Testing → your track → Releases within a minute or two (Google's processing time, not LingCode's). From there you can roll it out, promote it to another track, or submit it for review.

Reading the progress log

While Magic Deploy runs, the right-hand panel streams the live output of gradlew and every HTTP call LingCode makes to Google's servers. Lines prefixed with [Google Play] are LingCode's own markers; everything else is verbatim tool output.

A successful run looks like this end-to-end:

[Google Play] Building signed AAB...
> Task :app:bundleRelease
BUILD SUCCESSFUL in 45s
[Google Play] Getting access token...
[Google Play] Creating edit...
[Google Play] Uploading AAB...
[Google Play] Committing edit...
[Google Play] Upload complete. Check Google Play Console for the new build.

When it fails, scroll to the bottom of the log. LingCode's short error messages ("Build failed. Check errors above.") are pointers; the real reason is in the last 20 lines of raw output — usually the line that starts with FAILURE: (gradle) or an HTTP status code (Google API).

Per-phase troubleshooting

Failures group cleanly by phase. Fixing each one looks different.

Build phase (gradlew bundleRelease)

Log containsWhyFix
No Android project found LingCode didn't find a build.gradle or build.gradle.kts at the open folder's root. Open the folder that contains gradlew, not its parent. If you're inside a monorepo, temporarily open the Android subdir directly.
Permission denied: ./gradlew The wrapper script isn't executable. chmod +x gradlew in the project root.
Could not find method signingConfig() Gradle syntax mismatch — you mixed Groovy and Kotlin DSL snippets. Use the syntax that matches your file extension: build.gradle is Groovy, build.gradle.kts is Kotlin.
Keystore file '…' not found The keystore path in Magic Deploy, or the one referenced by RELEASE_STORE_FILE, doesn't resolve on disk. Use the absolute path to the .jks or .keystore. Relative paths are resolved from the gradle module dir, which can surprise you.
Failed to read key <alias> from store: Keystore was tampered with, or password was incorrect Either the store password or the key password is wrong. In Terminal, run keytool -list -keystore release.keystore — it'll prompt for the store password and confirm whether it's right.
No key with alias "…" found The key alias doesn't exist in the keystore. keytool -list -keystore release.keystore shows all aliases. Copy the exact one into the Key alias field.
AAB not found at app/build/outputs/bundle/release/app-release.aab The gradle build succeeded but didn't produce an AAB at the expected path — usually because your app module is named something other than app, or a product flavor changed the output filename. Rename the module to app, or move the AAB into that path via a gradle Copy task. For flavor-based builds, run ./gradlew bundle<Flavor>Release manually for now.
Execution failed for task ':app:lintVitalRelease' A lint issue flagged as an error by your gradle config blocks release builds. Either fix the lint issue, or temporarily add android.lintOptions.checkReleaseBuilds false to unblock (and open a ticket to fix properly).
Java heap space / OutOfMemoryError Gradle daemon is too small for your project. In gradle.properties, add org.gradle.jvmargs=-Xmx4g.

OAuth phase (JWT → access token)

Log containsWhyFix
OAuth failed: invalid_grant The JWT LingCode signed with the service account private key was rejected. Usually the JSON file is malformed or from the wrong project. Re-download the JSON key from the service account's Keys tab. Don't edit the file — opening it in some editors adds invisible characters.
OAuth failed: invalid_scope The service account doesn't have the androidpublisher API enabled in Cloud Console. In Cloud Console → APIs & Services → Library, search for Google Play Android Developer API and enable it for the service account's project.
OAuth failed: 401 Unauthorized The service account's private key was revoked or deleted. Generate a new JSON key from the service account's Keys tab. Attach it in Magic Deploy.

Create Edit phase

Log containsWhyFix
Create edit failed: 404 applicationNotFound The packageName doesn't exist in Play Console — either a typo or the app was never created. Create the app record in Play Console first (Create app), using the exact applicationId from build.gradle.
Create edit failed: 403 developerDoesNotOwnApplication The service account isn't linked to this app in Play Console. In Play Console → Setup → API access → your service account → Manage Play Console permissions → App permissions → Add app, and pick this app.
Create edit failed: 403 insufficientScopes The service account's Account permissions are too limited. Same page, Account permissions tab — enable at least "Release apps to testing tracks." Admin role works too.

Upload AAB phase

Log containsWhyFix
Upload failed: 403 apkNotificationMessageKeyUpgradeVersionConflict / versionCodeTooLow The versionCode in your AAB is not greater than one already on Play. Bump versionCode in app/build.gradle and re-run — or tick Auto-bump versionCode in the pane so LingCode does it for you on every deploy.
Upload failed: 400 apkUploadApkNotSigned The AAB isn't signed (or is signed with a debug key). Either fill in all four keystore fields in Magic Deploy, or configure signingConfigs.release in build.gradle. Don't do both — they can conflict.
Upload failed: 400 apkSignedWithDifferentCertificate The AAB was signed with a key different from the one Google has recorded for this app. Either sign with the original keystore, or if you've lost it, request a key reset via Play Console → Setup → App signing → Request upload key reset (Google reviews these manually).
Upload failed: 413 Request Entity Too Large Your AAB is bigger than the 150 MB AAB limit. Enable R8/ProGuard minification, prune large drawables or fonts, or ship assets via Play Asset Delivery.
Upload failed: network error / timed out Slow or flaky connection; LingCode has a 600s timeout on the upload call. Retry. If you've uploaded the same AAB a few times, Play's deduplication usually short-circuits.

Commit Edit phase

Log containsWhyFix
Commit failed: 400 validationError Play rejected the edit during validation — often missing required fields on the app record (content rating, privacy policy URL, target audience). Open the app in Play Console and complete the red-dotted sections in Policy and programs and App content. The upload already succeeded; you can commit the edit manually from Play Console after fixing.
Commit failed: 409 editAlreadyCommitted A previous attempt already committed this edit; your retry has nothing to do. Safe to ignore. Check Play Console — the build is probably there.

What happens after a successful upload

When Magic Deploy finishes, the build lands in Play Console → your app → Testing → <your track> → Releases. Unlike App Store Connect, there's usually no separate "processing" state for AABs — the build is available within a minute or two. From there:

You can promote any build from a lower track to a higher one from inside Play Console without re-uploading — go to the release page for the target track and click Create new release → Add from library.

Quick reference: where to go when something's wrong

SymptomWhere to fix
Deploy button stays disabledPackage name or service account JSON is missing. LingCode requires both before enabling the button.
"No Android project found"Open the folder that contains gradlew / build.gradle, not its parent.
Build phase failsRun the same ./gradlew bundleRelease in Terminal with your -P props (Magic Deploy prints the exact command). Gradle's error will be identical and easier to debug outside LingCode.
AAB built but not foundYour module isn't named app, or flavor builds are producing a different filename. Rename or symlink into app/build/outputs/bundle/release/app-release.aab.
OAuth / 401 errorsRe-download the service account JSON key and re-attach in Magic Deploy.
403 developerDoesNotOwnApplicationPlay Console → Setup → API access → your service account → App permissions → Add this app.
versionCodeTooLowBump versionCode in app/build.gradle. Any larger integer works.
Upload succeeds, nothing in Play ConsoleYou haven't done the first manual upload yet. Build an AAB locally and drag it into Play Console → Internal testing → Create new release, once.
Commit fails with validationErrorOpen Play Console, resolve the red dots in Policy and programs / App content, then commit the edit manually (it's already uploaded).
Still stuckCopy the exact gradle command from the log and run it in Terminal; for API errors, hit the same endpoint with curl and the access token to see the full response body.

Common mistakes (quick checklist)

Security notes

Both the service account JSON and the keystore are credential material. LingCode never uploads either anywhere except to oauth2.googleapis.com and androidpublisher.googleapis.com — i.e. Google itself — and only as part of the deploy you trigger. Both files stay on your Mac otherwise.

If you lose a laptop or a teammate leaves, rotate the service account key (delete the old one in Cloud Console → IAM → Service accounts → Keys and generate a new one) and, if the keystore itself was in a compromised backup, stop using that app for new releases — Google does not allow rotating an app's signing key without their explicit involvement via Play App Signing.

Further reading

Primary sources for the concepts on this page, from Google and the IETF:

Ready to ship?

Shipping an iOS or Mac app too? See the App Store Connect API key guide. Or browse the full Magic Deploy feature set.

Download LingCode for Mac