- 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 sure `project.yml` already carries them (if not, tell the user to reconcile - see `/swift-preflight` Check 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:
```bash
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):
```sh
#!/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 `sed` form above matches a `CURRENT_PROJECT_VERSION:` line in `project.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 generate` for `brew 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:
```yaml
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:
```sh
#!/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.
Ver no GitHub