Skip to main content

macOS

macOS applications are distributed as .app bundles. Despite the .app extension, a bundle is actually a directory with a defined structure that macOS treats as a single launchable application. It contains your compiled executable, resources such as icons and images, and metadata that tells macOS how to run and display your app. To make .NET and Avalonia projects work in a .app bundle, some extra work is required after publishing.

Use Parcel for easier packaging

Parcel automates macOS packaging for Avalonia apps. It handles bundle structure, Info.plist generation, code signing, notarization, and DMG creation from a single command. Parcel can also create macOS bundles from Windows and Linux. If you'd prefer to avoid the manual steps below, see the Parcel setup guide to get started.

Manual packaging

The rest of this page covers how to create, sign, notarize, and distribute a .app bundle manually.

An Avalonia .app bundle has the following structure:

MyProgram.app
Contents
_CodeSignaturestores code signing information
CodeResources
MacOSall your DLL files, etc. -- the output of dotnet publish
MyProgram
MyProgram.dll
Avalonia.dll
Resources
MyProgramIcon.icnsicon file
Info.pliststores information on your bundle identifier, version, etc.
embedded.provisionprofilefile with signing information

The Info.plist file is an XML property list that acts as the bundle's manifest. It declares your app's identity, version, icon, executable name, and other metadata that macOS reads at launch. Every .app bundle must include one.

Making the application bundle

There are a few options available for creating the .app file/folder structure. You can do this on any operating system, since a .app file is just a set of folders laid out in a specific format and the tooling isn't specific to one operating system. However, if you build on Windows outside of WSL, the executable may not have the right attributes for execution on macOS, and you may have to run chmod +x on the published binary output from a Unix machine. This is the binary output that ends up in the folder MyApp.app/Contents/MacOS/, and the name should match CFBundleExecutable.

The .app structure relies on the Info.plist file being properly formatted and containing the right information. Use Xcode to edit Info.plist as it has auto-completion for all properties. The following keys are required:

KeyDescriptionExample
CFBundleExecutableMust match the binary name from dotnet publish (your assembly name without .dll).MyApp
CFBundleNameDisplay name for your application (max 15 characters).My App
CFBundleDisplayNameFull display name. Only needed if CFBundleName exceeds 15 characters.My Application
CFBundleIconFileName of your .icns icon file, including extension.MyApp.icns
CFBundleIdentifierUnique identifier in reverse-DNS format.com.myapp.macos
NSHighResolutionCapableEnables Retina display support. Set to <true/>.true
CFBundleVersionInternal build version number.1.4.2
CFBundleShortVersionStringUser-visible version string.1.4.2

If you need to register custom URL schemes or file type associations, see the macOS platform integration guide.

For a complete list of Info.plist keys, see Apple's Information Property List reference.

If at any point the tooling gives you an error that your assets file doesn't have a target for osx-64, add the following runtime identifiers to the top <PropertyGroup> in your .csproj:

<RuntimeIdentifiers>osx-x64</RuntimeIdentifiers>

Add other runtime identifiers as necessary. Each one should be separated by a semicolon (;).

Creating icon files

This type of icon file can be created not only on Apple devices but also on Linux devices.
You can find more information about how you can achieve that in this blog post:
Creating macOS Icons (icns) on Linux

About the .app executable file

The file that is actually executed by macOS when starting your .app bundle will not have the standard .dll extension. If your publish folder contents, which go inside the .app bundle, do not have both a MyApp (executable) and a MyApp.dll, things are probably not generating properly, and macOS will probably not be able to start your .app properly.

Some recent changes in the way .NET Core is distributed and notarized on macOS have caused the MyApp executable (also called the "app host" in the linked documentation) to not be generated. You need this file to be generated in order for your .app to function properly. To make sure this gets generated, do one of the following:

  • Add the following to your .csproj file:
<PropertyGroup>
<UseAppHost>true</UseAppHost>
</PropertyGroup>
  • Add -p:UseAppHost=true to your dotnet publish command.

Additionally, you might want to add the '-p:PublishSingleFile=true' to your dotnet command, compiling most of the DLLs into a single application, simplifying the signing and notarizing process.

Manual

First, publish your application (dotnet publish documentation):

dotnet publish -r osx-x64 --configuration Release -p:UseAppHost=true

Create your Info.plist file, adding or modifying keys as necessary:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIconFile</key>
<string>myicon-logo.icns</string>
<key>CFBundleIdentifier</key>
<string>com.identifier</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
<key>CFBundleExecutable</key>
<string>MyApp.Avalonia</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

