Wrap xcodebuild with structured output and logs

Wrap xcodebuild with structured output and logs

Wrap xcodebuild with structured output and logs

In about 10 minutes you’ll install FlowDeck, launch the interactive TUI (which walks you through trial activation on first run), open or scaffold a project, build and run it, and wire up AI agent skills. The whole loop, driven from the keyboard. No Xcode UI.

In about 10 minutes you’ll install FlowDeck, launch the interactive TUI (which walks you through trial activation on first run), open or scaffold a project, build and run it, and wire up AI agent skills. The whole loop, driven from the keyboard. No Xcode UI.

Before you start

This guide assumes:

  • FlowDeck is installed and your trial is active. If not, follow the getting-started guide first.

  • You have at least one existing xcodebuild script you'd like to migrate. A fastlane lane that shells out to xcodebuild works too.

  • The script currently runs (even if grudgingly).

By the end you'll have a side-by-side translation of your existing commands, a smoke-tested replacement, and a switch-over plan that keeps xcodebuild available as a fallback the whole time.

Audit your current xcodebuild commands

List every place your project shells out to xcodebuild, xcrun simctl, xcrun devicectl, or log stream. Common locations:

  • A scripts/ or tools/ directory in your repo.

  • Your CI config (.github/workflows/, bitrise.yml, Jenkinsfile, etc.).

  • A Fastfile with sh "xcodebuild ..." calls.

  • Build phases inside Xcode that shell out.

  • Makefiles, justfiles, or npm scripts in mixed-repo projects.

For each call site, write down what it does in plain English. "Build for CI release on iPhone 16 simulator." "Run the smoke test suite." "Boot a simulator and wait for it." The plain-English version is your migration target.

Translate command by command

Use this table as a starting point. The patterns are deliberately mechanical so the migration can happen one line at a time.

What you're doing

xcodebuild / xcrun

FlowDeck

Discover schemes

xcodebuild -list -workspace App.xcworkspace

flowdeck context --json

Build for a simulator

xcodebuild build -workspace ... -scheme ... -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0'

flowdeck build -S "iPhone 16"

Run a single test

-only-testing:MyAppTests/LoginTests/testValidLogin

--only LoginTests/testValidLogin

Skip a slow test

-skip-testing:MyAppTests/SlowTests

--skip SlowTests

Boot a simulator

UDID=$(xcrun simctl list ...); xcrun simctl boot "$UDID"

flowdeck simulator boot "iPhone 16"

Stream logs from one app

xcrun simctl spawn booted log stream --predicate 'subsystem contains "com.myapp"'

flowdeck logs <app-id>

Clean derived data

xcodebuild clean + rm -rf ~/Library/Developer/Xcode/DerivedData

flowdeck clean --all

Parse output

Pipe through a regex, hope subsystem strings didn't change

Add --json, read typed events

This is not a one-to-one feature map. xcodebuild archive, xcodebuild -exportArchive, and the signing-and-distribution pipeline don't have FlowDeck equivalents, those operations are out of scope. Keep xcodebuild for them.

Run both in parallel and compare

Before switching anything, run the FlowDeck command alongside the existing xcodebuild call. This catches surprises while you still have a rollback.

# Existing script (unchanged)
xcodebuild build -workspace App.xcworkspace -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16'

# New, side-by-side
flowdeck build -w App.xcworkspace -s App -S "iPhone 16" --json \
  > /tmp/flowdeck-build.ndjson
# Existing script (unchanged)
xcodebuild build -workspace App.xcworkspace -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16'

# New, side-by-side
flowdeck build -w App.xcworkspace -s App -S "iPhone 16" --json \
  > /tmp/flowdeck-build.ndjson
# Existing script (unchanged)
xcodebuild build -workspace App.xcworkspace -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16'

# New, side-by-side
flowdeck build -w App.xcworkspace -s App -S "iPhone 16" --json \
  > /tmp/flowdeck-build.ndjson

Compare the exit code, the time taken, and the failure mode if anything goes wrong. The FlowDeck command should be at least as fast (often faster, because scheme discovery is native instead of a xcodebuild -list hit) and the exit code should match.

If output differs (FlowDeck reports a warning your CI ignored, or your CI flags an error FlowDeck rolled up), it's almost always FlowDeck being more precise. Investigate before assuming it's wrong.

Save a project config to eliminate flag repetition

One of the wins of migrating is that you stop passing the same -workspace / -scheme / -destination on every command. FlowDeck reads from a per-project config:

flowdeck config set \
  -w App.xcworkspace \
  -s App \
  -S "iPhone 16" \
  -C Debug
flowdeck config set \
  -w App.xcworkspace \
  -s App \
  -S "iPhone 16" \
  -C Debug
flowdeck config set \
  -w App.xcworkspace \
  -s App \
  -S "iPhone 16" \
  -C Debug

