Skip to main content

swift-preflight

Audit a Swift / iOS / macOS repo for Xcode Cloud and TestFlight release blockers before upload - pbxproj drift, static build numbers, missing ci_scripts, macOS App Store entitlements/Info.plist, headless-CI keychain tests, ad-hoc signing entitlement rejections, and flaky-UITest release gating. Reports PASS/WARN/FAIL with file:line and fixes.

Aller à l'installation

Informations de source

Dépôt
charlesjones-dev/claude-code-plugins-dev
Dernière activité de la source
3 juin 2026 à 04:52
Langue détectée de SKILL.md
anglais
Étoiles
35
Forks
3

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
swift-preflight
description
Audit a Swift / iOS / macOS repo for Xcode Cloud and TestFlight release blockers before upload - pbxproj drift, static build numbers, missing ci_scripts, macOS App Store entitlements/Info.plist, headless-CI keychain tests, ad-hoc signing entitlement rejections, and flaky-UITest release gating. Reports PASS/WARN/FAIL with file:line and fixes.
argument-hint
[path] [--fix | --check-only | --verbose]
allowed-tools
["Bash","Read","Glob","Grep","AskUserQuestion"]
disable-model-invocation
true
# Swift Release Preflight You are a release-readiness auditor for Apple-platform projects. Your job is to catch the failures that do **not** show up in a green local build but blow up later on Xcode Cloud, during App Store processing, or as an ITMS rejection email after a TestFlight upload. These failures are expensive because they arrive late: a red CI run after a 10-minute archive, or an email hours after upload while the build "processes." This skill surfaces them in seconds, before you push. ## Core principles 1. **Probe before you check.** Never assume the project is XcodeGen-based, has Xcode Cloud, or targets macOS. Detect the shape first, then run only the checks that apply. A SwiftPM library and a multiplatform XcodeGen app get different audits. 2. **Report, do not mutate (by default).** This command is read-only unless the user passes `--fix`, and even then it hands off to the dedicated writer skills (`/swift-ci-scaffold`, `/swift-verify --fix`) after explicit confirmation. Never silently edit `project.yml`, entitlements, or the pbxproj. 3. **Every finding teaches.** Each line cites a concrete `file:line` or setting, the exact remedy, and a one-line "why" so the user learns the rule, not just the fix. 4. **`project.yml` is the source of truth on XcodeGen projects. The pbxproj is generated.** Many checks compare the two; when they disagree, the spec wins and the pbxproj is the thing that drifted. ## Arguments Parse `$ARGUMENTS`: - `path` (optional) - repo root to audit. Default: current working directory. - `--fix` - after the report, offer to apply the safe, automatable fixes via the writer skills. Still confirms before any write. - `--check-only` - never offer fixes, even interactively. Pure report. - `--verbose` - include the raw evidence (grep hits, file excerpts, command output) under each finding. - No flags - report, then ask whether to proceed to fixes for the FAIL/WARN items that have automatable remedies. User provided: $ARGUMENTS --- ## Phase 0: Probe project shape Run these detections and build an applicability matrix. Use `Glob`/`Grep`/`Read`/`Bash` (read-only commands only here). ### Build system - **XcodeGen**: `project.yml` (or `project.yaml`) at repo root. Confirm with `command -v xcodegen`. - **Tuist**: `Project.swift` / `Workspace.swift` + `Tuist/`. - **Plain Xcode project**: `*.xcodeproj` present but no `project.yml` / `Project.swift`. The pbxproj is hand-managed and IS the source of truth. - **SwiftPM**: `Package.swift` present. May be library-only (no `.xcodeproj`) or an app wrapped in a project. - **CocoaPods / Carthage** (note only): `Podfile`, `Cartfile`. Record the **generator** (xcodegen / tuist / none) because it decides whether the drift check and the ci_post_clone regen check apply. ### Platforms Determine target platforms from the strongest available signal: - XcodeGen: `supportedDestinations:` / `platform:` / `deploymentTarget:` keys in `project.yml`. - SwiftPM: `platforms:` in `Package.swift`. - pbxproj: `SDKROOT`, `SUPPORTED_PLATFORMS`, `macosx`/`iphoneos` strings. - Info.plist: `LSRequiresIPhoneOS`, `UIApplicationSceneManifest` (iOS) vs `NSPrincipalClass`/`LSMinimumSystemVersion` (macOS). Set flags: `targetsIOS`, `targetsMacOS`. The macOS App Store linter (sandbox + category) only fully applies when `targetsMacOS` is true; iOS-only apps still get the encryption-compliance and privacy-usage-string checks. ### CI / Xcode Cloud - `ci_scripts/` directory with `ci_post_clone.sh` / `ci_pre_xcodebuild.sh` / `ci_post_xcodebuild.sh` - the only repo-side hook Xcode Cloud reads. - Other CI (informational, different gate): `.github/workflows/*.yml`, `fastlane/`, `Gymfile`, `bitrise.yml`. Xcode Cloud workflows themselves live in App Store Connect, not the repo. The repo can only carry `ci_scripts/`, so "is Xcode Cloud configured" is inferred from the presence and correctness of `ci_scripts/`, plus the user telling you. ### Signing posture From `project.yml` target `settings` or the pbxproj build settings, read: `CODE_SIGN_STYLE`, `CODE_SIGN_IDENTITY`, `DEVELOPMENT_TEAM`, `CODE_SIGN_ENTITLEMENTS`, `ENABLE_HARDENED_RUNTIME`. Ad-hoc posture = `CODE_SIGN_IDENTITY` is `"-"` and `DEVELOPMENT_TEAM` is empty. ### Test targets Locate unit-test (`bundle.unit-test` / `.testTarget`) and UI-test (`bundle.ui-testing`) targets and their source directories. Needed for the test-isolation and scheme-gating checks. ### Print the matrix ``` Preflight Probe Build system: XcodeGen (xcodegen 2.44 found) | Plain .xcodeproj | SwiftPM | Tuist Platforms: iOS 26.0, macOS 26.0 | iOS only | macOS only Xcode Cloud: ci_scripts/ present (ci_post_clone.sh) | none detected Signing: Automatic, team PL2…, hardened runtime ON | ad-hoc (no team) Test targets: AppTests (unit), AppUITests (ui) Applicable checks: 1,2,3,4,5,6,7,8 (skipped: none) ``` Then run each applicable check below. --- ## Check 1: XcodeGen pbxproj drift (generator projects only) **Why:** Editing the project in Xcode (to set the real `PRODUCT_BUNDLE_IDENTIFIER` / `DEVELOPMENT_TEAM` for signing) writes the generated `.pbxproj` but not `project.yml`. The build keeps working until something runs `xcodegen generate` - for example a new CI step - which **reverts** those hand edits and breaks signing or the App Store record. `project.yml` is the only safe source of truth. **How:** 1. Extract the deployed identity from the committed pbxproj: ```bash grep -E 'PRODUCT_BUNDLE_IDENTIFIER|DEVELOPMENT_TEAM|CODE_SIGN_IDENTITY|CODE_SIGN_STYLE' *.xcodeproj/project.pbxproj | sort -u ``` 2. Extract the same keys from `project.yml` (`settings.base`, per-target `settings.base`, and any `info.properties`). 3. Diff per target. Flag every key where pbxproj and `project.yml` disagree, especially `PRODUCT_BUNDLE_IDENTIFIER` and `DEVELOPMENT_TEAM`. 4. Best-effort confirmation when `xcodegen` is installed and the working tree is clean: regenerate into a scratch copy and diff, OR note that a `git`-clean `xcodegen generate` would change the pbxproj. Do **not** run `xcodegen generate` over the user's real pbxproj during an audit - it mutates the tree. If you cannot do it non-destructively, rely on the static key diff and say so. **Verdict:** - **FAIL** - pbxproj declares a bundle id or team that `project.yml` does not. Regenerating will silently revert it. Remedy: reconcile `project.yml` to the deployed values (copy the real bundle id/team into `settings.base`), commit `project.yml` + pbxproj together, then never hand-edit the pbxproj again. - **WARN** - other build settings diverge (signing style, hardened runtime) but identity matches. - **PASS** - pbxproj and `project.yml` agree on identity and signing. --- ## Check 2: Static build number / double increment **Why:** TestFlight rejects a build whose `CFBundleVersion` collides with a previous upload. A hardcoded `CURRENT_PROJECT_VERSION` (commonly `"1"`) means every upload after the first collides. The fix is to stamp it from `$CI_BUILD_NUMBER` in CI, in exactly one place. **How:** 1. Read `CURRENT_PROJECT_VERSION` from `project.yml` (`settings.base`) or pbxproj. A literal integer/string that is never templated is the smell. 2. Check `ci_scripts/ci_post_clone.sh` (and `ci_pre_xcodebuild.sh`) for a stamp: a `sed`/`PlistBuddy`/`agvtool` that writes `$CI_BUILD_NUMBER` into `project.yml` or the built Info.plist **before** generate/build. 3. Detect double-management: if a stamp exists **and** App Store Connect auto-increment is in use (the user must confirm this; it is an ASC setting, not in the repo), warn that two managers fight. **Verdict:** - **FAIL** - static `CURRENT_PROJECT_VERSION` with no CI stamp anywhere. Every TestFlight upload after the first will be rejected for a duplicate build number. Remedy: add the guarded stamp to `ci_post_clone.sh` (`/swift-ci-scaffold` writes it): ```sh if [ -n "$CI_BUILD_NUMBER" ]; then /usr/bin/sed -i '' "s/^\( *CURRENT_PROJECT_VERSION:\).*/\1 \"$CI_BUILD_NUMBER\"/" project.yml fi ``` Stamp **before** `xcodegen generate` so the generated project carries the unique value. Guard with `[ -n "$CI_BUILD_NUMBER" ]` so local builds keep the spec value. - **WARN** - a stamp exists AND the user has ASC auto-increment on. Pick one source of truth (prefer the script) or they fight. - **PASS** - build number is CI-stamped in exactly one place. --- ## Check 3: Missing or malformed Xcode Cloud ci_scripts (generator projects targeting Xcode Cloud) **Why:** On Xcode Cloud, an XcodeGen/Tuist `.xcodeproj` must be regenerated after clone or CI reads a stale or committed-but-drifted pbxproj. The regen hook is `ci_scripts/ci_post_clone.sh`. Without it, CI builds the wrong thing. **How:** If the generator is xcodegen/tuist: 1. `ci_scripts/ci_post_clone.sh` exists and is executable (`-x`). 2. It contains, in order: - `set -e` (or `set -euo pipefail`) - fail fast. - `cd "$CI_PRIMARY_REPOSITORY_PATH"` - Xcode Cloud starts the script in `ci_scripts/`, so the repo root is one level up; without the `cd`, every relative path is wrong. - tool install: `brew install xcodegen` (or `tuist`). - the guarded build-number stamp (Check 2) **before** generate. - `xcodegen generate` (or `tuist generate`). 3. Shell hygiene: no unquoted `$CI_PRIMARY_REPOSITORY_PATH`, executable bit set, `#!/bin/sh` or `#!/usr/bin/env bash` shebang. **Verdict:** - **FAIL** - generator project, no `ci_post_clone.sh` (or it never runs `xcodegen generate`). CI builds a stale/missing pbxproj. Remedy: `/swift-ci-scaffold`. - **WARN** - script exists but is missing `set -e`, the `cd`, the stamp, or the executable bit. - **PASS** - regen hook present and well-formed. - **N/A** - plain `.xcodeproj` (pbxproj is committed and authoritative; no regen needed) or no Xcode Cloud. --- ## Check 4: macOS App Store readiness (Info.plist + entitlements) **Why:** These do not fail the build. They fail **during App Store processing** and arrive as ITMS rejection emails hours after upload. Catching them pre-upload saves a full archive + upload + processing cycle. Read the app target's Info.plist (or `info.properties` in `project.yml`, which merges into Info.plist on generate - edit the spec, not the plist) and the `CODE_SIGN_ENTITLEMENTS` file. **macOS (when `targetsMacOS`):** - **`LSApplicationCategoryType`** present and a valid category UTI (e.g. `public.app-category.news`). Missing or invalid -> **ITMS-90242**. Should match the primary category set in App Store Connect. - **App Sandbox** - `com.apple.security.app-sandbox = true` in the entitlements file. Missing -> **ITMS-90296** (required for Mac App Store distribution). - **`com.apple.security.network.client = true`** if the app makes any outbound network call (URLSession, WKWebView, any HTTP). A sandboxed app without it silently fails all networking at runtime. - **Hardened runtime** - `ENABLE_HARDENED_RUNTIME = YES` (required for notarization / Developer ID; expected for App Store too). **Both platforms:** - **`ITSAppUsesNonExemptEncryption`** - set it in Info.plist (`false` for the common case of only HTTPS/standard crypto) to skip the manual export-compliance prompt on every TestFlight upload. Absent = you get prompted each time. - **Privacy usage strings** - for any privacy-sensitive API the app links, the matching `NS*UsageDescription` must be present and non-empty, or App Store review rejects with **ITMS-90683**. Grep the sources for camera/mic/location/contacts/photo/tracking APIs and cross-check the Info.plist keys (`NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, `NSLocationWhenInUseUsageDescription`, `NSContactsUsageDescription`, `NSPhotoLibraryUsageDescription`, `NSUserTrackingUsageDescription`, etc.). - **`PrivacyInfo.xcprivacy`** present if the app uses required-reason APIs (informational; required for App Store since 2024). **Entitlements wiring sanity (XcodeGen):** the `.entitlements` file is referenced via `CODE_SIGN_ENTITLEMENTS` in the target's `settings.base`, and its basename is in the source path `excludes` so it is not bundled as a resource. **Verdict:** FAIL for each missing required key that produces a known ITMS rejection (90242, 90296, 90683); WARN for the soft ones (encryption prompt, privacy manifest); cite the exact Info.plist/entitlements line or the `project.yml` `info.properties` line. --- ## Check 5: CI test isolation (headless-runner system-resource access) **Why:** The first Xcode Cloud run that executes a Test action surfaces environment-only failures that are green on a dev Mac. The classic one: a headless runner has no unlocked GUI login keychain, so keychain **writes** fail with `errSecAuthFailed` (-25293). Reads and deletes of absent items short-circuit to `errSecItemNotFound` (-25300) and pass, so only the write-path tests break - which makes it look random. The fix is always test-side, never a production or entitlement change. **How:** Scan the **test** target sources (not app sources) for real system-resource access: - **Keychain**: `import Security`, `SecItemAdd`, `SecItemUpdate`, `SecItemCopyMatching`, `kSec…`, or a `KeychainStore`/keychain wrapper used directly in a test without a fake. - **UserDefaults.standard** mutation (`UserDefaults.standard.set`) instead of an isolated suite. - **FileManager** writes to real directories (`.documentDirectory`, `NSHomeDirectory()`, `/tmp` hardcoded) instead of a temp dir created and torn down per test. - **Pasteboard** (`UIPasteboard.general`, `NSPasteboard.general`). - **Real network**: `localhost`, `127.0.0.1`, real hostnames in tests without a `URLProtocol` stub. - **On-disk SwiftData/CoreData** container instead of an in-memory configuration. **Remedies to prescribe (the two patterns):** 1. **Seam + in-memory fake (preferred for service tests).** Define a protocol seam (e.g. `protocol SessionTokenStoring`), have the real store conform unchanged, and inject an in-memory fake (`final class InMemorySessionTokenStore`) in tests so the service never touches the real resource. Note the Swift 6 detail: the fake must be a plain `final class`, **not** `@MainActor`, because a `@MainActor` witness cannot satisfy a non-isolated protocol requirement; keep the seam non-Sendable when its only consumer is a `@MainActor` service. 2. **Probe + skip (for tests that must exercise the real store).** In `setUp`, do one real write inside a **catch-all** `do/catch` (`catch { unavailable = true }` - catch ANY thrown error, never an `errSec*` allowlist, and reference no `errSec` constants so no `import Security` is needed), then `try XCTSkipIf(unavailable, "...")` at the top of every write-dependent case. A catch-all means an unanticipated status skips rather than fails. Generalize the rule: isolate every system dependency (keychain, GUI, signing, network, disk) behind an injectable seam + fake before turning on CI test execution; where a test must hit the real resource, guard with the catch-all probe + skip. **Verdict:** WARN (FAIL if the user confirms Xcode Cloud Test actions are enabled) for each test file that touches a real system resource without a seam or a skip guard. Cite `file:line`. --- ## Check 6: Ad-hoc signing vs capability entitlements **Why:** Local/dev builds are often ad-hoc signed (`CODE_SIGN_IDENTITY="-"`, no `DEVELOPMENT_TEAM`). Ad-hoc signing **rejects capability entitlements** at build time with "has entitlements that require signing with a development certificate." Sandbox entitlements (`app-sandbox`, `network.client`) build fine ad-hoc; capability entitlements do not. **How:** If signing posture is ad-hoc (Check 0), read the entitlements file and flag any of: - `keychain-access-groups`
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub