Skip to content

Auto Choose Live Update or Native Build

Most Capacitor releases are JavaScript-only and should ship as a live update. Some changes touch native code and need a new binary from Capgo Build. This guide shows how to make GitHub Actions, GitLab CI, or any other CI/CD platform pick the correct path on every push — without a human deciding.

Capgo already knows which path is safe. After your web build (and before you upload or request a native build), run:

Terminal window
npx @capgo/cli@latest bundle releaseType com.example.app --channel production
# → OTA ship with bundle upload
# → native ship with Capgo Build

OTA means the native packages match what is already live on the channel. native means a plugin, Capacitor version, or other native dependency changed — an over-the-air bundle alone cannot update those devices safely.

releaseType compares native package metadata (Capacitor/Cordova plugins and versions). It does not see every edit under ios/, android/, or capacitor.config.*. Gate those paths in git first, then use releaseType for dependency compatibility — the examples below do both.

See Native Compatibility for the full rules and the manual bundle compatibility table.

  • Capgo app registered and a Capgo API key in CI secrets as CAPGO_TOKEN
  • Live Updates upload working (bundle upload) — see CI/CD Integration
  • Capgo Build credentials in CI if you expect native jobs — see GitHub Actions or Credentials
  • A channel name that matches production (examples use production)
  1. Build web assets as usual.
  2. If the commit touches ios/, android/, or capacitor.config.*, force the native path.
  3. Otherwise ask Capgo releaseType whether the commit is OTA-safe.
  4. If OTA, upload the bundle (with --fail-on-incompatible as a safety rail).
  5. If native, run Capgo Build, then upload the matching bundle with --auto-min-update-version so the channel’s native metadata advances (use the metadata strategy). Do not use --fail-on-incompatible for that baseline upload — the new native packages are supposed to differ.

One workflow that gates native paths, then branches on releaseType:

.github/workflows/capgo-release.yml
name: Capgo Release
on:
push:
branches: [main]
jobs:
decide:
runs-on: ubuntu-latest
outputs:
release_type: ${{ steps.verdict.outputs.type }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Decide OTA vs native
id: verdict
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
run: |
BEFORE="${{ github.event.before }}"
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
BEFORE="$(git rev-parse HEAD~1 2>/dev/null || echo '')"
fi
if [ -z "$BEFORE" ] || git diff --name-only "$BEFORE" "${{ github.sha }}" \
| grep -qE '^(ios/|android/|capacitor\.config\.)'; then
TYPE=native
echo "Native path/config changed (or no prior commit) — forcing native"
else
TYPE=$(npx @capgo/cli@latest bundle releaseType com.example.app --channel production | tr -d '[:space:]')
fi
echo "type=$TYPE" >> "$GITHUB_OUTPUT"
echo "Capgo release type: $TYPE"
live_update:
needs: decide
if: needs.decide.outputs.release_type == 'OTA'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Upload live update
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
run: |
npx @capgo/cli@latest bundle upload com.example.app \
--channel production \
--fail-on-incompatible
native_build:
needs: decide
if: needs.decide.outputs.release_type == 'native'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: [ios, android]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npx cap sync ${{ matrix.platform }}
- name: Capgo Build ${{ matrix.platform }}
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
CAPGO_IOS_PROVISIONING_MAP: ${{ secrets.CAPGO_IOS_PROVISIONING_MAP }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
ANDROID_KEYSTORE_FILE: ${{ secrets.ANDROID_KEYSTORE_FILE }}
KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }}
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
PLAY_CONFIG_JSON: ${{ secrets.PLAY_CONFIG_JSON }}
run: |
npx @capgo/cli@latest build request com.example.app \
--platform ${{ matrix.platform }} \
--build-mode release
native_bundle:
needs: [decide, native_build]
if: needs.decide.outputs.release_type == 'native'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Upload bundle for new native baseline
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
run: |
# One-time: channel set production --disable-auto-update metadata
npx @capgo/cli@latest bundle upload com.example.app \
--channel production \
--auto-min-update-version

Replace com.example.app and wire the signing secrets as described in GitHub Actions for Capgo Build.

GitLab evaluates rules when the pipeline is created, so branch with a shell if inside one deploy job (or generate a dynamic child pipeline if you need separate native matrix jobs):

.gitlab-ci.yml
image: node:24
stages:
- build
- deploy
variables:
APP_ID: com.example.app
CHANNEL: production
build_web:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
- node_modules/
expire_in: 1 hour
only:
- main
deploy:
stage: deploy
needs: [build_web]
script:
- |
BEFORE="${CI_COMMIT_BEFORE_SHA:-}"
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
BEFORE="$(git rev-parse HEAD~1 2>/dev/null || echo '')"
fi
if [ -z "$BEFORE" ] || git diff --name-only "$BEFORE" "$CI_COMMIT_SHA" \
| grep -qE '^(ios/|android/|capacitor\.config\.)'; then
TYPE=native
else
TYPE=$(npx @capgo/cli@latest bundle releaseType "$APP_ID" --channel "$CHANNEL" | tr -d '[:space:]')
fi
echo "Capgo release type: $TYPE"
if [ "$TYPE" = "OTA" ]; then
npx @capgo/cli@latest bundle upload "$APP_ID" \
--channel "$CHANNEL" \
--fail-on-incompatible
elif [ "$TYPE" = "native" ]; then
npx cap sync
npx @capgo/cli@latest build request "$APP_ID" --platform ios --build-mode release
npx @capgo/cli@latest build request "$APP_ID" --platform android --build-mode release
npx @capgo/cli@latest bundle upload "$APP_ID" \
--channel "$CHANNEL" \
--auto-min-update-version
else
echo "Unexpected release type: $TYPE" >&2
exit 1
fi
only:
- main

Store CAPGO_TOKEN and Capgo Build signing variables as masked/protected CI/CD variables.

The same three steps work anywhere:

StepCommand
Verdictnpx @capgo/cli@latest bundle releaseType APP_ID --channel production
OTA pathnpx @capgo/cli@latest bundle upload APP_ID --channel production --fail-on-incompatible
Native pathnpx @capgo/cli@latest build request APP_ID --platform ios (or android) --build-mode release

Map the shell exit / stdout into your platform’s conditionals (or keep a single job with a shell if, like GitLab above):

  • Azure Pipelines — set an output variable from a script step, then use condition: eq(variables['releaseType'], 'OTA')
  • Bitbucket Pipelines — write RELEASE_TYPE=… to $BITBUCKET_PIPELINES_VARIABLES_PATH, declare it under output-variables, and branch later steps with condition: state: RELEASE_TYPE == "OTA" (file artifacts alone cannot drive condition)
  • CircleCIwhen is evaluated at config-compile time, so branch with a runtime shell if (or dynamic config / continuation), not a workspace value in when
  • Jenkins — capture stdout into an env var and use when { environment name: 'RELEASE_TYPE', value: 'OTA' }

Path filters are a cost optimization, not a substitute for the Capgo check. Prefer excluding docs-only paths rather than maintaining a fragile allowlist — web builds often also depend on vite.config.*, tsconfig*.json, and framework config files:

on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**'

If you use an allowlist instead, include every input your web and native builds read, not only src/ and package.json.

When CI chooses native:

  1. Capgo Build produces signed binaries and can submit to TestFlight / Play (see configuration).
  2. Upload the matching JS bundle with --auto-min-update-version (metadata strategy) so the channel records the new native packages — otherwise the next JS-only commit still returns native.
  3. Once users install the new binary, later JavaScript-only commits return to OTA again.