I am currently maintaining numerous Swift Packages that don't receive a constant flow of updates, but do receive updates when new Swift updates come out, or as I think of useful additions.
To ensure that I can make some of these less frequent updates without too much friction and with confidence in their correctness I rely heavily on GitHub Actions, which I'll go over in this blog post.
I have 2 workflows that I use across my projects, one for running tests and the other for performing releases.
Tests Workflow
The tests workflow runs on every commit.
name: Tests on: [push] jobs: macos_tests: name: macOS Tests (SwiftPM) runs-on: macos-latest strategy: fail-fast: false matrix: xcode: ["11.4"] steps: - uses: actions/checkout@v2 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select --switch /Applications/Xcode_${{ matrix.xcode }}.app - name: Cache SwiftPM uses: actions/cache@v1 with: path: .build key: ${{ runner.os }}-xcode_${{ matrix.xcode }}-swiftpm-deps-${{ github.workspace }}-${{ hashFiles('Package.resolved') }} restore-keys: | ${{ runner.os }}-xcode_${{ matrix.xcode }}-swiftpm-deps-${{ github.workspace }} - name: SwiftPM tests run: swift test --enable-code-coverage - name: Convert coverage to lcov run: xcrun llvm-cov export -format="lcov" .build/debug/PersistPackageTests.xctest/Contents/MacOS/PersistPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: fail_ci_if_error: true xcode_tests: name: ${{ matrix.platform }} Tests (Xcode) runs-on: macos-latest strategy: fail-fast: false matrix: xcode: ["11.4"] platform: ["iOS", "tvOS"] steps: - uses: actions/checkout@v2 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select --switch /Applications/Xcode_${{ matrix.xcode }}.app - name: Cache SwiftPM uses: actions/cache@v1 with: path: CIDependencies/.build key: ${{ runner.os }}-xcode_${{ matrix.xcode }}-swiftpm-ci-deps-${{ github.workspace }}-${{ hashFiles('CIDependencies/Package.resolved') }} restore-keys: | ${{ runner.os }}-xcode_${{ matrix.xcode }}-swiftpm-ci-deps-${{ github.workspace }} - name: Cache DerivedData uses: actions/cache@v1 with: path: ~/Library/Developer/Xcode/DerivedData key: ${{ runner.os }}-${{ matrix.platform }}_derived_data-xcode_${{ matrix.xcode }} restore-keys: | ${{ runner.os }}-${{ matrix.platform }}_derived_data - name: Run Tests run: swift run --configuration release --skip-update --package-path ./CIDependencies/ xcutils test ${{ matrix.platform }} --scheme Persist --enable-code-coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: fail_ci_if_error: true watchos_build: name: watchOS Build (Xcode) runs-on: macos-latest strategy: fail-fast: false matrix: xcode: ["11.4"] steps: - uses: actions/checkout@v2 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select --switch /Applications/Xcode_${{ matrix.xcode }}.app - name: Cache SwiftPM uses: actions/cache@v1 with: path: CIDependencies/.build key: ${{ runner.os }}-xcode_${{ matrix.xcode }}-swiftpm-ci-deps-${{ github.workspace }}-${{ hashFiles('CIDependencies/Package.resolved') }} restore-keys: | ${{ runner.os }}-xcode_${{ matrix.xcode }}-swiftpm-ci-deps-${{ github.workspace }} - name: Cache DerivedData uses: actions/cache@v1 with: path: ~/Library/Developer/Xcode/DerivedData key: ${{ runner.os }}-watchOS_derived_data-xcode_${{ matrix.xcode }} restore-keys: | ${{ runner.os }}-watchOS_derived_data - name: Build for watchOS run: swift run --configuration release --skip-update --package-path ./CIDependencies/ xcutils build watchOS --scheme Persist linux_tests: name: SwiftPM on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-16.04, ubuntu-latest] swift: ["5.2.3"] steps: - uses: actions/checkout@v2 - name: Install swiftenv run: | eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" echo "::set-env name=SWIFTENV_ROOT::$HOME/.swiftenv" echo "::add-path::$SWIFTENV_ROOT/bin:$PATH" - name: swift test run: swift test --enable-test-discovery
The tests are split in to 4 sections:
- macOS tests, which are run via
swift test
- iOS and tvOS tests, which are run via Xcode
- watchOS build, which is run via Xcode but does not run tests because tests do not work on watchOS
- Linux tests, which are run via
swift test
on Ubuntu
macOS, iOS, and tvOS tests gather test coverage and upload it to Codecov, which provides some insight it to how much new code is covered by tests.
For the iOS and tvOS tests, along with the watchOS build, I used xcutils
. xcutils
is another tool of mine that is used to improve the CLI of Xcode. Here it is used to run the tests/build against the latest versions of iOS/tvOS/watchOS, which means it should work on any machine and is resistant to changes made by GitHub.
On Linux the --enable-test-discovery
flag is passed to swift test
to remove the need for a LinuxMain.swift
file that much be kept in sync with the tests.
These tests have helped me match many mistakes before merging, especially for platforms such as watchOS and Linux that are less frequently used.
Release Workflow
The release workflow is triggered by the creation of a git tag that starts with a v
.
name: Release on: push: tags: - "v*" jobs: create_release: name: Create Release runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Fetch tag run: git fetch --depth=1 origin +${{ github.ref }}:${{ github.ref }} - name: Get the release version id: release_version run: echo "::set-output name=version::${GITHUB_REF/refs\/tags\//}" - name: Get release description run: | description="$(git tag -ln --format=$'%(contents:subject)\n\n%(contents:body)' ${{ steps.release_version.outputs.version }})" # Fix set-output for multiline strings: https://github.community/t/set-output-truncates-multiline-strings/16852 description="${description//'%'/'%25'}" description="${description//$'\n'/'%0A'}" description="${description//$'\r'/'%0D'}" echo "$description" echo "::set-output name=description::$description" id: release_description - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.release_version.outputs.version }} release_name: ${{ steps.release_version.outputs.version }} body: ${{ steps.release_description.outputs.description }} prerelease: ${{ startsWith(steps.release_version.outputs.version, 'v0.') || contains(steps.release_version.outputs.version, '-') }} build_docs: name: Build Docs runs-on: macos-latest strategy: fail-fast: false matrix: xcode: ["11.4"] steps: - uses: actions/checkout@v2 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select --switch /Applications/Xcode_${{ matrix.xcode }}.app - name: Setup Ruby uses: ruby/setup-ruby@v1 - uses: actions/cache@v1 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('.ruby-version') }}-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems-${{ hashFiles('.ruby-version') }}- - name: Bundle install run: | bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - name: Build docs run: bundle exec jazzy - name: Upload Docs uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs
The first job creates a GitHub release for the tag. The release uses the contents of the tag as the body for the release, which allows for markdown, so I write the body of the tag using markdown to benefit from improved rendering on the GitHub website. The Get release description
step modifies the body by escaping new lines and %
characters. This is required to prevent the output being truncated. See https://github.community/t/set-output-truncates-multiline-strings/16852.
Since my releases follow sematic versioning 2.0.0 if the release starts with v0.
or contains a -
the release is marked as pre-release.
The second job runs jazzy
to build HTML docs and uploads it to a gh-pages
branch, which is configured to be deployed automatically by GitHub, and also provides a badge displaying the percentage of public code that is documented.
Final Thoughts
With these workflows in place I can make a change, add some tests, push, create a pull request, merge, and tag a new release with confidence and all within a couple of hours.
Since this workflow will be receiving small tweaks over time and I may not remember to update this workflow straight away (maybe I should make a workflow for that 🤪) you should check out the Persist workflows to find my latest changes.