Build, Sign and Deliver Flutter MacOS Desktop Applications on GitHub Actions

Muhammed Salih Guler
Flutter Community
Published in
11 min readFeb 1, 2021
https://unsplash.com/photos/SRjZtxsK3Os

At Superlist we have been developing our Flutter applications for all platforms with a focus on Desktop. That is why we wanted to automate our build and signing process for our macOS application. Since we are using GitHub Actions for our CI/CD system, the obvious option was to automate it over GitHub Actions.

I started to do some research and realized that besides a handful of resources about signing and building for native macOS apps or just over Xcode.

That is why I wanted to put down my guide for you, so other people can save some time by following it. :)

My research on the matter :)

Apple started a new system called Notarizing, for checking out the macOS applications for malicious software, with this blog post, we will not cover that.

Creating the Certificate for Signing

Start off by going to Certificate Creation Page, and create a Developer ID Application.

Once you click on creating Developer ID Application the website will ask you to create a Certificate Signing Request (CSR).

Developer ID Application should be created by the Account Holder. If you can not create the certificate, please reach out to your Account Holder :)

Open your Keychain Access, navigate yourself to Certificate Assistant -> Request a Certificate From a Certificate Authority…

With the opening dialog above, fill in the information with whoever is creating the certificate. One important topic is, check the "Saved to Disk" option and download the signing request. Go back to the website and upload the request.

The next step is downloading and adding the certificate to your local machine.

Adding the Certificate to Your Local Machine

Click on the certificate that you downloaded and it should automatically add it to your keychain.

Open your Keychain Access, find the certificate you added. It should be under "My Certificates". Click the arrow on the left side of the certificate and expand it.

Pick both options and right-click on them. Pick "Export 2 Items…" option. It should open the dialog to download a p12 file.

It should ask you to add a password to your p12 file. Note that down, we will be needing it later on.

That is all for the certificates. Let's move on the GitHub Actions.

Creating the GitHub Actions Steps

For creating GitHub Actions, you need to create a folder called .github (if it doesn't exist). Under that folder, create a folder called workflows (if it doesn't exist).

GitHub Actions uses the YAML format for defining the workflow. For creating a workflow, you need to create a YAML format file under workflows. Let's name it on_macos_deploy.yml.

Let's start by adding a trigger to start the workflow.

#.github/workflows/on_macos_deploy.yml
---
name: On MacOS Deploy
'on':
push:
branches:
- "main"

What we do above is, name the workflow under name. After that, we can decide on when the workflow should happen by writing on keyword. push keyword is used for defining the operation, it can also be pull-request for defining the trigger type. After that, branches will define the limit for which branches should this workflow happen.

Adding Flutter and Configuring GitHub Actions for Flutter Desktop

Under GitHub Actions, each individual operation is under jobs. Each job can have a name right after it. You also need to define the machine that it runs. It should be a macOS because we need to have Xcode build tools for building Apple product-related applications.

Add the following to the on_macos_deploy.yml file to create the job and set the machine.

#.github/workflows/on_macos_deploy.yml

jobs:
on-push-main:
runs-on: macos-latest
env:
MACOS_APP_RELEASE_PATH: build/macos/Build/Products/Release

With the same indentation as runs-on we need to add a GitHub Action for adding Flutter. At the moment, you should be using dev channel for Desktop support.

MACOS_APP_RELEASE_PATH variable is the path where the release is going to build. Since it is used multiple times, it made sense to move it to an environment variable.

There is a Flutter Action for having the Flutter in the GitHub Actions. You need to add the following for having Flutter action.

#.github/workflows/on_macos_deploy.yml
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: subosito/flutter-action@v1
with:
channel: dev

The following two commands are there to enabling the macOS Desktop support and building the application for release.

#.github/workflows/on_macos_deploy.yml
- name: Enable Macos
run: flutter config --enable-macos-desktop
- name: Build macOS app
run: flutter build macos --release

Signing the macOS Desktop Application

For signing our application, you need to remove the auto signing specifications for our application. You should do that because the app signing process should be part of the GitHub Actions and it should use the certificate that we created. For that, you need to open your Xcode (Quoted from StackOverflow answer).

  1. Select your project in the project navigator.
  2. Select your app in the list of targets.
  3. Click “Build Settings”.
  4. Click “All”.
  5. Click “Levels”.
  6. Type “identity” into the search field.

Click on the Code Signing Identity row, under the column for your app target (labeled “test” in my example). That cell of the table might appear empty.

In the pop-up menu that appears, choose “Other…”.

In the popover text box that appears, delete all text so the box is empty.

Press return to dismiss the popover. This way your application will not be signed.

Now it is time to add the certificate to your repository. Obviously, we do not want to add the certificate to our repository. We will add the certificate to the repository secrets.

We will encode the certificate in base64 format. Open the Terminal and write the following.

base64 Certificates.p12 | pbcopy

This will encode the certificate, turn it into a string, and copy it.

Now navigate to your repository on GitHub. Go to Settings -> Secrets.

Click on New Repository Secret button.

Write the name of the secret as MACOS_CERTIFICATE (or whatever you prefer). Paste the copied base64 encoded to value section. You are all set now. Click on Add Secret button.

After this, we will add another secret like described above. Write the name of the secret as MACOS_CERTIFICATE_PWD (or whatever you prefer). Paste the previously saved certificate password to value section. You are all set now. Click on Add Secret button.

Now that we added the secrets, let's use them. Let's start by creating a new step in our job.

- name: Codesign executable
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}

This way we are assigning the values in secrets into variables. The next step is; using them.

One thing about certificates is, they need to be part of your keychain, and this way they can be used to sign the macOS applications.

There are several different keychains such as login, default, etc. What we are going to do is, we will create a new keychain and define it as our default keychain during the signing.

