Walkthrough
Step-by-step tour from project init to a successful TestFlight upload.
On this page
ShipItSwifty Walkthrough
A step-by-step guide to setting up and running your first release.
Guided setup
Build the public 0.1.0 release from source, or audit every line before running it.
git clone https://github.com/ShipItSwifty/shipitswifty.gitcd shipitswiftygit checkout 0.1.0swift build -c release# Binary at .build/release/shipit
Run shipit generate to inspect your project and scaffold a Shipfile.yml in seconds. It walks through the missing answers, detects your scheme and bundle ID, and writes a reviewable config you can commit.
shipit generate# Or target a specific release flowshipit generate --goal beta# Machine-readable output for CI or agentsshipit generate --goal beta --non-interactive --output json
Check that Xcode, credentials, and profiles are all in order before running a build.
shipit envshipit doctor
Dry-run first to preview steps. Then run for real.
# Preview without executingshipit run beta --dry-run# Ship itshipit run beta --ci
Generate produces a stable, reviewable config. Edit it once if you need to, then commit it. Every CI run after that is just shipit run.
Every command is non-interactive by default. Export credentials as environment variables and set --ci to disable TTY prompts.
# GitHub Actions- run: shipit run beta --cienv:ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
Detailed walkthrough
Prerequisites
- macOS 15+, Swift 6 / Xcode 16+
- An Apple Developer account
- (For ASC features) An App Store Connect API key
Step 1 — Clone and build
git clone https://github.com/shipitswifty/shipitswifty
cd shipitswifty
git checkout 0.1.0
swift buildStep 2 — Create your config file
Use generate as the default setup path. It walks you through the missing project details, auto-detects what it can from Xcode, and writes Shipfile.yml for you.
shipit generateIf you already know the release goal, target it explicitly:
shipit generate --goal betaFor non-interactive use in CI or agent flows:
shipit generate --goal beta --non-interactive --output jsonThe generated result includes:
appConfig— every inferred value withsource,confidence, andwhymissing— required fields that couldn't be resolved, with env var namesambiguities— fields where multiple candidates exist and confirmation is neededreadiness— blockers, missing secrets, and signing risk levelnextAction.command— the exact command to run nextshipfilePath— where the generated config was written
If your project is ambiguous (e.g., multiple runnable schemes), resolve those first, then re-run to get the create_shipfile action.
If you want to hand the project off to an agent after generation, ai-session is still available as a secondary, agent-focused command:
shipit ai-session --goal beta --output jsonManual setup is still available if you prefer to start from the example file:
cp Shipfile.example.yml Shipfile.ymlShipfile.yml is only the default filename. You can rename it and pass --shipfile <path> on every command.
Config-backed commands require the file to exist. If you rename the file, pass the same --shipfile <path> on env, doctor, run, and any action command.
Edit your config file to match your project:
app:
workspace: MyApp.xcworkspace
scheme: MyApp
bundle_id: com.example.myapp
team_id: ABCDE12345Step 3 — Set App Store Connect credentials
For actions that call the App Store Connect API, you need ASC_KEY_ID, ASC_ISSUER_ID, and exactly one of ASC_PRIVATE_KEY or ASC_PRIVATE_KEY_PATH.
ASC_ISSUER_ID is shown on the App Store Connect API Keys page. It is not stored in the .p8 file.
If you only plan to use local build flows such as build, test, archive, or export, you can skip this step.
Where the Apple values come from:
| Value | Where to find it |
|---|---|
team_id | Usually auto-detected by shipit generate from Xcode signing settings. If you need to set it manually, use the 10-character Apple Developer Team ID shown in the Apple Developer account or in Certificates, IDs & Profiles for the selected team. |
ASC_KEY_ID | App Store Connect -> Users and Access -> Integrations -> App Store Connect API -> API key Key ID |
ASC_ISSUER_ID | Same App Store Connect API page. This is account-level metadata, not part of the downloaded .p8 file. |
ASC_PRIVATE_KEY_PATH | Local filesystem path to the .p8 file you downloaded when creating the API key |
ASC_PRIVATE_KEY | Raw contents of that same .p8 file, typically stored directly in CI secrets |
If team_id is missing and you are unsure which one to use, open your Xcode project Signing settings and use the team selected for the app target. That should match the Team ID ShipIt needs.
Find the values in App Store Connect:
- Open
App Store Connect. - Go to
Users and Access. - Open
Integrations. - Open
App Store Connect API. - Create or select an API key.
- Copy
Key IDintoASC_KEY_ID. - Copy
Issuer IDintoASC_ISSUER_ID. - Download the
.p8file.
For local development, point at a .p8 file:
app_store_connect:
key_id: ${ASC_KEY_ID}
issuer_id: ${ASC_ISSUER_ID}
key_path: ./.secrets/AuthKey_XXXXXXXXXX.p8Export the variables in your shell:
export ASC_KEY_ID=XXXXXXXXXX
export ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ASC_PRIVATE_KEY_PATH=./.secrets/AuthKey_XXXXXXXXXX.p8For CI, set ASC_PRIVATE_KEY to the raw .p8 file contents instead of using a file path. Do not set both private key forms unless you intentionally want ASC_PRIVATE_KEY to win.
You only need ASC credentials for commands that talk to Apple services, such as upload, testflight, metadata, and provision.
Step 4 — Verify resolved configuration
swift run shipit envThis prints all resolved values — workspace, scheme, credentials, etc. — so you can confirm everything is wired up before running any actions.
It also prints the selected shipfile path, processed files, and the relevant environment variables that were read during resolution.
If you used a custom filename, run swift run shipit env --shipfile ./path/to/your-config.yml.
Step 5 — Run individual commands
# Compile
swift run shipit build
# Discover available test destinations (simulators and devices)
xcodebuild -showdestinations -scheme MyApp -workspace MyApp.xcworkspace
# Run tests on a specific destination
swift run shipit test
# Archive for App Store
swift run shipit archive
# Export IPA
swift run shipit export
# Push to TestFlight
swift run shipit testflightTest destinations:
shipit testrequires adestinationslist inShipfile.yml(or--destinationflag). It never falls back to a hardcoded simulator name. Usexcodebuild -showdestinationsto find valid destination strings for your scheme.
Step 6 — Define workflows for repeatable releases
ShipItSwifty uses Apple's two-version model:
| Plist key | Format | Role |
|---|---|---|
CFBundleShortVersionString | MAJOR.MINOR.PATCH | User-facing marketing version. Bump manually for releases. |
CFBundleVersion | plain integer | Internal build counter. Auto-incremented on every beta run. |
Add workflows to Shipfile.yml. The version step with bump: build increments only CFBundleVersion and leaves CFBundleShortVersionString untouched — the correct behavior for beta runs.
Local-only beta (no upload to App Store Connect):
versioning:
strategy: sequential # keeps CFBundleVersion as a plain integer
source: xcodeproj
workflows:
beta:
- action: version
options: { bump: build } # bumps CFBundleVersion only
- action: archive
- action: export # exports .ipa locally; no ASC credentials neededLocal workflow with tests (run before archive):
versioning:
strategy: sequential
source: xcodeproj
workflows:
local:
- action: test
options:
destinations:
- "platform=iOS Simulator,id=<simulator-udid>"
# Run `xcodebuild -showdestinations -scheme MyApp` to get valid destination strings.
# Multiple destinations are supported — one xcodebuild test pass per destination.
- action: archive
- action: exportBeta with TestFlight upload (requires app_store_connect credentials):
workflows:
beta:
- action: version
options: { bump: build }
- action: archive
options: { configuration: Release, export_method: app-store }
- action: export
- action: testflight
options:
groups: ["Internal QA"]
changelog: "Bug fixes and improvements"Release (bumps marketing version, submits for review):
workflows:
release:
- action: version
options: { bump: patch } # bumps CFBundleShortVersionString patch segment
- action: archive
- action: export
- action: upload
options: { submit_for_review: true, phased_release: true }Then run a workflow:
swift run shipit run beta
swift run shipit run release --ciMigrating from fastlane
If you already have a Fastfile, the easiest migration path is to translate each lane into a Shipfile.yml workflow and move shared config into the top-level sections.
Concept mapping
| fastlane | ShipItSwifty |
|---|---|
Appfile | app: + app_store_connect: in Shipfile.yml |
Fastfile lane | workflows: entry |
| lane parameters / env vars | workflow options: + SHIPIT_* env vars |
match | shipit sign sync |
gym | shipit archive + shipit export |
scan | shipit test |
pilot | shipit testflight |
deliver | shipit metadata and shipit upload |
increment_build_number / increment_version_number | shipit version |
Typical migration steps
- Move app identity from
AppfileintoShipfile.yml:
app:
workspace: MyApp.xcworkspace
scheme: MyApp
bundle_id: com.example.myapp
team_id: ABCDE12345
app_store_connect:
key_id: ${ASC_KEY_ID}
issuer_id: ${ASC_ISSUER_ID}
key_path: ./.secrets/AuthKey_XXXXXXXXXX.p8- Convert each lane into a workflow. For example, this fastlane lane:
lane :beta do
increment_build_number
build_app
upload_to_testflight(groups: ["Internal QA"])
endbecomes (with TestFlight upload):
workflows:
beta:
- action: version
options: { bump: build }
- action: archive
options: { configuration: Release, export_method: app-store }
- action: export
- action: testflight
options:
groups: ["Internal QA"]Or, for a local-only beta that skips the App Store Connect upload:
versioning:
strategy: sequential # ensures CFBundleVersion stays a plain integer
source: xcodeproj # reads/writes directly in .xcodeproj
workflows:
beta:
- action: version
options: { bump: build } # bumps CFBundleVersion only; marketing version unchanged
- action: archive
- action: export # exports .ipa locally; no ASC credentials needed- Move secrets out of Ruby config and into environment variables:
export ASC_KEY_ID=XXXXXXXXXX
export ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ASC_PRIVATE_KEY="$(cat ./.secrets/AuthKey_XXXXXXXXXX.p8)"- Replace lane invocations with
shipitcommands:
# fastlane beta
swift run shipit run beta
# fastlane test
swift run shipit test
# fastlane deliver
swift run shipit metadata
swift run shipit upload- Validate the migration before using it in CI:
# Human-readable check
shipit env
shipit run beta --dry-run
# Machine-readable check (agents/CI)
shipit generate --goal beta --non-interactive --output json
shipit run beta --dry-run --output jsonIf you renamed the file, include --shipfile ./path/to/your-config.yml on both commands.
Migration advice
- Start by migrating one lane, usually your beta/TestFlight flow, before moving release automation.
- Keep workflows small and explicit. ShipItSwifty favors separate
archive,export,testflight, anduploadsteps over one large command. - If your fastlane setup uses
match, migrate signing separately and verifyshipit sign syncbefore wiring it into a full release workflow. - CI usually gets simpler: keep credentials in standard env vars and call
swift run shipit run <workflow> --ci --output json.
Dry Run Mode
Preview what a command or workflow would do without executing anything:
swift run shipit run release --dry-runFor structured output that agents and CI scripts can parse, combine with --output json:
swift run shipit run release --dry-run --output jsonThis returns the full step list with options, not mixed human text:
{
"action": "run",
"status": "dry_run",
"payload": {
"workflow": "release",
"mode": "dry_run",
"stepCount": 4,
"steps": [
{ "index": 1, "action": "version", "options": { "bump": "build" } },
{ "index": 2, "action": "archive", "options": null },
{ "index": 3, "action": "export", "options": null },
{ "index": 4, "action": "upload", "options": { "submit_for_review": true } }
]
}
}If you want colored human output in screenshots, demos, or piped logs, force it explicitly:
swift run shipit run release --dry-run --color always
swift run shipit doctor --no-colorJSON Output
For CI pipelines and downstream tooling, use --output json:
swift run shipit build --scheme MyApp --output json | jq .Color flags only affect human output. JSON output is always plain machine-readable text.
Debugging
Enable verbose logging:
swift run shipit build --verboseRun the doctor to diagnose environment issues:
swift run shipit doctorIf your config is not named Shipfile.yml, pass it explicitly:
swift run shipit doctor --shipfile ./path/to/your-config.ymlCommon Caveats
- The package depends on the remote
SwiftyShellSwift package, so your environment must be able to fetch Swift package dependencies. - Full code-signing (
sign,provision) flows require proper vault configuration (VAULT_PASSWORDand a cert repo). - Some advanced ASC features are still under active development.