Building a Pokédex on iOS — Part 10: CI/CD with GitHub Actions
Final part of the PokeTracker series: we set up a CI/CD pipeline with GitHub Actions that builds and tests the app on every push. Plus the repository.
In the previous nine posts we built the complete app. This is the final step: automating the build and tests so that every change to the repository is verified without manual intervention.
Why CI/CD from day one
The CI/CD conversation usually gets pushed to “when the project grows”. The problem is that setting it up in a large project with accumulated technical debt is much harder. In a small project it takes 20 minutes and from the very first commit it gives you:
- Confirmation that the code compiles on a clean machine (not just yours)
- Tests run automatically — no relying on someone remembering to hit
⌘U - A history of whether the code was green or red on each commit
GitHub Actions for iOS
GitHub Actions has runners with macOS and Xcode pre-installed. You don’t need any external service.
Create the file .github/workflows/ci.yml at the root of the PokeTracker repository:
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
build-and-test:
name: Build & Test
runs-on: macos-15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_16.3.app
- name: Build
run: |
xcodebuild build \
-project PokeTracker.xcodeproj \
-scheme PokeTracker \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
CODE_SIGNING_ALLOWED=NO \
| xcpretty
- name: Run Tests
run: |
xcodebuild test \
-project PokeTracker.xcodeproj \
-scheme PokeTracker \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
CODE_SIGNING_ALLOWED=NO \
| xcpretty
A few decisions in this file:
macos-15 is the runner with Xcode 16. If you need a specific version, GitHub publishes the list of available runners in their documentation.
CODE_SIGNING_ALLOWED=NO avoids the code signing problem in CI. On a GitHub runner you don’t have certificates installed. This flag disables code signing for test and simulator builds.
xcpretty formats the output of xcodebuild, which is very verbose by default. To install it, add this step before the build:
- name: Install xcpretty
run: gem install xcpretty
Or you can skip it and pass the output unformatted — it works the same, it’s just harder to read.
Adding a badge to the README
The README can show the current CI status. Add this line to the top of the project’s README.md:

The badge updates automatically — green if the last build passed, red if it failed.
The complete pipeline
With this workflow, every push to main or develop, and every Pull Request targeting those branches, triggers:
- Checkout of the code
- Build of the project without code signing
- Execution of all tests in the
PokeTrackerTeststarget
If something fails, the commit or PR is marked red on GitHub. If it passes, it goes green. Simple.
The repository
Throughout this series we built each piece explaining the reasoning behind every decision. If you followed the posts, you have the project up and running. If you want to compare with the complete implementation or just explore it:
The repository includes everything we covered in the series plus the Atoms and Molecules views from Atomic Design that we didn’t cover in detail due to space. If you have questions about any part of the code, you can reach me via the chat at slekens.dev.
Series summary
| Post | Topic |
|---|---|
| 1 | Project structure and architecture decisions |
| 2 | Network layer: APIClient, Endpoint, NetworkError |
| 3 | DTOs and the API contract |
| 4 | Repository Pattern |
| 5 | Domain models and Use Cases |
| 6 | Offline persistence with SwiftData |
| 7 | Image cache with NSCache + FileManager |
| 8 | ViewModel with @Observable |
| 9 | Unit testing with Swift Testing |
| 10 | CI/CD with GitHub Actions |
Thanks for reading the series.