CI/CD for Capacitor Applications Automate building, testing, and deploying Capacitor apps. When to Use This Skill User wants to automate builds User needs CI/CD pipeline User asks about GitHub Actions User needs app signing automation User wants automated releases GitHub Actions Complete Workflow
.github/workflows/build.yml
name : Build and Deploy on : push : branches : [ main , develop ] pull_request : branches : [ main ] env : NODE_VERSION : '20' jobs :
Run tests and linting
test : runs-on : ubuntu - latest steps : - uses : actions/checkout@v4 - uses : oven - sh/setup - bun@v1 - name : Install dependencies run : bun install - name : Lint run : bun run lint - name : Type check run : bun run typecheck - name : Unit tests run : bun test - - coverage - name : Upload coverage uses : codecov/codecov - action@v4
Security scan
security : runs-on : ubuntu - latest steps : - uses : actions/checkout@v4 - uses : oven - sh/setup - bun@v1 - run : bunx capsec scan - - ci
Build web assets
build-web : runs-on : ubuntu - latest needs : [ test , security ] steps : - uses : actions/checkout@v4 - uses : oven - sh/setup - bun@v1 - name : Install dependencies run : bun install - name : Build run : bun run build - name : Upload build artifacts uses : actions/upload - artifact@v4 with : name : web - build path : dist/
Build iOS
build-ios : runs-on : macos - latest needs : build - web steps : - uses : actions/checkout@v4 - uses : oven - sh/setup - bun@v1 - name : Download web build uses : actions/download - artifact@v4 with : name : web - build path : dist/ - name : Install dependencies run : bun install - name : Sync Capacitor run : bunx cap sync ios - name : Setup Ruby uses : ruby/setup - ruby@v1 with : ruby-version : '3.2' bundler-cache : true working-directory : ios/App - name : Install CocoaPods run : cd ios/App && pod install - name : Import certificates env : CERTIFICATE_P12 : $ { { secrets.CERTIFICATE_P12 } } CERTIFICATE_PASSWORD : $ { { secrets.CERTIFICATE_PASSWORD } } PROVISIONING_PROFILE : $ { { secrets.PROVISIONING_PROFILE } } run : |
Create keychain
security create-keychain -p "" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "" build.keychain security set-keychain-settings -t 3600 -u build.keychain
Import certificate
echo "$CERTIFICATE_P12" | base64 - - decode
certificate.p12 security import certificate.p12 - k build.keychain - P "$CERTIFICATE_PASSWORD" - T /usr/bin/codesign security set - key - partition - list - S apple - tool : , apple : , codesign : - s - k "" build.keychain
Install provisioning profile
mkdir
p ~/Library/MobileDevice/Provisioning\ Profiles echo "$PROVISIONING_PROFILE" | base64 - - decode
~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision
name : Build iOS run : | cd ios/App xcodebuild -workspace App.xcworkspace \ -scheme App \ -configuration Release \ -archivePath build/App.xcarchive \ archive - name : Export IPA run : | cd ios/App xcodebuild -exportArchive \ -archivePath build/App.xcarchive \ -exportPath build/ \ -exportOptionsPlist ExportOptions.plist - name : Upload IPA uses : actions/upload - artifact@v4 with : name : ios - build path : ios/App/build/ *.ipa
Build Android
build-android : runs-on : ubuntu - latest needs : build - web steps : - uses : actions/checkout@v4 - uses : oven - sh/setup - bun@v1 - name : Download web build uses : actions/download - artifact@v4 with : name : web - build path : dist/ - name : Setup Java uses : actions/setup - java@v4 with : java-version : '17' distribution : 'temurin' - name : Setup Android SDK uses : android - actions/setup - android@v3 - name : Install dependencies run : bun install - name : Sync Capacitor run : bunx cap sync android - name : Decode keystore env : KEYSTORE_BASE64 : $ { { secrets.KEYSTORE_BASE64 } } run : | echo "$KEYSTORE_BASE64" | base64 --decode > android/app/release.keystore - name : Build APK env : KEYSTORE_PASSWORD : $ { { secrets.KEYSTORE_PASSWORD } } KEY_ALIAS : $ { { secrets.KEY_ALIAS } } KEY_PASSWORD : $ { { secrets.KEY_PASSWORD } } run : | cd android ./gradlew assembleRelease \ -Pandroid.injected.signing.store.file=release.keystore \ -Pandroid.injected.signing.store.password=$KEYSTORE_PASSWORD \ -Pandroid.injected.signing.key.alias=$KEY_ALIAS \ -Pandroid.injected.signing.key.password=$KEY_PASSWORD - name : Build AAB env : KEYSTORE_PASSWORD : $ { { secrets.KEYSTORE_PASSWORD } } KEY_ALIAS : $ { { secrets.KEY_ALIAS } } KEY_PASSWORD : $ { { secrets.KEY_PASSWORD } } run : | cd android ./gradlew bundleRelease \ -Pandroid.injected.signing.store.file=release.keystore \ -Pandroid.injected.signing.store.password=$KEYSTORE_PASSWORD \ -Pandroid.injected.signing.key.alias=$KEY_ALIAS \ -Pandroid.injected.signing.key.password=$KEY_PASSWORD - name : Upload APK uses : actions/upload - artifact@v4 with : name : android - apk path : android/app/build/outputs/apk/release/ .apk - name : Upload AAB uses : actions/upload - artifact@v4 with : name : android - aab path : android/app/build/outputs/bundle/release/ .aab
Deploy to Capgo
deploy-capgo : runs-on : ubuntu - latest needs : build - web if : github.ref == 'refs/heads/main' steps : - uses : actions/checkout@v4 - uses : oven - sh/setup - bun@v1 - name : Download web build uses : actions/download - artifact@v4 with : name : web - build path : dist/ - name : Deploy to Capgo run : bunx @capgo/cli upload env : CAPGO_TOKEN : $ { { secrets.CAPGO_TOKEN } }
Deploy to App Store
deploy-ios : runs-on : macos - latest needs : build - ios if : github.ref == 'refs/heads/main' steps : - uses : actions/checkout@v4 - name : Download IPA uses : actions/download - artifact@v4 with : name : ios - build path : build/ - name : Upload to App Store Connect env : APP_STORE_CONNECT_API_KEY : $ { { secrets.APP_STORE_CONNECT_API_KEY } } run : | xcrun altool --upload-app \ --type ios \ --file build/*.ipa \ --apiKey ${{ secrets.API_KEY_ID }} \ --apiIssuer ${{ secrets.API_ISSUER_ID }}
Deploy to Play Store
deploy-android : runs-on : ubuntu - latest needs : build - android if : github.ref == 'refs/heads/main' steps : - uses : actions/checkout@v4 - name : Download AAB uses : actions/download - artifact@v4 with : name : android - aab path : build/ - name : Upload to Play Store uses : r0adkll/upload - google - play@v1 with : serviceAccountJsonPlainText : $ { { secrets.PLAY_SERVICE_ACCOUNT } } packageName : com.yourapp.id releaseFiles : build/ *.aab track : internal Fastlane Integration
.github/workflows/fastlane.yml
name : Fastlane Build on : push : tags : [ 'v*' ] jobs : ios : runs-on : macos - latest steps : - uses : actions/checkout@v4 - uses : ruby/setup - ruby@v1 with : ruby-version : '3.2' bundler-cache : true - name : Install Fastlane run : gem install fastlane - name : Build and Deploy run : fastlane ios release env : MATCH_PASSWORD : $ { { secrets.MATCH_PASSWORD } } APP_STORE_CONNECT_API_KEY : $ { { secrets.APP_STORE_CONNECT_API_KEY } } android : runs-on : ubuntu - latest steps : - uses : actions/checkout@v4 - uses : ruby/setup - ruby@v1 with : ruby-version : '3.2' bundler-cache : true - name : Install Fastlane run : gem install fastlane - name : Build and Deploy run : fastlane android release env : PLAY_STORE_JSON_KEY : $ { { secrets.PLAY_SERVICE_ACCOUNT } } Fastlane Setup iOS Fastfile
ios/App/fastlane/Fastfile
default_platform ( :ios ) platform :ios do desc "Build and deploy to TestFlight" lane :release do setup_ci
Match for code signing
match ( type : "appstore" , readonly : true )
Increment build number
increment_build_number ( build_number : ENV [ "GITHUB_RUN_NUMBER" ] )
Build
build_app ( workspace : "App.xcworkspace" , scheme : "App" , export_method : "app-store" )
Upload to TestFlight
upload_to_testflight ( skip_waiting_for_build_processing : true ) end desc "Build for development" lane :build do match ( type : "development" , readonly : true ) build_app ( workspace : "App.xcworkspace" , scheme : "App" , export_method : "development" ) end end Android Fastfile
android/fastlane/Fastfile
default_platform ( :android ) platform :android do desc "Build and deploy to Play Store" lane :release do
Increment version code
increment_version_code ( version_code : ENV [ "GITHUB_RUN_NUMBER" ] . to_i )
Build AAB
gradle ( task : "bundle" , build_type : "Release" )
Upload to Play Store
upload_to_play_store ( track : "internal" , aab : lane_context [ SharedValues :: GRADLE_AAB_OUTPUT_PATH ] ) end desc "Build APK" lane :build do gradle ( task : "assemble" , build_type : "Release" ) end end GitLab CI
.gitlab-ci.yml
stages : - test - build - deploy variables : BUN_VERSION : "1.0" .bun-cache : &bun-cache cache : key : $ { CI_COMMIT_REF_SLUG } paths : - node_modules/ - ~/.bun/install/cache test : stage : test image : oven/bun : $ { BUN_VERSION } << : bun-cache script : - bun install - bun run lint - bun test - - coverage coverage : '/All files[^|]|[^|]\s+([\d.]+)/' artifacts : reports : coverage_report : coverage_format : cobertura path : coverage/cobertura - coverage.xml security : stage : test image : oven/bun : $ { BUN_VERSION } script : - bunx capsec scan - - ci - - output json - - output - file security.json artifacts : reports : security : security.json build-web : stage : build image : oven/bun : $ { BUN_VERSION } << : bun-cache script : - bun install - bun run build artifacts : paths : - dist/ expire_in : 1 day build-ios : stage : build tags : - macos needs : [ build - web ] script : - bun install - bunx cap sync ios - cd ios/App && fastlane build artifacts : paths : - ios/App/build/ .ipa only : - main - tags build-android : stage : build image : thyrlian/android - sdk needs : [ build - web ] script : - bun install - bunx cap sync android - cd android && ./gradlew assembleRelease artifacts : paths : - android/app/build/outputs/apk/release/ .apk only : - main - tags deploy-capgo : stage : deploy image : oven/bun : $ { BUN_VERSION } needs : [ build - web ] script : - bunx @capgo/cli upload - - channel production only : - main environment : name : production Secrets Management Required Secrets Secret Description CERTIFICATE_P12 iOS distribution certificate (base64) CERTIFICATE_PASSWORD Certificate password PROVISIONING_PROFILE iOS provisioning profile (base64) KEYSTORE_BASE64 Android keystore (base64) KEYSTORE_PASSWORD Keystore password KEY_ALIAS Signing key alias KEY_PASSWORD Signing key password CAPGO_TOKEN Capgo API token APP_STORE_CONNECT_API_KEY App Store Connect API key PLAY_SERVICE_ACCOUNT Play Store service account JSON Encoding Secrets
iOS certificate
base64 -i certificate.p12 | pbcopy
iOS provisioning profile
base64 -i profile.mobileprovision | pbcopy
Android keystore
base64 -i release.keystore | pbcopy Version Management Semantic Release bun add -D semantic-release @semantic-release/git @semantic-release/changelog // .releaserc.json { "branches" : [ "main" ] , "plugins" : [ "@semantic-release/commit-analyzer" , "@semantic-release/release-notes-generator" , "@semantic-release/changelog" , [ "@semantic-release/npm" , { "npmPublish" : false } ] , [ "@semantic-release/git" , { "assets" : [ "package.json" , "CHANGELOG.md" ] , "message" : "chore(release): ${nextRelease.version}" } ] , "@semantic-release/github" ] } Version Bumping
.github/workflows/release.yml
name : Release on : push : branches : [ main ] jobs : release : runs-on : ubuntu - latest steps : - uses : actions/checkout@v4 with : fetch-depth : 0 persist-credentials : false - uses : oven - sh/setup - bun@v1 - run : bun install - name : Release env : GITHUB_TOKEN : $ { { secrets.GITHUB_TOKEN } } run : bunx semantic - release Build Caching Gradle Cache - name : Cache Gradle uses : actions/cache@v4 with : path : | ~/.gradle/caches ~/.gradle/wrapper key : gradle - $ { { runner.os } } - $ { { hashFiles(' /.gradle' , ' /gradle-wrapper.properties') } } restore-keys : gradle - $ { { runner.os } } - CocoaPods Cache - name : Cache CocoaPods uses : actions/cache@v4 with : path : ios/App/Pods key : pods - $ { { runner.os } } - $ { { hashFiles('ios/App/Podfile.lock') } } restore-keys : pods - $ { { runner.os } } - Resources GitHub Actions: https://docs.github.com/actions Fastlane: https://fastlane.tools Capgo CLI: https://capgo.app/docs/cli App Store Connect API: https://developer.apple.com/documentation/appstoreconnectapi Google Play API: https://developers.google.com/android-publisher