You can then create your .app folder structure as outlined at the top of this page. If you want a script to do it for you, you can use something like this (macOS/Unix):

#!/bin/bash
APP_NAME="/path/to/your/output/MyApp.app"
PUBLISH_OUTPUT_DIRECTORY="/path/to/your/publish/output/net10.0/osx-x64/publish/."
# PUBLISH_OUTPUT_DIRECTORY should point to the output directory of your dotnet publish command.
# One example is /path/to/your/csproj/bin/Release/net10.0/osx-x64/publish/.
# If you want to change output directories, add `--output /my/directory/path` to your `dotnet publish` command.
INFO_PLIST="/path/to/your/Info.plist"
ICON_FILE="/path/to/your/myapp-logo.icns"

if [ -d "$APP_NAME" ]
then
rm -rf "$APP_NAME"
fi

mkdir "$APP_NAME"

mkdir "$APP_NAME/Contents"
mkdir "$APP_NAME/Contents/MacOS"
mkdir "$APP_NAME/Contents/Resources"

cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
cp "$ICON_FILE" "$APP_NAME/Contents/Resources/$ICON_FILE"
cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"

If you created the .app on Windows, make sure to run chmod +x MyApp.app/Contents/MacOS/AppName from a Unix machine. Otherwise, the app will not start on macOS.

Signing your app

Once you have your .app file created, you'll probably want to sign your app so that it can be notarized and distributed to your users without Gatekeeper giving you a hassle. Notarization is required for apps distributed outside the App Store starting in macOS 10.15 (Catalina), and you'll have to enable hardened runtime and run codesign on your .app to notarize it successfully.

Hardened runtime is a macOS security policy that restricts what your app can do at runtime. It prevents things like code injection, unsigned library loading, and access to certain system resources unless you explicitly opt in via entitlements. Apple requires hardened runtime for all notarized apps.

You'll need a Mac computer for this step, to run the codesign command line tool that comes with Xcode.

Obtaining a Developer ID certificate

If you wish to notarize an app and distribute it outside the Mac App Store, you need a Developer ID Application certificate in your login keychain. Without it, codesign has nothing to sign with and distribution is impossible.

Note that Xcode defaults to cloud-managed signing certificates, which can be awkward to use with .NET command line tooling. Follow these steps to install a local certificate that codesign can use directly. How you install it depends on your role in the Apple Developer account.

If you own the account

  1. Open Xcode.
  2. Go to Xcode → Settings → Accounts.
  3. Add your Apple ID if it isn't already listed.
  4. Select your team and choose Download Manual Profiles. This downloads the certificate into your keychain.
  5. To share the certificate with another developer, find it in Keychain Access, right-click, and choose Export. This generates a password-protected .p12 file.

If you do not own the account

  1. Ask the account owner to export the Developer ID Application certificate as a .p12 file. (See above)
  2. Open Keychain Access and go to File → Import Items.
  3. Select the exported .p12 file and enter the password.

Verify the certificate is installed by running:

security find-identity -v

You should see a Developer ID Application identity listed. Its team ID appears in parentheses after the name. For example, in Developer ID Application: My Company (ABC123DEF5) the team ID is ABC123DEF5. You'll need it when notarizing.

Running codesign and enabling hardened runtime

Enabling hardened runtime is done in the same step as codesigning. You have to codesign everything in the .app bundle under the Contents/MacOS folder, which is typically done with a script since there are a lot of files. To sign your files, you need an Apple developer account.

To notarize your app, you need a Developer ID certificate, which requires a paid Apple developer subscription. See Obtaining a Developer ID certificate for instructions on how to install a certificate locally.

You'll also need to have the Xcode command line tools installed. You can get those by installing Xcode and running it or by running xcode-select --install on the command line and following the prompts to install the tools.

First, enable Hardened Runtime with exceptions by creating an MyAppEntitlements.entitlements file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>

Then, run this script to do all the code signing for you:

#!/bin/bash
APP_NAME="/path/to/your/output/MyApp.app"
ENTITLEMENTS="/path/to/your/MyAppEntitlements.entitlements"
SIGNING_IDENTITY="Developer ID: MyCompanyName" # matches Keychain Access certificate name

find "$APP_NAME/Contents/MacOS/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$fname"
fi
done

echo "[INFO] Signing app file"

codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$APP_NAME"

The --options=runtime part of the codesign line is what enables the hardened runtime with your app. Because .NET Core may not be fully compatible with hardened runtime, it is advisable to add exceptions to use JIT-compiled code and allow for Apple Events to be sent. The JIT-compiled code exception is required to run Avalonia apps under hardened runtime. The second exception for Apple Events fixes an error that shows up in Console.app.