For protecting your data, macOS put an abstraction layer on your keychain and you can use security keyword for making changes over your keychain.

Let's start off by copying our certificate to its own file.

echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12

This will retrieve the certificate string decode it and write it to certificate.p12 file. Now that we created the certificate, it is time for using it.

#1
security create-keychain -p <YOUR-PASSWORD> build.keychain
#2
security default-keychain -s build.keychain
#3
security unlock-keychain -p <YOUR-PASSWORD> build.keychain
#4
security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
#5
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <YOUR-PASSWORD> build.keychain
#6
security find-identity
#7
/usr/bin/codesign --force --deep -s <YOUR-IDENTITY> ./$MACOS_APP_RELEASE_PATH/<YOUR-APP-NAME>.app

What you are going to do above as follows:

1. You are creating a new keychain with a password. You can pick whatever password you prefer for it.
2. You are setting your new keychain into the default keychain.
3. Now you are unlocking your keychain with your defined password.
4. With the unlocked keychain, you can now import your password. With the defined password for the certificate, you are adding it to the keychain and making it available for codesign.
5. You are defining the partition list to be used with your keychain here. This way you are preventing some possible prompt windows for your application.
6. This step is optional but needs to be done either on your local machine or your CI/CD system. When you add your certificates, you are adding your identities to the system as well. For signing the application you need to have the identity number

You can find the <YOUR-IDENTITY> at the place that your IDENTITY ID is shown above.

7. Lastly, you sign the application that is defined in your path. And your application is signed. :)

Now that you signed the application, now it is time to deliver this application to the users/testers with GitHub Actions.

Creating a .dmg file

Right now, one of the most common ways to share an application is to create a dmg format file. For that purpose, we can use an open-source shell-script called create-dmg.

Let's start off by creating an extra step. You need to install the shell script from the brew.

- name: Create a dmg
run: |
echo "Install create-dmg"
brew install create-dmg

Adding as a follow-up to the brew installation, you need to direct yourself to the applications folder.

cd $MACOS_APP_RELEASE_PATH

create-dmg \
--volname "<YOUR-APP-NAME>" \
--window-pos 200 120 \
--window-size 800 529 \
--icon-size 130 \
--text-size 14 \
--icon "<YOUR-APP-NAME>.app" 260 250 \
--hide-extension "<YOUR-APP-NAME>.app" \
--app-drop-link 540 250 \
--hdiutil-quiet \
"<YOUR-APP-NAME>.dmg" \
"<YOUR-APP-NAME>.app"

With the code above, after you direct yourself to the release path, you are creating the dmg. The shell-script is giving you a chance to customize everything. Including background images, icons, etc. You might check the documentation here for more information.

Creating a GitHub Release, Uploading Artifacts, and BONUS: Generating a version number

For creating a release for our application, you will be using the GitHub Action from the GitHub team create-release. That will automatically help you to create a tag, a release out of it, and release notes if you want to have it. Start off by creating another GitHub Actions step.

- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: <YOUR-TAG-NAME>
release_name: Release <YOUR-RELEASE-NUMBER>
body: |
Whatever you want as release note.
draft: false
prerelease: false

As you see, you can define your tag and release number as well as your release notes by simply filling out the information above. One good thing about using this action is, it also automatically provides a link to the release so you can share it after you upload your artifacts.

For uploading artifacts, you will be using another GitHub Action from the GitHub team called upload-release-asset.

- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: <Asset-Path-to-dmg>
asset_name: <Name-of-the-output>.dmg
asset_content_type: application/octet-stream

With the above code, you are defining an action to upload the signed dmg to the release. You can name your asset in any way you want. Asset content type is the mime type of the artifact and in the case of dmg, it is application/octet-stream.

When you run these steps, it will help you to upload the signed asset to your release.

But, we are not done yet, I wanted to show you an additional step to create a release number out of the number of commits as metadata.

As a first step, you need to add an extra step before, the "Create Release"

- name: Create Version Number
id: versions #1
run: |
git fetch #2
VERSION_WITHOUT_SUFFIX="$(grep 'version:' pubspec.yaml | awk '{ print $2 }' | cut -d'+' -f 1)" #3
function parse_git_hash() {
git rev-list --count origin/main
} #4
MAIN_COUNT=$(parse_git_hash) #5
APP_VERSION="$VERSION_WITHOUT_SUFFIX+$MAIN_COUNT" #6
echo "::set-output name=version::$(echo $APP_VERSION)" #7

Let's go through the steps together:

  1. When you create a GitHub Actions which outputs some value, you are required to add an id to the action. That is why you are adding this here as well.
  2. For getting the latest merged version of the branch, you need to fetch the latest updates.
  3. This step is necessary only if you have a version with x.x.x+x format.
    Versioning in Flutter happens over the pubspec.yaml file with a version number that is using semantic versioning. In your case, you will get the version and split the metadata part, and add your metadata.
  4. This is a simple function to get the number of commits to the main branch.
  5. Assigning the number of commits to a variable
  6. Creating the app version with the metadata you decided to use.
  7. Outputs the data defined in step 6 with the name version.

Now that you have the version number let's update your release tag and release name as the following

tag_name: ${{ steps.versions.outputs.version }}
release_name: Release ${{ steps.versions.outputs.version }}

This way you will have dynamic versioning that would be increasing with each merge to the main branch.

I know this was a long journey but trust me, you have saved a lot of time by having everything in one place defined for you. :) I went through some amazing resources that I referenced or went through which I will be sharing below, in case you want to learn more about the topic.

You can find the full example here.

Thank you for reading and let me know if you have any questions!

Follow me on Twitter @salihgueler!

Resource and References:

--

--