CI/CD Integration
In CI, authenticate with a scoped CI token instead of user auth. Create tokens from the dashboard or via swiftpatch ci-tokens create.
Creating a CI token
# Scoped to one app + one channel
swiftpatch ci-tokens create \
--name "GitHub Actions · production" \
--apps my-app \
--channels production
The token is printed once. Store it as an encrypted CI secret. You cannot retrieve it later — regenerate with swiftpatch ci-tokens regenerate <id> if lost.
Signing keys in CI
Your CI pipeline needs access to your Ed25519 private key (.swiftpatch/keys/private.pem). Store it as an encrypted secret:
- GitHub Actions:
secrets.SWIFTPATCH_PRIVATE_KEY(the full PEM contents). - CircleCI: environment variable.
- Bitrise: secret env var.
Write it to a file in the job, then pass with -k:
- run: echo "$SWIFTPATCH_PRIVATE_KEY" > ./private.pem && chmod 600 ./private.pem
- run: npx swiftpatch-cli deploy -p ios --hermes -k ./private.pem
GitHub Actions
Deploy on tag push
name: OTA Deploy
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: macos-latest
strategy:
matrix:
platform: [ios, android]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- name: Write signing key
env:
SWIFTPATCH_PRIVATE_KEY: ${{ secrets.SWIFTPATCH_PRIVATE_KEY }}
run: |
mkdir -p .swiftpatch/keys
echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem
chmod 600 .swiftpatch/keys/private.pem
- name: Deploy OTA (${{ matrix.platform }})
env:
SWIFTPATCH_CI_TOKEN: ${{ secrets.SWIFTPATCH_CI_TOKEN }}
run: |
npx swiftpatch-cli deploy \
-p ${{ matrix.platform }} \
--hermes \
--rollout 5 \
--release-note "${{ github.event.head_commit.message }}"
Staged rollout over a week
name: Gradual rollout
on:
workflow_dispatch:
inputs:
release_id:
required: true
percent:
required: true
jobs:
rollout:
runs-on: ubuntu-latest
steps:
- run: |
npx swiftpatch-cli releases rollout \
${{ inputs.release_id }} \
--percent ${{ inputs.percent }}
env:
SWIFTPATCH_CI_TOKEN: ${{ secrets.SWIFTPATCH_CI_TOKEN }}
CircleCI
version: 2.1
jobs:
deploy-ota:
docker:
- image: cimg/node:20.10
parameters:
platform: { type: enum, enum: [ios, android] }
steps:
- checkout
- run: npm ci
- run: |
mkdir -p .swiftpatch/keys
echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem
chmod 600 .swiftpatch/keys/private.pem
- run: |
npx swiftpatch-cli deploy \
-p << parameters.platform >> \
--hermes \
--rollout 10
workflows:
ship:
jobs:
- deploy-ota: { platform: ios }
- deploy-ota: { platform: android }
Bitrise
Add a Script step:
export SWIFTPATCH_CI_TOKEN="$SWIFTPATCH_CI_TOKEN"
mkdir -p .swiftpatch/keys
echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem
chmod 600 .swiftpatch/keys/private.pem
npx swiftpatch-cli deploy -p ios --hermes --rollout 10
GitLab CI
ota:deploy:
image: node:20
stage: deploy
only: [tags]
script:
- npm ci
- mkdir -p .swiftpatch/keys
- echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem
- chmod 600 .swiftpatch/keys/private.pem
- npx swiftpatch-cli deploy -p ios --hermes --rollout 10
variables:
SWIFTPATCH_CI_TOKEN: $SWIFTPATCH_CI_TOKEN
JSON output for pipelines
Every command supports --json. Pipe it into downstream steps:
RELEASE=$(npx swiftpatch-cli deploy -p ios --hermes --json | jq -r '.release.id')
echo "Released $RELEASE"
npx swiftpatch-cli status "$RELEASE" --json --timeout 300
Exit codes
0 Success
1 Generic failure
2 Validation error (bad flag, missing arg)
3 Authentication failure (token invalid, expired)
4 Network / API error
5 Signing error (missing key, bad PEM, verification failed)
6 Build / bundle error (Metro crashed, Hermes failed)
Use these to fail the pipeline precisely. For example, retry on 4 (network blips) but fail fast on 3 (auth) or 5 (signing).
Multi-tenant CI (white-label apps)
Deploy to multiple tenant deployment keys from the same pipeline:
strategy:
matrix:
tenant: [acme, contoso, fabrikam]
steps:
- name: Deploy ${{ matrix.tenant }}
env:
SWIFTPATCH_CI_TOKEN: ${{ secrets[format('TOKEN_{0}', matrix.tenant)] }}
run: npx swiftpatch-cli deploy -a ${{ matrix.tenant }}-app -p ios --hermes
See White-label for the runtime side.
Sourcemap uploads
For clean stack traces in crash clusters:
npx swiftpatch-cli deploy -p ios --hermes --sourcemap
The sourcemap is uploaded alongside the bundle and consumed by the crash clustering pipeline.
Best practices
- One token per pipeline. Scope tokens tightly. If a pipeline is only for staging, scope the token to
staging. - Rotate tokens every 90 days.
swiftpatch ci-tokens regenerate <id>— update the secret in your CI provider. - Start rollouts at 5%. Watch the dashboard for a few hours before promoting to 100%.
- Use
--pausedfor scheduled releases. Ship a release in paused state, then promote at the scheduled time via a separate job. - Always pass
--release-note. The release page needs context; your future self will thank you. - Fail on non-zero exit. Don't swallow errors. If
swiftpatch deployfails, the pipeline should fail.
Next steps
- Commands — every flag.
- Staged rollouts — graduated rollout patterns.
- White-label — multi-tenant runtime.