Note: Microsoft lists some other hardened runtime exceptions as being required for .NET Core. The only one that is actually needed to run an Avalonia app is com.apple.security.cs.allow-jit. The others may impose security risks with your application. Use with caution.

Don't use the --deep flag

Some guides suggest signing the whole bundle in a single pass with codesign --deep. Apple recommends avoiding --deep because it can leave nested binaries improperly signed. Sign each item explicitly instead, as the script above does, by walking the files first and signing the bundle last.

Once your app is code signed, you can verify that it signed properly by making sure that the following command outputs no errors:

codesign --verify --verbose /path/to/MyApp.app

Notarizing your software

Notarization is an automated process where you upload your signed app to Apple's servers for a security scan. Once approved, Apple issues a ticket that confirms your software has been checked for malicious content. When a user downloads your app, macOS verifies this ticket and allows the app to launch without showing an "unidentified developer" warning. Notarization is required for all apps distributed outside the Mac App Store since macOS 10.15 (Catalina).

Notarization uses the notarytool command line tool, which ships with Xcode 13 and later. (The older altool --notarize-app workflow has been retired by Apple and no longer works.)

Because Apple requires multi-factor authentication on developer accounts, notarytool authenticates with an app-specific password that you generate on the Apple ID account page. Store it once in your keychain as a named profile so you don't have to pass credentials on every run:

xcrun notarytool store-credentials "AC_PASSWORD" \
--apple-id "[email protected]" \
--team-id "YOURTEAMID" \
--password "your-app-specific-password"

AC_PASSWORD is the profile name you choose, --team-id is the team ID from App Store Connect, and --password is the app-specific password you generated.

With the profile stored, notarize your app:

  1. Make sure your .app is code signed properly.

  2. Put your .app in a .zip file. Apple recommends using ditto for this step because the zip command can cause notarization issues: ditto -c -k --sequesterRsrc --keepParent MyApp.app MyApp.zip

  3. Submit the archive and wait for the result. The --wait flag polls automatically and returns once notarization succeeds or fails, so no separate status check is needed:

    xcrun notarytool submit MyApp.zip --keychain-profile "AC_PASSWORD" --wait
  4. If a submission is rejected, fetch the detailed log using the submission ID from the output: xcrun notarytool log <submission-id> --keychain-profile "AC_PASSWORD"

  5. If successful, staple the notarization ticket to the app with xcrun stapler staple MyApp.app. You can validate this by running xcrun stapler validate MyApp.app.

Once notarization is complete, you should be able to distribute your application.

Notarizing for .dmg distribution

If you distribute your app as a .dmg, you must perform an additional notarization step on the .dmg itself.

  1. Notarize your .app as normal, in a .zip file.
  2. Add your notarized and stapled (xcrun stapler) app into the .dmg.
  3. Notarize your .dmg file using the same xcrun notarytool submit command. This time, point it at the .dmg file instead of the .zip.
  4. Staple the notarization to the .dmg file: xcrun stapler staple MyApp.dmg.

App Store packaging

You need a lot of things:

  • Your app satisfies App Store Review Guidelines.
  • Your app satisfies macOS Human Interface Guidelines.
  • Apple Developer Account, with your Apple ID connected to it.
  • Your app is registered in App Store Connect.
  • Transporter app installed from App Store.
  • Latest Xcode installed with your Apple ID authorized into it.
  • Two certificates: 3rd Party Mac Developer Installer for signing .pkg file and 3rd Party Mac Developer Application for signing a bundle.
  • App Store Provision Profile. Get it for your app from the Apple Developer provisioning profiles page.
  • Two entitlements: One for signing .app and the other for signing app helpers.
  • Your app content is bundled correctly.
  • Your bundle is signed correctly.
  • Your .dylib files don't contain any non-ARM/x64 architectures. You can remove these by using the lipo command line tool.
  • Your app is ready to run inside the macOS sandbox (see the Sandbox section below).

Getting certificates

  • Go to Xcode → Settings → Accounts → Manage Certificates...
  • Add them if they do not exist.
  • Export them with a password.
  • Open them and import into KeyChain Access.
  • In KeyChain you should see these certificates: 3rd Party Mac Developer Installer and Apple Distribution. If cert names start with another string, you may have created the wrong certificate. Try again.
  • Expand imported keys in KeyChain and double click on a private key inside.
  • Go to Access Control Tab.
  • Select Allow all applications to access this item in case you don't want to enter a Mac profile password for every file sign.

