Publish DocC to GitHub Pages with GitHub Actions
In this post, we’ll see how we can use GitHub Actions to automatically build and publish a Swift Package’s DocC docs to GitHub Pages, every time we push to a specific branch.
The workflow presented in this post is heavily inspired from @maxxfrazer’s Medium post, so if you like this, make sure to jump over there and give it a like.
Background
I used to have DocC generation as part of my standard open-source workflow, where every new version required the code to pass linting, testing, and DocC generation.
For every new version, I would then take the generated docs and move them to a different folder that pushed to the gh-pages
branch of that repository.
Since these docs can grow huge (some are around 300MB), I used git amend
to only get a single commit for the gh-pages
branch.
This has been tedious and error-prone, since each release has consisted of many manual steps. Since I have many projects, the amount of manual work addded up.
I was therefore very happy to see that GitHub now lets you use GitHub Actions to generate new GitHub Pages every time you push new changes. Let’s see how it works.
GitHub Settings
In your GitHub repository dashboard, go to Settings
then select Pages
in the side menu.
Under Build and deployment
, you can now select your GitHub Pages Source
. Switch from Deploy from a branch
to GitHub Actions
.
DocC Publish Workflow
With this, I could delete my gh-pages
branch, remove all DocC code from my Fastfile
, and replace the Fastlane script with this workflow, placed in .github/workflows/docc.yml
:
name: DocC Runner
on:
push:
branches: ["main"]
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
# A single job that builds and deploys the DocC documentation
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Select Xcode 15.1
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.1.0'
- name: Build DocC
run: |
swift package resolve;
xcodebuild docbuild -scheme ApiKit -derivedDataPath /tmp/docbuild -destination 'generic/platform=iOS';
$(xcrun --find docc) process-archive \
transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/ApiKit.doccarchive \
--output-path docs \
--hosting-base-path 'ApiKit';
echo "<script>window.location.href += \"/documentation/apikit\"</script>" > docs/index.html;
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'docs'
- id: deployment
name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
The job sets up the environment & token permissions, specifies that is must run on macOS 14 and Xcode 15.1, then builds and uploads a new DocC build.
The DocC build step currently only builds for iOS, but you can add more xcodebuild
rows to include more platforms. It adds a redirect to the root folder, then deploys the doc
folder.
The DocC build step also adds a JavaScript redirect to the root index.html
, to redirect it to the generated documentation. Without this, the root page would just show a blank page.
Build Runner Workflow
Besides this DocC workflow, I also have a .github/workflows/build.yml
workflow that will trigger on every new push and pull request, and build the package for all platforms:
name: Build Runner
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
SCHEME: ApiKit
jobs:
build:
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.1.0'
- name: Build iOS
run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=iOS';
- name: Build macOS
run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=OS X';
- name: Build tvOS
run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=tvOS';
- name: Build watchOS
run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=watchOS';
- name: Build visionOS
run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=xrOS';
- name: Test iOS
run: xcodebuild test -scheme $SCHEME -derivedDataPath .build -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' -enableCodeCoverage YES;
The workflow has individual build steps for iOS, macOS, tvOS, watchOS and visionOS, to ensure that the package builds for all supported platforms.
If your package only supports one or a few platforms, make sure to adjust the build steps to only include the platforms that your package supports.
Fastlane Version Bump Script
With these GitHub Action workflows in place, we still need to have a build & test runner in the Fastlane version script, to avoid faulty versions.
This is the new Fastlane file that I will use in all my open-source projects. It’s basically just defining a version bump script (lane) that uses other lanes to ensure that the code is legit.
fastlane_version "2.129.0"
default_platform :ios
platform :ios do
name = "PACKAGE_NAME"
main_branch = "main"
# Build ==================
lane :build do |options|
platform = options[:platform]
sh("cd .. && xcodebuild -scheme " + name + " -derivedDataPath .build -destination 'generic/platform=" + platform + "';")
end
lane :build_all do
build(platform: "iOS")
build(platform: "OS X")
build(platform: "tvOS")
build(platform: "watchOS")
build(platform: "xrOS")
end
# Test ==================
lane :test_ios do
sh("cd .. && xcodebuild test -scheme " + name + " -derivedDataPath .build -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' -enableCodeCoverage YES;")
end
# Version ================
desc "Create a new version"
lane :version do |options|
version_validate
type = options[:type]
version = version_bump_podspec(path: 'Version', bump_type: type)
git_commit(path: "*", message: "Bump to #{version}")
add_git_tag(tag: version)
push_git_tags()
push_to_git_remote()
end
desc "Validate that the repo is valid for release"
lane :version_validate do
ensure_git_status_clean
ensure_git_branch(branch: main_branch)
swiftlint(strict: true)
build_all
test_ios
end
end
It defines a build
lane that build the package for any platform, as well as an iOS test lane.
The version
lane calls version_validate
to check that the git repo status is clean, that it’s on the correct branch, and that swiftlint
passes, then calls build_all
and test_ios
.
Conclusion
With the new GitHub Action workflows, I don’t have to manually handle DocC. Every push to main
automatically publishes updated DocC docs to GitHub Pages.
I also get continuous build and test checks with the build
workflow, which builds and tests all supported platforms on every new push to main
.
I found GitHub Actions trickier to set up than e.g. Bitrise, but once in place it integrates well with pull requests and other open-source events.
I have migrated all open-source projects from my manual workflow to this fully automated approach. It’s very nice to have it in place, and saves me a lot of time.
Discussions & More
If you found this interesting and would like to share your thoughts, please comment in the Disqus section below or reply to this tweet or this toot.
Follow on Twitter and Mastodon to be notified when new content & articles are published.