GitHub Actions and Private Swift Packages

At Muse, we have our primary Xcode application, and we also have a number of private Swift packages to help organize our code and control dependencies. Each package has its own unit tests, and then our main project has its unit tests as well. Our app is built on iOS and uses Catalyst for the Mac app. There’s a few places in our codebase where we #if targetEnvironment(macCatalyst), so it’s important to run the tests compiled for both platforms.

GitHub provides a guide for testing Swift Packages, which started me on the right track.However, since our packages require iOS vs Catalyst, it wasn’t enough to use swift test, I needed to run the tests in Xcode and specify the platform directly.

I ran into two issues right away:

  1. We need to test not only on Mac, but also iOS and Catalyst
  2. Our main project file includes private swift packages, and some of those depend on other private swift packages

Out of the box, using swift test will only test on a Mac destination, and won’t handle cloning the other private swift packages we depend on. Instead, we need to use xcodebuild and find a way to support cloning those private repos.

So our swift.yml is setup a little different:

name: CI

on:
  push:
    branches:
      - main
    paths:
      - '**.swift'
jobs:
  linting:
    runs-on: macos-12
    steps:
    - name: Repository checkout
      uses: actions/checkout@v2
    - name: Lint
      run: swiftlint
  ios_tests:
    runs-on: macos-12
    steps:
    - name: Repository checkout
      uses: actions/checkout@v2
    - name: Fix Up Private GitHub URLs
      # Add personal access token to all private repo URLs 
      run: find . -type f \( -name '*.pbxproj' -o -name 'Package.swift' -o -name 'Package.resolved' \) -exec sed -i '' "s/https:\/\/github.com\/${GITHUB_REPOSITORY_OWNER}/https:\/\/adamwulf:${{ secrets.SECRET_TOKEN }}@github.com\/${GITHUB_REPOSITORY_OWNER}/g" {} \;
    - name: Build for iOS
      run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild build-for-testing -scheme Muse -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty
    - name: Run iOS tests
      run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -scheme Muse -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty

There’s quite a bit going on here, so first things first. This only shows the iOS testing for brevity the Mac and Catalyst are nearly identical, with only the platform value changing. Depending on the platform, we use either one of the destinations below to target iOS, Mac, or Catalyst:

  • -destination "platform=iOS Simulator,OS=latest,name=iPhone 12"
  • -destination "platform=macOS"
  • -destination "platform=macOS,variant=Mac Catalyst"

Next, how do we handle the private packages? The “Fix Up Private GitHub URLs” step does the work here. Inside our project and package.swift files, we use https://... git urls for each package. But since it’s a private repo, the GitHub Action can’t see it by default – we need the action to authenticate somehow so that it can clone the private packages.

The find ... sed ... {} \;1 step searches for all repository URLs in the project or package files, and replaces them with URLs that have a personal access token embedded in them. This way, when xcodebuild attempts to fetch those repositories, it can authenticate just fine.

I suspect there’s a way to use the GITHUB_TOKEN in the URL to download the private repos, but I wasn’t able to figure it out. If you know a way to use the GITHUB_TOKEN, please let me know.

Each of our private packages has a similar swift.yml workflow file. Now all of our packages and our main project file are automatically tested thanks to GitHub actions. 🎉