name: swift-ci-scaffold description: "Scaffold the repo side of Xcode Cloud for an XcodeGen / Tuist Swift project - generate ci_scripts/ci_post_clone.sh (regen + guarded CI_BUILD_NUMBER stamping), propose a unit-test-only CI scheme, print the exact App Store Connect workflow checklist, and optionally install an opt-in .githooks/pre-push gate. Shows diffs and confirms before writing." argument-hint: "[path] [--hook]" allowed-tools: [Bash, Read, Glob, Grep, Write, Edit, AskUserQuestion] disable-model-invocation: true
Swift CI Scaffolder
You prepare the repo side of Xcode Cloud for a generator-based Swift project, and you fix the two build-system blockers that /swift-preflight flags: a missing/weak ci_post_clone.sh and a static build number. You also optionally install a local pre-push gate.
You cannot create or edit the Xcode Cloud workflow itself - those live in App Store Connect, not the repo. You scaffold what the repo can hold and print the exact UI steps for the rest.
Rules
- Non-destructive. Probe first, propose a diff, confirm, then write. Never overwrite an existing
ci_post_clone.sh,project.yml, or hook without showing the change and getting a yes. - Preserve the source of truth. On XcodeGen, edits go into
project.yml, never the pbxproj. Read the real deployed bundle id / team from the pbxproj first and make sureproject.ymlalready carries them (if not, tell the user to reconcile - see/swift-preflightCheck 1 - before you scaffold a regen step, because regen would otherwise revert their signing identity). - Idempotent. If a well-formed script/scheme/hook already exists, say so and offer to only patch the missing pieces.
Arguments
$ARGUMENTS: optional path (repo root, default cwd); --hook jumps straight to the pre-push hook step.
Phase 0: Probe
Detect the generator (xcodegen / tuist), platforms, existing ci_scripts/, the app + unit-test + UI-test targets, and the current CURRENT_PROJECT_VERSION. If the project is a plain .xcodeproj (no generator) or SwiftPM library, tell the user this scaffolder is for generator-based app projects on Xcode Cloud and stop (offer /swift-verify instead).
Critical pre-check (XcodeGen drift): before proposing a regen-on-CI script, confirm project.yml already declares the real PRODUCT_BUNDLE_IDENTIFIER and DEVELOPMENT_TEAM that the pbxproj uses:
grep -E 'PRODUCT_BUNDLE_IDENTIFIER|DEVELOPMENT_TEAM' *.xcodeproj/project.pbxproj | sort -u
If project.yml is missing either, stop and warn: adding a CI regen step now would revert the hand-set signing identity and break the App Store record. Have the user copy the real values into project.yml settings.base first.
Phase 1: ci_scripts/ci_post_clone.sh
If absent or malformed, propose this (adapt generator and stamp target):
#!/bin/sh
# Xcode Cloud runs this immediately after cloning, before it resolves
# dependencies or reads the Xcode project. The .xcodeproj is generated from
# project.yml by XcodeGen, so we regenerate here to guarantee the build matches
# the spec (no stale/hand-edited pbxproj drift). We also stamp the build number
# from CI_BUILD_NUMBER so each TestFlight upload is unique.
set -e
# Xcode Cloud starts in ci_scripts/; the repo root is one level up.
cd "$CI_PRIMARY_REPOSITORY_PATH"
echo "Installing XcodeGen..."
brew install xcodegen
# Stamp the build number into the spec BEFORE generating, so the generated
# project carries a unique CFBundleVersion. Guarded so local builds (where
# CI_BUILD_NUMBER is unset) keep the spec value.
if [ -n "$CI_BUILD_NUMBER" ]; then
echo "Setting CURRENT_PROJECT_VERSION to $CI_BUILD_NUMBER"
/usr/bin/sed -i '' \
"s/^\( *CURRENT_PROJECT_VERSION:\).*/\1 \"$CI_BUILD_NUMBER\"/" \
project.yml
fi
echo "Generating Xcode project..."
xcodegen generate
echo "ci_post_clone complete."
After writing, set the executable bit: chmod +x ci_scripts/ci_post_clone.sh.
Notes to surface:
- The
sedform above matches aCURRENT_PROJECT_VERSION:line inproject.yml. If the project version lives elsewhere (a base xcconfig, or only in the pbxproj), adapt the stamp target and tell the user. - For Tuist, swap
brew install xcodegen+xcodegen generateforbrew install tuist+tuist generate, and stamp wherever the version is declared. - Do not also enable App Store Connect auto-increment; pick one source of truth (this script).
Phase 2: Unit-test-only CI scheme
If a UI-test target exists and no unit-test-only shared scheme is present, propose adding one to project.yml schemes: (so it generates as shared). Example shape:
schemes:
App-CI:
build:
targets:
App: all
AppTests: [test]
test:
targets:
- AppTests # unit only — no AppUITests, so a flaky UITest can't gate a release
run:
config: Debug
archive:
config: Release
Explain: point the Xcode Cloud Test actions (iOS + macOS) at App-CI, keep the Archive action on the full App scheme. The scheme is only visible to Xcode Cloud after the commit that defines it is pushed.
Phase 3: App Store Connect workflow checklist (print, cannot automate)
App Store Connect → your app → Xcode Cloud → Manage Workflows. Repo cannot hold these:
1. Start Conditions: Branch Changes on your release branch (e.g. `staging` or `main`).
2. Environment: select the Xcode version matching project.yml `xcodeVersion`.
3. Test action (iOS): scheme = App-CI, "Required to Pass". Repeat for macOS if you ship Mac.
4. Archive action: scheme = App (full), one per platform you distribute.
5. Skip Build and Analyze actions — Archive already does a full signed release build, and
the Clang analyzer adds near-zero value for pure Swift (strict concurrency + swiftlint cover it).
6. Post action: TestFlight (internal) for iOS and/or macOS.
7. First run will surface environment-only test failures that pass locally
(headless keychain writes). Fix test-side — see /swift-preflight Check 5 — not in production.
Phase 4: Opt-in pre-push hook (only if user agrees, or --hook)
Offer, never force. If yes, write .githooks/pre-push (version-controlled, like a .githooks/ convention) that runs the local gate before a push:
#!/bin/sh
# Pre-push gate: regenerate, format-check, lint strict, unit tests.
# Enable once per clone: git config core.hooksPath .githooks
set -e
exec ./scripts/verify.sh # or inline the /swift-verify pipeline if no verify.sh exists
If there is no scripts/verify.sh, inline the detected pipeline (xcodegen generate -> swiftformat --lint -> swiftlint --strict -> xcodebuild test on the cheapest destination). chmod +x .githooks/pre-push. Then tell the user to run git config core.hooksPath .githooks once per clone, and that --no-verify bypasses it (discouraged - fix the failure instead). Do not auto-run git config; that changes their git setup - tell them the command.
Output
Summarize what was written (with the diff already shown and confirmed), what was skipped because it already existed, and the remaining manual App Store Connect steps. End by suggesting a re-run of /swift-preflight to confirm the FAILs cleared.