A collection of shell scripts for iOS CI/CD workflows, designed to work with Xcode Cloud and local development environments. Built from learnings gleaned from the developer community and shared back for others to learn from, adapt, and improve upon.
These scripts also served as the inspiration for the Claude Code Release Plugin.
- Scripts
- Installation
- Updating the Submodule
- Local Development: Run manually
- Environment Variables
- License
Xcode Cloud post-clone hook. Runs after the repository is cloned. Executes swiftlint.sh to lint the codebase during CI builds.
Xcode Cloud pre-build hook. Runs before xcodebuild starts. Calls generate_xcconfig.sh to generate xcconfig files from environment variables.
This enables you to inject API keys, feature flags, and other configuration at build time without committing them to source control.
Generates xcconfig files from all ENV_ prefixed environment variables.
⚠️ Security WarningThis script generates plain text xcconfig values. Values can be extracted from compiled binaries using simple tools like
strings.Appropriate for:
- Analytics IDs (Google Analytics, Firebase)
- Feature flags
- Public API endpoints
- Non-sensitive configuration
NOT appropriate for:
- Payment/billing API keys
- Authentication secrets
- Database credentials
- Any key that could cause financial or security damage if exposed
For sensitive credentials, use a backend proxy (keys never leave your server) or a dedicated secrets manager with obfuscation.
Usage:
generate_xcconfig.sh <output_directory> [product_name]| Parameter | Description | Default |
|---|---|---|
output_directory |
Where to write the xcconfig file | Required |
product_name |
Name prefix for output file | CI_PRODUCT or "App" |
Generated Files:
| File | Contents | Git |
|---|---|---|
<ProductName>.xcconfig |
Project config that includes the dynamically generated <ProductName>Config.xcconfig (no values) |
Tracked |
<ProductName>Config.xcconfig |
Actual values from ENV_ variables | Ignored |
The wrapper file is only created locally (not in CI) and only if it doesn't already exist. It contains just the #include? directive - no default values. If the config file is missing, Xcode variables remain undefined and your code should handle nil gracefully.
Example:
export ENV_AnalyticsConfig_ga4MeasurementId="G-XXXXXXXXXX"
export ENV_AnalyticsConfig_ga4ApiSecret="your-api-secret"
./generate_xcconfig.sh ./Config MyAppFirst run creates two files:
Config/MyApp.xcconfig (commit this - scaffolding only):
//
// MyApp.xcconfig
//
#include? "MyAppConfig.xcconfig"
Config/MyAppConfig.xcconfig (gitignored - contains actual values):
ga4ApiSecret = your-api-secret
ga4MeasurementId = G-XXXXXXXXXX
Xcode Setup:
- Run the script locally to generate both files
- Add
MyApp.xcconfigto your Xcode project configurations - Add
MyAppConfig.xcconfigto.gitignore - Reference values in Info.plist:
$(propertyName) - Access in Swift:
Bundle.main.object(forInfoDictionaryKey: "PropertyName") as? String ?? ""
Xcode Cloud post-build hook. Runs after xcodebuild completes. Only executes when CI_ARCHIVE_PATH is available (archive builds).
Calls:
firebase_upload_symbols.sh- Upload dSYMs to Crashlyticstestflight_whattotest.sh- Generate TestFlight release notes
Installs and runs SwiftLint for code linting.
| Environment | Behavior |
|---|---|
| CI | Installs SwiftLint via Homebrew, lints the repository |
| Local | Uses existing SwiftLint installation, lints SRCROOT |
Configuration: .swiftlint.yml - A sample SwiftLint configuration is included. Customize it to match your project's coding standards. See the SwiftLint Rules Directory for available rules.
Uploads dSYM files to Firebase Crashlytics for crash symbolication.
| Environment | Behavior |
|---|---|
| CI | Uses upload-symbols directly from the Firebase SDK checkout |
| Local | Uses the Firebase Crashlytics run script from SourcePackages |
The script automatically detects if Firebase is configured by checking for GoogleService-Info.plist. If not found, it skips gracefully without failing the build.
Generates TestFlight "What to Test" release notes from git history. Creates TestFlight/WhatToTest.en-US.txt containing the last 20 commits formatted as:
- YYYY-MM-DD: commit message
Interactive version management tool for Xcode projects. Automates semantic versioning, project file updates, and git tag creation.
Usage:
./bump-version.sh [major|minor|patch|tag]
./bump-version.sh tag [-y|--yes] # Non-interactive mode for CIIf no argument is provided, the script displays an interactive menu to select the bump type. Use -y or --yes with the tag option to skip confirmation prompts and automatically push the tag to remote (useful for GitHub Actions or other CI pipelines).
Bump Types:
| Type | Description | Example |
|---|---|---|
patch |
Bug fixes, minor changes | 1.2.3 → 1.2.4 |
minor |
New features, backwards compatible | 1.2.3 → 1.3.0 |
major |
Breaking changes | 1.2.3 → 2.0.0 |
tag |
Update existing tag only (no version change) | Re-tags current commit as rel.v1.2.3 |
Features:
- Automatically discovers
.xcodeprojin the parent directory - Reads current
MARKETING_VERSIONfromproject.pbxproj - Updates all occurrences of
MARKETING_VERSIONin the project file - Commits the version change with message "Bump version to X.Y.Z"
- Creates git tag in format:
rel.vX.Y.Z - Confirmation prompts before making changes
The tag Option and Xcode Cloud Incremental Builds:
The tag option is particularly useful for triggering incremental Xcode Cloud builds. When your Xcode Cloud workflow is configured to build on tag changes (e.g., tags matching rel.v*), you can use the tag option to:
- Force-update an existing release tag to point to a newer commit
- Trigger a new Xcode Cloud build without incrementing the version number
- Re-deploy the same version with additional fixes or changes
This enables a workflow where you can iterate on a release candidate by updating the tag, triggering rebuilds without burning through version numbers:
# Initial release
./bump-version.sh patch # Creates rel.v1.2.4
# Need to include a quick fix in the same version
git commit -m "Fix critical bug"
./bump-version.sh tag # Updates rel.v1.2.4 to current commit
git push origin -f rel.v1.2.4 # Force-push triggers new Xcode Cloud buildExample Workflow:
# Bump patch version (1.0.0 → 1.0.1)
./bump-version.sh patch
# Push commit and tag to remote
git push && git push origin rel.v1.0.1Add this repository as a git submodule named ci_scripts at your project root. The ci_scripts name is required for Xcode Cloud compatibility.
cd /path/to/YourApp
# Add the submodule
git submodule add https://github.com/user/ios-ci-scripts.git ci_scripts
# Commit the submodule reference
git commit -m "Add CI scripts submodule"Your project structure will look like:
YourApp/
├── YourApp.xcodeproj/
├── YourApp/
│ ├── AppDelegate.swift
│ ├── GoogleService-Info.plist
│ └── ...
├── ci_scripts/ # ← Submodule (this repo)
│ ├── ci_post_clone.sh
│ ├── ci_post_xcodebuild.sh
│ ├── firebase_upload_symbols.sh
│ ├── swiftlint.sh
│ └── ...
└── .gitmodules
To run scripts during local builds, add them as Run Script phases in your Xcode project:
- Open your project in Xcode
- Select your app target
- Go to Build Phases
- Click + → New Run Script Phase
- Configure the script (see examples below)
SwiftLint (Build Phase)
Add as an early build phase to lint code before compilation:
| Setting | Value |
|---|---|
| Shell | /bin/sh |
| Based on dependency analysis | ☐ Unchecked |
Script:
# Only run locally - Xcode Cloud uses ci_post_clone.sh
if [ -z "$CI" ]; then
./ci_scripts/swiftlint.sh
fiFirebase Crashlytics (Build Phase)
Add as the final build phase to upload dSYM symbols:
| Setting | Value |
|---|---|
| Shell | /bin/sh |
| Based on dependency analysis | ☐ Unchecked |
Script:
# Only run locally - Xcode Cloud uses ci_post_xcodebuild.sh
if [ -z "$CI" ]; then
./ci_scripts/firebase_upload_symbols.sh
fiInput Files:
$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib
Note: Adding input files helps Xcode determine when the script needs to re-run and improves incremental build performance.
Xcode Cloud automatically discovers and executes scripts in the ci_scripts/ directory at specific points in the build lifecycle:
| Script | Trigger | Purpose |
|---|---|---|
ci_post_clone.sh |
After repository clone | Runs SwiftLint |
ci_pre_xcodebuild.sh |
Before build | Generates xcconfig from env variables |
ci_post_xcodebuild.sh |
After archive build | Uploads dSYMs, generates TestFlight notes |
Xcode Cloud and Submodules
Xcode Cloud automatically handles git submodules:
- Submodules are recursively initialized and updated during clone
- No additional configuration required in Xcode Cloud settings
- Scripts execute with full access to submodule contents
Configuring Xcode Cloud Workflows
For release builds triggered by git tags:
- In Xcode, go to Product → Xcode Cloud → Manage Workflows
- Create or edit a workflow
- Under Start Conditions, add:
- Source Branch Changes:
main(for development builds) - Tag Changes:
rel.v*(for release builds)
- Source Branch Changes:
- Under Actions, select Archive for release builds
The ci_post_xcodebuild.sh script only runs for archive builds (when CI_ARCHIVE_PATH is set), so it won't interfere with test or analysis workflows.
To pull the latest CI script updates into your project:
# Update to latest commit
cd ci_scripts
git pull origin main
cd ..
# Commit the updated submodule reference
git add ci_scripts
git commit -m "Update CI scripts submodule"
git pushOr update all submodules at once:
git submodule update --remote --merge
git commit -am "Update submodules"Run scripts directly from the command line:
cd ci_scripts
# Lint the codebase
./swiftlint.sh
# Bump version and create release tag
./bump-version.shThe scripts detect CI environments using these variables:
| Variable | Description |
|---|---|
CI |
Set in CI environments |
CI_PRIMARY_REPOSITORY_PATH |
Xcode Cloud repository checkout path |
CI_WORKSPACE_PATH |
Xcode Cloud workspace path |
CI_ARCHIVE_PATH |
Path to the archive (post-build) |
CI_DERIVED_DATA_PATH |
Derived data location |
CI_PRODUCT |
Product name (used for xcconfig file naming) |
SRCROOT |
Xcode project source root (local builds) |
BUILD_DIR |
Xcode build directory (local builds) |
This project is licensed under the MIT License - see the LICENSE file for details.