Building Closed-Source Binaries with GitHub Actions
Nov 9, 2025 ·
Using GitHub Actions is a great way to automate your build pipeline. In this post, we’ll take a look at how to use GitHub Actions to build distribution binaries for a closed-source Swift package.
Why using a cloud service to build distribution binaries?
Before we start looking at how to use GitHub Actions to build your distribution binaries, let’s discuss why you may want to consider this instead of a local build setup.
Building the binaries with a cloud service like GitHub Actions lets you decouple your release process from your local environment. It also reduces the risk of malware affecting the build.
You naturally don’t have to use GitHub Actions for this. You can use Xcode Cloud or another service with similar capabilities. While you’d have to adjust the workflow, most of the steps below still apply.
How to set up a closed-source Swift package
Setting up a closed-source Swift package involves a few steps. The details are beyond the scope of this post, but you can watch my talk from iOSKonf 25 for more information.
Basically, you’d have to set up a public distribution repository, and a private source code repository. I also have separate repositories for build binaries and for the online documentation.
When releasing a new version of a closed-source package, the private source code is compiled to an XCFramework. You can also generate dSYMs for each release to enable symbolicated crash logs.
We’ll not take a look at how we can use GitHub Actions to build the XCFramework and dSYMs for us.
Step 1 - Generating code signing certificates
If you currenly make your binary builds locally, you may have to configure your closed-source Xcode project to use manual code signing instead of automatic, to let us archive with GitHub Actions.
To do this, toggle off the “Automatically manage signing” checkbox and select “Apple Distribution” as the signing certificate, with the Signing Certificate picker.

If your project supports multiple platforms, you must set a distribution certificate for each platform. In this case, we can just use the same certificate for all platforms:

When you select “Apple Distribution” for the first time, Xcode will open the certificate generator and guide you through all the steps. Make sure that you generate a distribution certificate.
Once the certificate is generated, you can right-click the certificate file and export it as a .p12 file. You can export it anywhere, for instance to the Desktop:

If you have to enter a password when exporting the distribution certificate, it’s very important that you remember it, since we will need it later.
Step 2 - Setting up GitHub repository secrets
This article describes how to set up the repository secrets that are required to build the distribution binaries. We just need the Base64 .p12 file content, the .p12 file secret, and a keychain password:

These secrets will be used by the workflow to allow GitHub Actions to archive the source-code with proper code signing.
Step 3 - Setting up build scripts
Even though GitHub Actions makes it easy to automate build processes, you still need build scripts.
I have an open-source project that provides various scripts for open-source and closed-source Swift packages, as well as GitHub Actions workflows templates for common tasks.
Our workflow will use the validate-release script to ensure that the code is ready for release, and the framework script to generate the binaries. Have a look at SwiftPackageScripts for more scripts.
In the workflow below, we’ll assume that the required build scripts are in the /scripts root folder.
Step 4 - Setting up the GitHub Actions workflow
To set up the GitHub Actions workflow that will be used to build our distribution binaries, create a .github/workflows folder in the project root and add a binaries.yml file to it.
Since the GitHub article doesn’t include the build script used to generate the required binaries, the workflow below contains everything you need, provided that you use SwiftPackageScripts.
First, add this to the workflow file to give it a name, specify that we will trigger it manually, and that it should run on macOS 15. We also specify the framework name as a variable.
name: Create Binary Artifacts
on:
workflow_dispatch: # Manual trigger
# Or add other triggers like:
# push:
# tags:
# - 'v*'
jobs:
build:
runs-on: macos-15 # macos-latest
env:
FRAMEWORK_NAME: VietnameseInput
Let’s now add a build step that checks out the code, and one that uses the repository secrets that we registered earlier to set up the distribution certificate:
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up certificate
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
With the distribution certificate added to the keychain, we can now create the XCFramework binary.
Here, we specify the Xcode version, then validate the project and generate the distribution binaries:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.4 # latest-stable doesn't currently work
- name: Validate Project
run: ./scripts/validate_release.sh --swiftlint 0
- name: Generate distribution binaries
run: ./scripts/framework.sh -p iOS --dsyms 1
Since the validation script is called without an explicit --platform, the project will be validated for all platforms. We also disable swiftlint, since GitHub doesn’t have that tool as a Terminal script.
The framework script is called with an explicit --platform/-p argument, to make it generate an iOS-explicit binary. We also pass in --dsyms 1 to generate dSYMs for the framework.
We finally upload the generated XCFrameworks and dSYMs zip files, then calculate and upload the checksum that is needed by the Swift Package Manager:
- name: Upload XCFramework zip
uses: actions/upload-artifact@v4
with:
name: $
path: .build/$.zip
if-no-files-found: error
- name: Upload dSYMs zip
uses: actions/upload-artifact@v4
with:
name: $-dSYMs
path: .build/$-dSYMs.zip
if-no-files-found: error
- name: Compute Checksum
run: |
CHECKSUM=$(swift package compute-checksum .build/$.zip)
echo "$.zip checksum: \`$CHECKSUM\`" >> $GITHUB_STEP_SUMMARY
echo "$CHECKSUM" > checksum.txt
- name: Upload checksum
uses: actions/upload-artifact@v4
with:
name: $-checksum
path: checksum.txt
We can now push the workflow to GitHub and run it from the Actions tab. The workflow was set up to require manual builds, but you can use on: to define automated triggers.
If the workflow finishes successfully, it outputs the generated checksum and lists all generated files:

You should download the binary artifact files and publish them somewhere else, since they are only available for a limited time.
I have a separate binary repository for each project, where I create a release tag for each release and uploads the binary files there before creating the Swift package release.
Conclusion
GitHub Actions makes it easy to automate your binary distribution file generation. Since I find the workflow file format a bit confusing, I hope you found this article helpful.
Discussions & More
If you found this interesting, please share your thoughts on Bluesky and Mastodon. Make sure to follow to be notified when new content is published.