CI/CD for a Native Swift Package with GitHub Actions
Build, test, and lint your Swift package across platforms on each push.
A Swift Package Manager library is consumed by other Swift projects and must build cleanly across Apple platforms. This recipe resolves deps, builds, runs swift test, and lints with SwiftLint.
What the pipeline does
- check out on a macOS runner
- resolve deps with swift package resolve
- build with swift build
- test with swift test
- lint with SwiftLint and tag a release
The workflow
swift build and swift test run on a macOS runner with the bundled toolchain; SwiftLint enforces style. Tagging a semantic version is the release mechanism for SPM consumers.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- run: swift --version
- run: swift package resolve
- run: swift build -v
- run: swift test -v
- run: brew install swiftlint && swiftlint --strict
- uses: actions/upload-artifact@v4
with:
name: build-log
path: .build/debug.yamlCaching and speed
Cache the .build directory with actions/cache keyed on Package.resolved to skip rebuilding unchanged dependencies. macOS runners are the priciest tier on GitHub-hosted; running on cheaper managed runners such as Latchkey (around 69% cheaper) makes frequent Apple-platform builds affordable, and auto-retry shrugs off transient toolchain or SPM fetch failures.
Releasing
SPM has no central registry by default -- consumers pull by Git tag. Push a semantic version tag (for example 1.2.0) to release; downstream Package.swift entries resolve to it. Optionally publish to the Swift Package Index by adding the repo.
Key takeaways
- Swift packages build and test on a macOS runner.
- Releases are Git tags -- there is no default central registry.
- Cache .build on Package.resolved to skip dependency rebuilds.