Slash commands that save hours: /build /test /perf /release-notes for iOS
Turn repetitive iOS workflows into reliable one-liners. Define a small set of slash commands that run the right builds, tests, and checks, and generate release notes without ceremony.
Most iOS teams do the same things all week:
- build the app
- run a targeted test slice
- check performance regressions
- prepare release notes
The waste is not the work. The waste is the variation.
One person runs the wrong scheme. Another forgets to clear derived data. Someone runs unit tests but not UI tests. A PR “works on my machine” because the local steps did not match CI.
The fix is boring and effective: define a small set of commands that always do the right thing.
I like to make them look like chat slash commands because they read as intent:
- /build
- /test
- /perf
- /release-notes
They can live in a Makefile, a small scripts/ folder, a Taskfile, or a dedicated tool. The important part is that they are versioned with the repo and they match how CI runs.
What a “slash command” is (in this context)
A slash command is just a named entry point for a workflow. It should:
- be deterministic
- run locally and in CI
- print what it is doing
- exit non-zero on failure
- capture logs and artifacts in predictable locations
If you standardize the inputs and outputs, everything else gets easier: PR checks, performance baselines, and release prep.
/build: a build that matches CI
Goal: compile what you ship, using the same scheme and configuration CI uses.
Common mistakes:
- building a different scheme than CI
- building Debug when CI builds Release (or vice versa)
- silently building for the wrong destination
A minimal starting point:
#!/usr/bin/env bash
set -euo pipefail
SCHEME="App"
CONFIG="Debug"
DERIVED_DATA=".build/DerivedData"
mkdir -p .build
xcodebuild \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-derivedDataPath "$DERIVED_DATA" \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build | xcpretty
Notes:
- Pick one or two canonical destinations and stick to them.
- Use a dedicated derived data path inside the repo so you can clean it without nuking everything.
- If your CI uses
xcodebuilddirectly, use the same flags locally.
If you use Swift Package Manager for modules, consider a second command that builds packages too, but keep /build focused on the app build.
/test: fast by default, strict when needed
Goal: make it easy to run the right tests for the change.
Two patterns work well:
- A fast default that runs unit tests only.
- A strict mode that matches CI (unit + UI tests, or multiple destinations).
Example:
#!/usr/bin/env bash
set -euo pipefail
SCHEME="App"
DERIVED_DATA=".build/DerivedData"
RESULT_BUNDLE=".build/TestResults.xcresult"
rm -rf "$RESULT_BUNDLE"
mkdir -p .build
xcodebuild \
-scheme "$SCHEME" \
-derivedDataPath "$DERIVED_DATA" \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath "$RESULT_BUNDLE" \
test | xcpretty
echo "Saved result bundle: $RESULT_BUNDLE"
Tips that actually reduce flakes:
- Pin the simulator model and OS version in CI and in your scripts.
- Prefer “test plans” in Xcode to encode which tests run in which configuration.
- Save the
.xcresultbundle. It is gold when a test fails.
If your UI tests are slow or flaky, do not hide them. Make them a separate command:
- /test (unit tests)
- /test-ui (UI tests)
Then decide which one runs on every PR.
/perf: measure what matters, not everything
Goal: catch regressions early with a small, repeatable set of checks.
A good /perf command is not a full benchmark lab. It is a tripwire.
Useful categories for iOS apps:
- app launch (cold start)
- first frame time
- scrolling smoothness in one representative list
- memory footprint during a common flow
How you implement this depends on your tooling:
- Xcode Instruments templates (Time Profiler, Allocations)
- MetricKit for production telemetry
- custom signposts with
os_signpost - automated UI flows that record timings
A practical starting point is a scripted run of a representative UI test that collects signpost metrics and prints deltas vs a baseline.
Design guidelines for /perf:
- keep it under a few minutes
- use the same device or simulator config every time
- store baselines in the repo
- fail only on meaningful regressions, warn on small noise
If a check is noisy, fix the check before you enforce it.
/release-notes: stop rewriting the same sentences
Goal: generate a clean first draft of release notes from the source of truth.
The source of truth is usually one of:
- merged PR titles (with a label convention)
- commit messages (if your team is disciplined)
- a changelog file updated as part of the PR
A simple approach that works with GitHub PR titles:
- require a prefix in PR titles, for example:
feat:,fix:,perf:,chore: - pull merged PRs since the last tag
- group by prefix
Even if you do not automate the GitHub API, you can still standardize the output format and make the “human edit” step short.
Example release notes template:
- New
- …
- Improved
- …
- Fixed
- …
The command should output markdown that you can paste into App Store Connect and into your internal announcement.
Keep the command set small
This is the part people skip.
If you create 18 commands, nobody remembers them, and they will run the wrong one. Start with four:
- /build
- /test
- /perf
- /release-notes
Then add only when you can justify the cost of cognitive load.
Make CI call the same scripts
If CI uses different commands than developers, your “slash commands” become a false sense of safety.
The cleanest setup is:
- local:
./scripts/build,./scripts/test,./scripts/perf,./scripts/release-notes - CI: calls the same scripts
Now a developer can reproduce CI failures without guessing.
A quick checklist
When you add a new command, verify it meets these requirements:
- runs from a clean checkout
- does not depend on local state outside the repo
- prints the exact xcodebuild command it runs
- writes artifacts to a predictable folder
- returns non-zero on failure
Make the workflow boring. That is how you get your time back.