← Back to blog

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:

![CI](https://github.com/slekens/PokeTrack/actions/workflows/ci.yml/badge.svg)

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:

  1. Checkout of the code
  2. Build of the project without code signing
  3. Execution of all tests in the PokeTrackerTests target

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:

github.com/slekens/PokeTrack

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

PostTopic
1Project structure and architecture decisions
2Network layer: APIClient, Endpoint, NetworkError
3DTOs and the API contract
4Repository Pattern
5Domain models and Use Cases
6Offline persistence with SwiftData
7Image cache with NSCache + FileManager
8ViewModel with @Observable
9Unit testing with Swift Testing
10CI/CD with GitHub Actions

Thanks for reading the series.