Build, sign, and ship Capacitor apps from CI with the right toolchain for the Capacitor major in use.
When to Use
TRIGGER when:
- Creating or fixing
.github/workflows/*.yml,.gitlab-ci.yml,fastlane/Fastfile, or a Capgo Cloud Build job. - Picking runner images, Node/Xcode/JDK versions for Capacitor 8 or 9.
- Automating signing, TestFlight/Play uploads, version bumps, or Capgo OTA uploads.
- A CI build fails while a local build works.
Do not use for:
- Manual submission, listing, screenshots ->
capacitor-app-store. - Capgo Cloud Build credential setup and troubleshooting ->
capgo-native-builds. - Channel promotion, staged OTA rollouts ->
capgo-release-workflows. - Writing unit/E2E tests ->
capacitor-testing. - Security scanning rules ->
capacitor-security.
Live Project Snapshot
Capacitor version, Node engine, and scripts:
!node -e "const fs=require('fs');if(!fs.existsSync('package.json'))process.exit(0);const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const out=[];if(pkg.engines)out.push('engines='+JSON.stringify(pkg.engines));if(pkg.packageManager)out.push('packageManager='+pkg.packageManager);for(const section of ['dependencies','devDependencies']){for(const [name,version] of Object.entries(pkg[section]||{})){if(name.startsWith('@capacitor/')||name==='@capgo/cli'||name==='@capgo/capacitor-updater')out.push(section+'.'+name+'='+version)}}for(const [name,cmd] of Object.entries(pkg.scripts||{})){if(/build|test|lint|sync|release/i.test(name))out.push('scripts.'+name+'='+cmd)}console.log(out.join('\n'))"
Existing CI, lockfiles, and native build files:
!find . -maxdepth 4 -not -path '*/node_modules/*' \( -path './.github/workflows/*' -o -name '.gitlab-ci.yml' -o -name 'Fastfile' -o -name 'Appfile' -o -name 'Matchfile' -o -name 'ExportOptions.plist' -o -name 'Podfile' -o -name 'Package.swift' -o -name 'gradle-wrapper.properties' -o -name 'variables.gradle' -o -name '.nvmrc' -o -name '.node-version' -o -name 'bun.lock' -o -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' \)
Toolchain Matrix
Capacitor 8.x (stable, latest) |
Capacitor 9 (next, pre-GA) |
|
|---|---|---|
| Node | 22+ | 24+ (ships npm 11) |
| Xcode | 26+ (27 OK on 8.5+ with UIScene adopted) | 27+ |
| GitHub macOS runner | macos-26 (default Xcode 26.x) |
xcode-27 image (preview at time of writing) or a macos-* image that lists Xcode 27; check actions/runner-images |
| iOS deployment target | 15.0 | 16.0 |
| JDK | 21 | 21 (AGP 9 itself needs 17+, Capacitor's Android library compiles with Java 21) |
| Gradle / AGP | 8.14.3 / 8.13 | 9.5.1 / 9.2.1 |
| compile/target SDK | 36 | 37 |
Store-side deadlines: Apple requires the iOS 26 SDK now and the iOS 27 SDK from April 2027; Google Play requires target API 36 since Aug 31, 2026. A Capacitor 8 app on macos-26 meets both today.
Pin Xcode explicitly when several are installed: sudo xcode-select -s /Applications/Xcode_<version>.app (exact folder names are listed in the runner image README).
Choose a Pipeline Shape
Ask the user when unclear:
- Capgo Cloud Build - no macOS runner or local signing setup; any Linux runner calls
npx @capgo/cli@latest build request <appId> --platform ios|android. Credentials are saved once withnpx @capgo/cli@latest build credentials save ...(orbuild initfor guided iOS setup) and passed in CI as env vars. Loadcapgo-native-buildsfor details. - Self-hosted GitHub Actions / GitLab - full control; you manage macOS runners, certificates, and keystores. See
references/github-actions.md,references/gitlab-ci.md. - Fastlane inside either - best when match/pilot/supply are already in use. See
references/fastlane.md.
Web-only changes after a native release can skip native builds entirely: upload a Capgo bundle (npx @capgo/cli@latest bundle upload --channel <channel>, auth via CAPGO_TOKEN). Native changes (plugins, permissions, Capacitor upgrades) always need a native build.
Procedure
- Read the snapshot: Capacitor major, package manager + lockfile, iOS dependency manager (
Package.swift-> SPM,Podfile-> CocoaPods), existing workflows. - Pick toolchain versions from the matrix. Set Node from
.nvmrc/enginesif present. - Structure jobs:
test(lint, typecheck, unit) ->build-web(artifact<webDir>) ->ios+androidin parallel -> deploy jobs gated on branch/tag. Always runnpx cap sync <platform>on the native job after restoring the web artifact; never commit synced assets as a CI shortcut. - Signing: follow
references/signing-and-secrets.md. Prefer App Store Connect API keys over Apple ID passwords; keep Android keystore and passwords in secrets, decode at runtime, delete after. - Build numbers: derive from CI (
GITHUB_RUN_NUMBER,CI_PIPELINE_IID) or let Capgo Cloud Build bump them. Never reuse a build number. - Add caching (
references/caching-and-versioning.md). - Run the pipeline on a branch first, uploading to TestFlight internal / Play internal track only.
Verification
- The workflow passes on a clean runner twice in a row (cache cold and warm).
- Logs show the expected tool versions: add
node -v,xcodebuild -version,java -version,./gradlew --versionsteps. - iOS artifact:
unzip -l App.ipa | grep Payload/App.appand the build appears in TestFlight with the expected build number. - Android artifact:
bundletool dump manifest --bundle app-release.aab | grep -E "versionCode|targetSdkVersion"matches expectations; build appears in the Play internal track. - Capgo:
npx @capgo/cli@latest bundle listshows the uploaded version on the intended channel. - No secret values printed in logs (GitHub masks registered secrets; never
echodecoded files).
Error Handling
| Error in CI log | Fix |
|---|---|
error ... The engine "node" is incompatible with this module. Expected version ">=22.0.0" (or >=24) |
Bump actions/setup-node node-version to match the Capacitor major |
Could not find the web assets directory: ./dist. |
Web artifact not downloaded to webDir, or build skipped; fix artifact path |
No signing certificate "iOS Distribution" found |
Certificate not imported into the active keychain, or wrong team; see signing reference |
"App" requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor. |
Install the profile, or use -allowProvisioningUpdates with API-key auth flags |
error: exportArchive: No profiles for 'com.x.y' were found |
Profile type mismatch with ExportOptions.plist method (app-store-connect) |
xcodebuild: error: SDK "iphoneos27.0" cannot be located / Swift 6 errors on @UIApplicationMain |
Wrong Xcode selected; xcode-select the required version; replace @UIApplicationMain with @main |
Unsupported class file major version 65 / Android Gradle plugin requires Java 17 to run / invalid source release: 21 |
Install JDK 21 with actions/setup-java (distribution: temurin, java-version: '21') |
Plugin [id: 'org.jetbrains.kotlin.android'] ... conflicts after Capacitor 9 upgrade |
AGP 9 has built-in Kotlin; remove the kotlin-android plugin per capacitor-app-upgrades |
SDK location not found. Define a valid SDK location with an ANDROID_HOME |
Use an image with the Android SDK (ubuntu-latest has it) or android-actions/setup-android |
Play API: APK specifies a version code that has already been used. |
Derive versionCode from the CI run number |
Capgo: Cannot find API key in local folder or global, please login first |
Export CAPGO_TOKEN from secrets in that step's env (or pass -a) |
Capgo: Cannot auth user with apikey |
Key revoked or lacks upload permission; create a new key in the Capgo dashboard |
npm error 404 Not Found - GET https://registry.npmjs.org/capsec |
Security scanner package is @capgo/capgo-sec: npx @capgo/capgo-sec@latest scan --ci |
References
references/github-actions.md- complete workflow: test, web build, iOS (SPM/CocoaPods), Android, Capgo OTA, Capgo Cloud Build job.references/gitlab-ci.md- GitLab pipeline with macOS runner tags.references/fastlane.md- iOS and Android lanes with API keys and match.references/signing-and-secrets.md- certificate/profile import, API key auth, Android keystore wiring, secret inventory.references/caching-and-versioning.md- npm/Gradle/SPM/CocoaPods caches, build numbers, semantic-release.