Building Closed-Source Binaries with GitHub Actions

Nov 9, 2025  ·  swift sdks automation

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.

A screenshot of how to change Xcode code signing from Automatic to Manual

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:

A screenshot of how to change Xcode code signing for all platforms from Automatic to Manual

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:

A screenshot of the exported p12 file

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:

A screenshot of the GitHub secrets page

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:

A screenshot of the GitHub result screen

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.