Now you can deploy your app as a DMG
The codesign process is painful, but if you really want to deploy your app as a DMG, you can do that now
The primary purpose of jDeploy is to make it easier to deploy your Java apps to the desktop. You’ve put together a simple proof-of-concept, and you want to sent it to a friend for comment, but you find out that packaging a desktop app ain’t so easy. That little app that you whipped up in a couple of hours is held hostage on your machine while you string together a Rube Goldberg machine of bespoke scripts and third-party tools that will hopefully bundle your jars into something resembling a native app. Then, after hours, or perhaps even days, of tedious toil, you emerge from your office dungeon, sweat-stained and deserving of a Ph.D. in “native deployment theory”, holding a release candidate which you then proudly attach to an email and wait for your friend’s glowing feedback, only to find out that your friend’s computer won’t open the app. When they click on your app, all they see is a scary dialog box warning that the app appears to be damaged, or worse, malware, and that they should delete it immediately.
Far too many Java developers bear the scars of their early encounters with native deployments. There are allegedly developers out there who managed to produce a working native app on their first time through, but for every one of those, there are at least five others who went on to found a billion-dollar start-up.
But I digress.
So jDeploy was meant to make it easier to deploy apps. One of the The most painful parts of the deployment process is codesigning and notarization for MacOS, so it was important that jDeploy address this in a meaningful way. I’m proud to say that completely eliminates this pain. Using jDeploy, you can deploy your app as a native Mac app without having to codesign or notarize your app. You don’t even need your own Mac developer account. This is because jDeploy provides an installer that is signed and notarized by jDeploy’s developer certificate.
The user experience of this installer is quite smooth for the user, but some developers have expressed interest being able to deploy their apps as a .dmg, since this would be more familiar to users. Some developers also have their own Apple developer account, and would prefer to sign the app themselves rather than deploy using the jDeploy certificate.
I’m proud to announce that this is now possible, via a new GitHub action. Setting up code-signing is still painful, but I will try to lay it out in the most painless way possible in the remainder of this article. By the end of it, if you’re following step by step, your GitHub releases will include a download link for your app as a signed and notarized .DMG file.
An example of the finished product
This GitHub action can be run in the same workflow as your existing jDeploy deployment to add .dmg downloads for MacOS.
Here is a sample release that was generated using this action.
When you download either of the Mac bundles, you’ll receive a .dmg file which provides you with a familiar option to install the app by dragging it onto the Applications directory.
The “Copy-Paste” Snippet Version
Are you a GitHub workflow wizard who already swizzle’s Apple developer codesigning workflows in your sleep?
- name: Create DMG and Upload to Release
uses: shannah/jdeploy-action-dmg@main
with:
github_token: ${{ secrets.GITHUB_TOKE }}
developer_id: ${{ secrets.MAC_DEVELOPER_ID }}
developer_certificate_p12_base64: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_P12_BASE64 }}
developer_certificate_password: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_PASSWORD }}
notarization_password: ${{ secrets.MAC_NOTARIZATION_PASSWORD }}
target_repository: 'myapp-releases' # optional only if publishing releases to different repository
Add this to your jDeploy workflow, after the `shannah/jdeploy` action has run successfully.
You can see this in the full context here.
Since you’re already an experienced codesigner, the action parameters here are self explanatory, but I’ll go over them here anyways, so that I can refer back to them later.
github_token
: A GitHub token for an account that has permission to update releases target repository.developer_id
: Your Apple developer ID. This is generally the email address you use to login to your Apple developer account.developer_certificate_p12_base64
: Your developer certificate, base64-encoded. You obtain this by exporting your developer certificate from Keychain as a .p12, and then encoding this in base64.developer_certificate_password
: The password that you used to protect your .p12 file when you exported it from your keychain.notarization_password
: The app-specific password that you need to configure in your Apple developer account.target_repository
: Optional. Use this only if you are publishing your releases to a different repository.
From the beginning…
For those of you who aren’t veteran codesigners of the Apple persuasion, I’ll go into some more detail.
I’ll go through each parameter of the `shannah/jdeploy-action-dmg` action, and then explain how to obtain or generate it.
github_token
You need to supply a GitHub token to the action so that it has permission to update the release. If you are publishing releases to the same repository as the workflow, then you can just use the ${{ secrets.GITHUB_TOKEN }}
variable, which automatically made available to all workflows, and has permission to the current repository.
If you are publishing to a different repository than the current workflow, you will need to generate your own token with appropriate permissions, add it to your repository secrets, and then use that one.
developer_id
This the username or email that you use to log into your Apple developer account.
developer_certificate_p12_base64
This one is a little bit more involved to obtain, because you need to first create a certificate in your Apple Developer account. You’ll want to create a “Developer ID Application” certificate.
After adding the certificate to your keychain, you should export it export it as a .p12 file by opening the “Keychain Access” application, right-clicking the certificate entry (it will be the one that begins with “Developer ID:”), and selecting “Export…”.
This will display a file dialog to select the location where you wish to save the .p12 file. Make sure to select “.p12” as the output format. You will also be prompted for a password. The password that you enter here, you will use later as the developer_certificate_password
parameter to the GitHub action.
After saving the .p12 file, you should encode it as base64. Since the next step will be to copy the base64 text into a repository secret, we can kill two birds with one stone by running the following command:
base64 yourfile.p12 | pbcopy
Now that you have the certificate’s base64 on your, you create a repository secret and paste the contents into it. You can name the secret whatever you like, but, in the spirit of matching the names from the above snippet, name it “MAC_DEVELOPER_CERTIFICATE_P12_BASE64”
developer_certificate_p12_password
This is the password that you used to encrypt your .p12 file when you exported it from your keychain. Add this as a repository secret named “MAC_DEVELOPER_CERTIFICATE_PASSWORD”
notarization_password
In case you aren’t experiencing password fatigue yet, Apple requires you to generate yet another password for notarization your app. This is not the password that you use to log into your developer account, nor is it the password that you used to encrypt your .p12 file. This is an entirely new one, which Apple calls an “app-specific password”. You’ll add this inside your Apple developer account as follows:
Sign into your Apple ID account.
If you do not yet have 2FA security configured on your account, you’ll need to enable it before proceeding to the next step.
Go to the section named “Security”, and find where it says “App-specific Passwords”. Press “Generate Password”, and follow the prompts.
Copy and paste this password into a repository secret named “
MAC_NOTARIZATION_PASSWORD”
target_repository
This parameter is optional, and should only be used if you are publishing your releases to a different repository than where this workflow resides. Furthermore, this action assumes that the jDeploy release has already been created by running the `shannah/jdeploy
` action in a previous job of the same workflow, and the `target_repository
` parameter of this action must match the `target_repository
` of that action invocation.
Putting it all together
In this article, I’ve focused on just dmg creation part of the process, but this action needs to be run in context that meets the following requirements:
It must be run in a workflow _after_ the `
shannah/jdeploy
` action has already run, because that action will generate the release and release notes, as well as all of the installers for each platform. The dmg action works by creating a dmg, and then patching the existing release and release notes with it.It must run on a MacOS runner. The regular
jdeploy
action can run on any host - Windows, MacOS, or Linux - but the dmg action needs to run on a Mac because it uses the Mac toolchain for performing the code-signing and creating the DMG file. You could just run them both on the same MacOS instance, but I like to use Linux runners wherever possible, so in my workflows I usually split them into separate jobs, and run the first one on an ubuntu runner, then run the dmg job on a MacOS runner.
Here is a complete GitHub workflow file, which serves as the default template now when you create a project with the jDeploy IntelliJ plugin.
# This workflow will build a Java project with Maven and bundle them as native app installers with jDeploy
# See https://www.jdeploy.com for more information.
name: jDeploy CI with Maven
on:
push:
# Run on all commits for all branches, except the gh-pages branch
# We don't run on gh-pages because it usually includes the website
# This generates a tag by the same name as the branch with installers that
# that are kept in sync with the head of the branch.
branches: ['*', '!gh-pages']
# Run on all releases/tags so that we generate native bundles on all
# releases
tags: ['*']
jobs:
# Job Number 1: Build the app and deploy with jdeploy
build:
permissions:
# Important to enable write permission so that the jdeploy action
# is allowed to modify the release
contents: write
# The first job, which builds your app from source, and runs
# the jdeploy action can run on any host type, but ubuntu-latest
# is cheap and plentiful.
runs-on: ubuntu-latest
steps:
- name: Set up Git
run: |
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
git config --global user.name "${{ github.actor }}"
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
# This uses java 11, but you can use 17, 21, or whichever you like.
java-version: '11'
java-package: jdk
distribution: zulu
cache: maven
# Now build your app. This example uses Maven, but you do you
- name: Build with Maven
run: mvn package
# In order to make your build artifacts available to the
# DMG job, which runs on a different runner, we need to upload
# our build artifacts
- name: Upload Build Artifacts
if: ${{ vars.JDEPLOY_CREATE_DMG == 'true' }} # Control DMG creation with the CREATE_DMG variable
uses: actions/upload-artifact@v3
with:
name: build-target
path: ./target
- name: Build App Installer Bundles
uses: shannah/jdeploy@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Job Number 2: Generate a DMG and then add it to the release
create_and_upload_dmg:
if: ${{ vars.JDEPLOY_CREATE_DMG == 'true' }} # Control DMG creation with the CREATE_DMG variable
name: Create and upload DMG
permissions:
contents: write
runs-on: macos-latest
needs: build
steps:
- name: Set up Git
run: |
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
git config --global user.name "${{ github.actor }}"
- uses: actions/checkout@v3
- name: Download Build Artifacts
uses: actions/download-artifact@v3
with:
name: build-target
path: ./target
- name: Create DMG and Upload to Release
uses: shannah/jdeploy-action-dmg@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
developer_id: ${{ secrets.MAC_DEVELOPER_ID }}
#developer_team_id: ${{ secrets.MAC_DEVELOPER_TEAM_ID }}
#developer_certificate_name: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_NAME }}
developer_certificate_p12_base64: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_P12_BASE64 }}
developer_certificate_password: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_PASSWORD }}
notarization_password: ${{ secrets.MAC_NOTARIZATION_PASSWORD }}
The above workflow is a variation on the new “jdeploy.yml
” template that is included in the projects templates that the jDeploy IntelliJ plugin uses. The “new” part is the create_and_upload_dmg
job, which will run after the “build” action, and initiates the new `shannah/jdeploy-action-dmg
` action to generate and upload the DMG to the release.
It uses a GitHub config variable to enable/disable DMG creation, so you’ll need to create a config variable on the repository named “JDEPLOY_CREATE_DMG
”, with a value of “false”.
With that in place, you should be good to go. You might be wondering about the “Download Build Artifacts” step:
- name: Download Build Artifacts
uses: actions/download-artifact@v3
with:
name: build-target
path: ./target
This is necessary to avoid building the app again with Maven. Since we already built the app in the “build” job, we get a tiny bit of efficiency by uploading the target directory with the build artifacts at the end of the “build” job, and then downloading them here.
This needs the corresponding “Create and upload DMG” step from the “build” job in order to work correctly.
- name: Upload Build Artifacts
if: ${{ vars.JDEPLOY_CREATE_DMG == 'true' }} # Control DMG creation with the CREATE_DMG variable
uses: actions/upload-artifact@v3
with:
name: build-target
path: ./target
Building unsigned DMG Files
The jDeploy DMG action also supports building unsigned DMG files. All you need to do is omit all of the parameters pertaining to codesigning, and just call it directly as follows:
- name: Create DMG and Upload to Release
uses: shannah/jdeploy-action-dmg@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
This will build a DMG file and add it to the release. However, because it is unsigned users won’t be able to simply download it and double-click to open it. They’ll need to right click on your app, and select “Open”. After which they’ll receive a scary warning about it being from an unidentified developer. In the best case, they’ll be able to accept the danger and still open your app, but, in the worst case, it still might not let them open it. It depends on the OS and the security settings.
If you plan to distribute your app as a DMG, then it is best to use codesigning as described here. Otherwise, you are much better off just using the default jDeploy installer which is signed and notarized by jDeploy.
Thank you for all your hard work, but after almost thirty years as a Java developer I've finally given up and started to study Rust in order to have a language that I can actually compile and deploy an app without doing absurd incarnations and compromises. Unfortunately Rust seems to have heavily embraced "let's make this as cryptic as possible so the learning curve is absurdly steep".