Sandbox and bundle

The App Store requires apps to run inside a sandbox. Sandboxing is a security mechanism that isolates your app from the rest of the system. It restricts access to the file system, network, hardware, and other apps unless you explicitly request permission via entitlements. This protects users from malicious or buggy software.

Your app must be prepared for this restricted environment and must not crash if a folder or resource is inaccessible.

If your app crashes when running inside the sandbox, try publishing with the single file option enabled. Example:

dotnet publish src/MyApp.csproj -c Release -f net10.0 -r osx-x64 --self-contained true -p:PublishSingleFile=true

Your app content must be bundled correctly. macOS expects specific file types in specific directories within the bundle.

The most important rules:

  • .dll files are not considered code by Apple. So they should be placed inside the /Resources folder and do not need to be signed.
  • /MacOS files should contain only executable mach-o, i.e., your app executable and any other helper executables.
  • All other mach-o .dylib files should be inside the Frameworks/ folder.

To satisfy this requirement, you can use relative symlinks from the MacOS/ folder to Resources/ and Frameworks/ folders. As an example:

ln -s fromFile toFile

Also it's better to rewrite your app's resources access scheme to directly access Resources/ folder without using any symlinks, because over symlinks you might get I/O access issues in sandbox.

Sandbox entitlements and signing

Entitlements are key-value pairs that declare the specific capabilities your app needs, such as network access, file system access, or JIT compilation. You declare them in an entitlements .plist file, and macOS enforces them at runtime. Choose only the entitlements your app actually requires.

The first entitlements file is for signing all helper executables inside the .app/Contents/MacOS/ folder. It should look like this.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>

The second is for signing the app executable and the whole app bundle. It should contain all of the app's permissions. Here is an example:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>com.apple.coreservices.launchservicesd</string>
</array>
</dict>
</plist>

Here are some optional permissions your app may need:

    <key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.document-scope</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>[Your Team ID].[Your App ID]</string>
</array>

Packaging script

Here is an example packaging script with comments.

#cleanup folders
rm -rf "App/AppName.app/Contents/MacOS/"
rm -rf "App/AppName.app/Contents/CodeResources"
rm -rf "App/AppName.app/Contents/_CodeSignature"
rm -rf "App/AppName.app/Contents/embedded.provisionprofile"
mkdir -p "App/AppName.app/Contents/Frameworks/"
mkdir -p "App/AppName.app/Contents/MacOS/"

#Build app
dotnet publish ../../ProjectFolder/AppName.csproj -c release -f net10.0 -r osx-x64 --self-contained true -p:PublishSingleFile=true

