Section 11

When it breaks

The failures you will actually meet, in rough order of frequency.

Start with leas doctor

Most of the table below is diagnosable before a build ever starts. leas doctor resolves every credential the way a build would, reads what is actually inside them, and calls Apple and Google to confirm the keys work — in about two seconds.

leas doctor                      # everything, including live API checks
leas doctor --offline            # skip the calls to Apple and Google
leas doctor --json               # machine-readable, for CI

It exits non-zero when anything is broken, so it works as a gate in front of an expensive cloud build. The checks worth knowing about:

  • Identifier drift. That app.json, leas.json, and the provisioning profile all name the same app. This is the single most common cause of a failed build, and it is invisible until xcodebuild refuses.
  • Certificate and profile together. A certificate the profile does not list cannot sign against it. doctor compares the fingerprint of the certificate inside your .p12 against the ones the profile embeds.
  • Ad-hoc masquerading as App Store. A profile that provisions specific devices is an ad-hoc profile. Exporting it with app-store fails late and confusingly.
  • Expiry, early. Certificates last three years and profiles one. It warns thirty days out instead of on the morning you need to ship a hotfix.
  • The store APIs, for real. It distinguishes a rejected key (401) from a key with no role granted (403), and opens and immediately abandons a Play edit — the only way to prove the service account was actually granted release permission on that app.
  • The fleet. That the workflows exist and every secret they reference is present on the repository.
What you seeWhat it meansFix
No signing certificate "iOS Distribution" found The certificate expired, was revoked, or belongs to a different team than the profile. Section 8, "Replace an expired iOS distribution certificate."
Provisioning profile doesn't include signing certificate The profile was generated against a certificate you have since replaced. Regenerate the profile against the current certificate and re-upload the secret.
The build hangs at the codesign step and eventually times out macOS is showing an invisible keychain permission prompt on a headless machine. leas handles this with set-key-partition-list. If you see it, the build is not going through leas — check for a stray custom signing script.
altool: Unable to authenticate Wrong Issuer ID, or the .p8 is not where altool expects it. Confirm the Issuer ID matches the team that owns the key. leas installs the file into ~/.appstoreconnect/private_keys/ automatically.
403 · The caller does not have permission The service account exists in Google Cloud but was never granted access in Play Console — or lacks access to this specific app. Setup step 7, second half.
APK signature is invalid / wrong signing key The artifact was re-signed with a keystore Play does not recognise as your upload key. Confirm LEAS_ANDROID_KEYSTORE_BASE64 is the keystore actually registered with this app. If it genuinely is lost, request an upload key reset.
Version code N has already been used Play accepts each version code exactly once, forever. Bump android.versionCode in app.json and rebuild. Never reuse.
SDK location not found ANDROID_HOME is unset on the machine running the build. Set it locally; the workflow's setup-android step handles the cloud.
Published an update and nothing reaches the device The update's runtime version does not match the installed binary's — the most common update problem by a wide margin. leas update:list and compare against the build on the device. Section 6.
App crashes on launch right after an update The JS calls native code the installed binary does not contain. leas update:rollback --embedded immediately, then ship it as a build instead and bump the runtime version.
Assets 404 after an update downloads its manifest The update server was started with the wrong --url, so asset URLs point somewhere devices cannot reach. Restart it with the public base URL and republish nothing — manifests are generated per request.
Cloud build fails but works locally Almost always a secret that is missing, stale, or was pasted with a trailing newline. gh secret list to check what exists, then re-upload the suspect one.
Debugging order that saves the most time

Reproduce locally first. A local build prints the full toolchain error; a cloud build prints a log you have to go find and read. If it works locally and fails in the cloud, the problem is a secret — not your code.