After that, flowdeck build, flowdeck run, flowdeck test, and the rest pick up the saved values. The config is stored in .flowdeck/ in your project root. CI commits it (so jobs use the same configuration); developers either commit it or .gitignore it depending on whether your team standardizes on one scheme.

You can still pass flags on the command line; they override the saved config for that single call. Useful for "build the same scheme but on a different simulator just this once."

Switch over piecemeal

The lowest-risk migration is one job at a time. A typical order:

  1. Local developer scripts first. The build-and-run.sh someone wrote three years ago. Lowest blast radius, easy to roll back.

  2. PR validation builds next. The fastest, most-run CI job. Catches problems quickly because it runs on every push.

  3. Nightly regression runs after that. Once PR jobs are stable, move the nightly suite. Volume is lower; observation period is longer.

  4. Release builds last. Or not at all. If your release job uses xcodebuild archive and -exportArchive, leave it on xcodebuild. Releases are not the place to swap out tooling.

Keep the previous script in version control with a date in the filename (build.legacy.sh, workflow-2026-03.yml) until you've shipped through a full release cycle on the new tooling. The cost of keeping it is small. The cost of needing it and not having it is large.

If something breaks, roll back fast

The migration is designed so any single job can revert to raw xcodebuild without disturbing the rest. Concrete recipe:

  1. Revert the one file. If a PR validation job fails after the swap, restore the build.legacy.sh (or workflow-2026-03.yml) you kept around. Jobs are independent; the local dev script and the nightly job keep using FlowDeck.

  2. Capture the diff. Before reverting, save the FlowDeck output: flowdeck build --json > /tmp/failed-build.ndjson. The structured events make it obvious where FlowDeck and xcodebuild diverged. Almost always a different scheme, configuration, or simulator selection, not a real build difference.

  3. File an issue with the NDJSON attached. FlowDeck issues with structured output attached are resolved faster than "the build failed."

Once the migrated job is back on FlowDeck and stable for a release cycle (PRs, nightly, release), the legacy script can be deleted. Not before.

Convert text parsers to JSON consumers

If your CI has a script that parses xcodebuild output to detect failures or count warnings, replace it. The new version is shorter and won't break across Xcode versions:

# Before: a regex that breaks when xcodebuild changes its output
xcodebuild ... | grep "error:" | wc -l

# After: structured filtering with jq
flowdeck build --json \
  | jq 'select(.type == "error") | .file + ":" + (.line|tostring)'
# Before: a regex that breaks when xcodebuild changes its output
xcodebuild ... | grep "error:" | wc -l

# After: structured filtering with jq
flowdeck build --json \
  | jq 'select(.type == "error") | .file + ":" + (.line|tostring)'
# Before: a regex that breaks when xcodebuild changes its output
xcodebuild ... | grep "error:" | wc -l

# After: structured filtering with jq
flowdeck build --json \
  | jq 'select(.type == "error") | .file + ":" + (.line|tostring)'

The same pattern works for tests:

flowdeck test --json \
  | jq 'select(.type == "test_failed") | .name'
flowdeck test --json \
  | jq 'select(.type == "test_failed") | .name'
flowdeck test --json \
  | jq 'select(.type == "test_failed") | .name'

And for build summaries you can dump to a CI artifact:

flowdeck build --json > build-events.ndjson
# Attach build-events.ndjson as a CI artifact on failure
flowdeck build --json > build-events.ndjson
# Attach build-events.ndjson as a CI artifact on failure
flowdeck build --json > build-events.ndjson
# Attach build-events.ndjson as a CI artifact on failure

Stable schemas mean these parsers don't break the next time Apple changes a string in xcodebuild's output.

Patterns that come up during migration

Keep xcodebuild for archive and export.
FlowDeck doesn't wrap xcodebuild archive or -exportArchive. Release pipelines that produce a signed .ipa should stay on raw xcodebuild. There's no benefit to migrating them.

Use --verbose when debugging a migration.
FlowDeck summarizes output by default. If you're comparing against a xcodebuild baseline and the FlowDeck output looks suspiciously short, pass --verbose to see the underlying xcodebuild stream. Useful for catching cases where your old script depended on a warning the summary suppressed.

Compose with fastlane, don't replace it.
If you have a Fastfile that orchestrates lanes, the right move is usually to keep the Fastfile and swap the sh "xcodebuild ..." calls inside lanes for sh "flowdeck ...". Fastlane keeps its orchestration role; FlowDeck replaces the manual xcodebuild plumbing.

Pin the FlowDeck version in CI.
Same way you pin Xcode in CI. The install script accepts a --version argument, so you can pin to a known build for reproducibility: curl -sSL https://flowdeck.studio/install.sh | sh -s -- --version <tag>.

When things go wrong

FlowDeck “succeeds” but produces no output where xcodebuild did

The build is slower than expected on CI

A test that passed in xcodebuild fails in FlowDeck (or vice versa)

Some xcodebuild flag isn’t supported

The signing setup breaks under FlowDeck