#Move app
cd ..
cd ..
cp -R -f ProjectFolder/bin/release/net10.0/osx-x64/publish/* "build/osx/App/AppName.app/Contents/MacOS/"
cd "build/osx/"

APP_ENTITLEMENTS="AppEntitlements.entitlements"
APP_SIGNING_IDENTITY="3rd Party Mac Developer Application: [***]"
INSTALLER_SIGNING_IDENTITY="3rd Party Mac Developer Installer: [***]"
APP_NAME="App/AppName.app"

#<here is moving your app resources to Resources folder using relative symlinks>

#<here is moving your .dylib files to Frameworks folder using relative symlinks>

echo "[INFO] Switch provisionprofile to AppStore"
\cp -R -f AppNameAppStore.provisionprofile "App/AppName.app/Contents/embedded.provisionprofile"

echo "[INFO] Fix libuv.dylib architectures"
lipo -remove i386 "App/AppName.app/Contents/Frameworks/libuv.dylib" "App/AppName.app/Contents/Frameworks/libuv.dylib"

find "$APP_NAME/Contents/Frameworks/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --sign "$APP_SIGNING_IDENTITY" "$fname"
fi
done

echo "[INFO] Signing app executable"
codesign --force --entitlements "$APP_ENTITLEMENTS" --sign "$APP_SIGNING_IDENTITY" "App/AppName.app/Contents/MacOS/AppName"

echo "[INFO] Signing app bundle"
codesign --force --entitlements "$APP_ENTITLEMENTS" --sign "$APP_SIGNING_IDENTITY" "$APP_NAME"

echo "[INFO] Creating AppName.pkg"
productbuild --component App/AppName.app /Applications --sign "$INSTALLER_SIGNING_IDENTITY" AppName.pkg

Testing a package

Copy your .app into the Applications folder and launch it. If it launches correctly, you did everything right. If it crashes, open Console.app and check the crash report.

Uploading a package to the App Store

Open the Transporter app, sign in, select your *.pkg package, and wait for validation and uploading to App Store Connect.

If you receive any errors, fix them, package the app again, remove the file in Transporter, and select it again.

When the upload succeeds, you will see your package in App Store Connect.

Packaging in GitHub Actions workflow

You can build the app in a CI/CD pipeline using the dotnet command. For code signing and notarization, a little extra work is required.

codesign and notarytool read the certificate and credentials to talk to the notarization service from a Keychain on the build machine:

# Create a new keychain
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
# Set it as the default keychain
security default-keychain -s build.keychain
# Unlock the keychain so it can be used without an authorisation prompt
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain

KEYCHAIN_PASSWORD is a password you generate specifically for this keychain. It can be generated on each build or a password you use for every build.

Next, the certificate for signing needs to be imported into the keychain. Because GitHub secrets only support strings, the certificate .p12 file needs to be stored in base64 encoded form. In the pipeline the string is decoded to a file and added to the keychain:

# Decode certificate to file
echo "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > certificate.p12
# Import into keychain
security import certificate.p12 -k build.keychain -P "${{ secrets.MACOS_CERTIFICATE_PWD}}" -T /usr/bin/codesign

MACOS_CERTIFICATE is the base64 encoded .p12 file, MACOS_CERTIFICATE_PWD is the password to the .p12 file.

To prevent authorisation prompt popups during code signing, instruct keychain to allow codesign access:

# Allow codesign to access keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain

As Apple requires multi-factor authentication (MFA) on developer accounts, notarytool uses a dedicated app-password that you can generate on the Apple developer site. Add the app-password for notarytool so it can be used later:

xcrun notarytool store-credentials "AC_PASSWORD" --apple-id "${{ secrets.APPLE_ID }}" --team-id ${{ env.TEAM_ID }} --password "${{ secrets.NOTARY_TOOL_PASSWORD }}"

TEAM_ID is the team id in App Store Connect, APPLE_ID is your Apple account e-mail address, NOTARY_TOOL_PASSWORD is the app-password you generated.

To use these steps in your GitHub Actions workflow add them as a step to the job that builds your app:

jobs:
build_osx:
runs-on: macos-latest
env:
TEAM_ID: MY_TEAM_ID
steps:
- name: Setup Keychain
run: |
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
echo "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "${{ secrets.MACOS_CERTIFICATE_PWD}}" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.KEYCHAIN_PASSWORD}}" build.keychain
xcrun notarytool store-credentials "AC_PASSWORD" --apple-id "${{ secrets.APPLE_ID }}" --team-id ${{ env.TEAM_ID }} --password "${{ secrets.NOTARY_TOOL_PASSWORD }}"

When configured like this you will not have to specify a specific keychain file for codesign or notarytool to use.

The next steps are to publish the app and sign it. Start by adding this environment variable to the job:

    env:
SIGNING_IDENTITY: thumbprint_of_certificate_added_to_keychain

And then add these steps:

    - name: Publish app
run: dotnet publish -c Release -r osx-x64 -o $RUNNER_TEMP/MyApp.app/Contents/MacOS MyApp.csproj
- name: Codesign app
run: |
find "$RUNNER_TEMP/MyApp.app/Contents/MacOS/"|while read fname; do
if [ -f "$fname" ]
then
echo "[INFO] Signing $fname"
codesign --force --timestamp --options=runtime --entitlements MyApp.entitlements --sign "${{ env.$SIGNING_IDENTITY }}" "$fname"
fi
done
codesign --force --timestamp --options=runtime --entitlements MyApp.entitlements --sign "${{ env.SIGNING_IDENTITY }}" "$RUNNER_TEMP/MyApp.app"

Note: RUNNER_TEMP is an environment variable provided by GitHub Actions

After code signing, the app bundle can now be notarized by adding this step to the job:

    - name: Notarise app
run: |
ditto -c -k --sequesterRsrc --keepParent "$RUNNER_TEMP/MyApp.app" "$RUNNER_TEMP/MyApp.zip"
xcrun notarytool submit "$RUNNER_TEMP/MyApp.zip" --wait --keychain-profile "AC_PASSWORD"
xcrun stapler staple "$RUNNER_TEMP/MyApp.app"

When you run this workflow you will have an app bundle that is signed and notarized, ready for packaging in a disk image or installer.

To verify that code signing worked, you will need to download it first to trigger the quarantine functionality of macOS. You can do this by e-mailing it to yourself or using a service like WeTransfer or similar.

Once you've downloaded the app bundle and want to start it, you should see the popup from macOS saying that the app was scanned and no malware was found.

See also