CI Setup
Run shipit on GitHub Actions, Bitrise, and self-hosted runners.
On this page
GitHub Actions
The repository includes two pre-built workflows:
| Workflow | File | Trigger |
|---|---|---|
| Build & Test | .github/workflows/ci.yml | Push / PR to main |
| Build DocC | .github/workflows/docc.yml | Push to main + manual |
The Build & Test workflow runs three jobs in parallel after lint passes:
| Job | Runner | Xcode | Scope |
|---|---|---|---|
build-and-test | macos-26 | 26.4 | Main build, unit tests, CLI tests |
cross-platform-fixture-integration | macos-26 | 26.4 | Blocking Flutter + React Native + KMP fixture integration suites |
build-and-test-linux | ubuntu-latest | — | Skips XcodeBuildKitTests, XcodeGenKitTests, IntegrationTests |
SwiftyShell dependency
ShipItSwifty depends on the remote SwiftyShell Swift package. CI runners must have network and authentication access to fetch Swift package dependencies during swift build and swift test.
Required Secrets
For App Store Connect API actions, CI needs ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY.
ASC_PRIVATE_KEY must be the raw contents of the downloaded .p8 file. ASC_ISSUER_ID comes from the App Store Connect API Keys page, not from the key file.
If your CI only runs local validation like swift test, shipit build, shipit test, shipit archive, or shipit export, you can skip these ASC secrets.
Add these under Settings → Secrets → Actions:
| Secret | Description |
|---|---|
ASC_KEY_ID | App Store Connect API key ID |
ASC_ISSUER_ID | App Store Connect issuer ID |
ASC_PRIVATE_KEY | Raw .p8 key contents (no file path in CI) |
SHIPIT_TEST_P12_BASE64 | Base64-encoded development .p12 for signing integration tests |
SHIPIT_TEST_P12_PASSWORD | Export password for the .p12 above |
SHIPIT_TEST_P12_BASE64 and SHIPIT_TEST_P12_PASSWORD are only needed if you want the signing
integration tests to run in CI. Without them, those tests skip automatically.
To produce the base64 value from a .p12 file:
# Copies the base64-encoded certificate to your clipboard — paste as the secret value
base64 -i MyCert.p12 | pbcopySee CONTRIBUTING.md for full signing credential setup instructions.
Example release workflow
name: Release
on:
workflow_dispatch:
inputs:
workflow:
description: "Workflow to run (beta or release)"
required: true
default: beta
jobs:
release:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Install shipit
run: brew install shipitswifty/tap/shipit
- name: Run workflow
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
run: shipit run ${{ github.event.inputs.workflow }} --ci --output jsonIf your CI stores the config at a non-default path, pass it explicitly:
shipit run beta --ci --shipfile ./config/Shipfile.ci.ymlBasic CI Steps
brew install shipitswifty/tap/shipit
shipit doctor --ci
shipit test --ci
shipit run beta --ci --output jsonJSON Output in CI
Use --output json for machine-readable results:
shipit run beta --ci --output json | jq .statusWriting your own CI script
You have two ways to drive a release from CI. Prefer the first; reach for the second only when a step must live in shell.
Approach 1 — One Shipfile workflow, one command (recommended)
Put the whole pipeline — build, upload, version bump, commit, and tag — in a Shipfile.yml workflow and invoke it with a single shipit run. This is the only way to get commit/tag/push as managed steps, because git is a workflow action, not a standalone CLI subcommand (there is no shipit git).
# Shipfile.yml
workflows:
release:
- action: archive
options: { configuration: Release, export_method: app-store }
- action: export
- action: upload
options: { submit_for_review: true }
- action: version # bump only after upload succeeds
options: { bump: patch }
- action: git
options:
operation: commit # stages all changes (git add -A) then commits
commit_message: "chore: release v{{version}} [skip ci]" # {{version}} from the step above
- action: git
when: "{{version_changed}}" # skipped on build-only bumps
options:
operation: tag
tag_name: "v{{version}}" # resolved from the version step — no shell glue
- action: git
options:
operation: push
push_tags: true#!/usr/bin/env bash
set -euo pipefail
# Git identity for the commit the `git` action will create.
git config user.name "ci-bot"
git config user.email "[email protected]"
shipit doctor --ci
shipit run release --ci --output json | tee result.json
test "$(jq -r '.status' result.json)" = "success"A non-zero exit from shipit run already fails the job; the jq status check is a belt-and-suspenders guard for pipelines that swallow exit codes.
Tagging: the
tagstep derives its name from the version you just bumped via the{{version}}token — no shell parsing or${ENV_VAR}round-trip needed.{{build_number}}is also available (e.g.tag_name: "v{{version}}+{{build_number}}"), and{{version_changed}}lets you skip the tag on build-only bumps withwhen: "{{version_changed}}". See Workflow tokens & step conditions.shipit generate --goal releasescaffolds this whole tail for you.
Approach 2 — Hand-rolled shell script
When you need shell-level control (extra tooling between steps, bespoke conditional logic, or values ShipIt doesn't expose as a token), drive the individual subcommands and parse their JSON. Tagging with the freshly bumped version no longer requires this — use the {{version}} token in Approach 1. Each shipit subcommand emits the envelope { "action", "status", "payload": { … } } and exits non-zero on failure.
#!/usr/bin/env bash
set -euo pipefail
git config user.name "ci-bot"
git config user.email "[email protected]"
shipit doctor --ci
# Build and distribute.
shipit archive --ci --output json
shipit export --ci --output json
shipit upload --ci --output json
# Bump the version, then read the new values straight from the JSON payload.
bump_json="$(shipit version --bump patch --ci --output json)"
new_version="$(jq -r '.payload.version' <<<"$bump_json")"
new_build="$( jq -r '.payload.buildNumber' <<<"$bump_json")"
echo "Bumped to ${new_version} (${new_build})"
# Commit and tag with plain git — there is no `shipit git` subcommand.
# `shipit version` writes the bump to disk; `git add -A` stages it.
git add -A
git commit -m "chore: release ${new_version} [skip ci]"
git tag -a "v${new_version}" -m "Release ${new_version}"
git push origin HEAD --follow-tagsWhy this shape:
set -euo pipefail— fail the script the instant anyshipitstep orjqparse fails, instead of pushing a tag for a build that never uploaded.--output json— every step stays machine-readable so you can gate on.statusor pull values out of.payload.- Bump after upload — if
uploadfails, the version files are never touched, so there is nothing to roll back. See "Version bump ordering and recovery" in the Configuration Reference. --follow-tags— pushes the commit and its annotated tag in one round trip.
Preview without side effects
Add --dry-run to any step to see what it would do without building, uploading, or writing version files:
shipit run release --dry-run --output json # full step list, no execution
shipit version --bump patch --dry-run --output json # computed before/after, nothing writtenLinux Swift Version
The Linux CI job uses swift:6.3.1-noble (Ubuntu 24.04, current stable Swift). To update it, change the container.image value in .github/workflows/ci.yml and the SWIFT_IMAGE variable at the top of the Makefile.
Cross-platform build systems on CI
When build_system resolves to anything other than native, ShipItSwifty layers a pre-build step on top of the regular pipeline. Each build system needs its own toolchain on the runner.
Kotlin Multiplatform
KMP iOS pipelines still run on macOS because they eventually invoke xcodebuild. Android-only KMP workflows can run on Linux.
jobs:
ios-beta:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v3
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle.kts', 'gradle.properties') }}
- run: shipit doctor --ci
- run: shipit run beta-ios --ci --output json
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
android-beta:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v3
- run: shipit run beta-android --ci --output json
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}Notes:
- JDK 17+ is required for modern Android and KMP builds.
- Gradle cache is essential — the first KMP framework link is slow; subsequent runs are nearly free if
~/.gradleis cached. shipit doctor --cifails fast on missing Java, Gradle, or Apple credentials.- If reproducibility matters on macOS runners, pin explicit simulator destinations for iOS test jobs.
Flutter and React Native
Flutter workflows need the Flutter SDK; React Native workflows need Node and the project package manager before shipit runs.
jobs:
flutter-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- run: shipit run beta-android --ci --output json
react-native-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- run: npm ci
- run: shipit run beta-android --ci --output jsonUse macOS runners for Flutter iOS or React Native iOS distribution — both require Xcode and Apple signing tooling.