Skip to main content

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

.github/workflows/ota.yml
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

.github/workflows/rollout.yml
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

.circleci/config.yml
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

.gitlab-ci.yml
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 --paused for 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 deploy fails, the pipeline should fail.

Next steps