Skip to content

Commit 463ee7c

Browse files
authored
Merge pull request #1148 from tunjid/tj/mac-github
Set up notarization for Mac app
2 parents fa8b894 + 71fcc8a commit 463ee7c

File tree

3 files changed

+134
-61
lines changed

3 files changed

+134
-61
lines changed

.github/workflows/publish.yml

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -93,64 +93,64 @@ jobs:
9393
draft: true
9494
allow_updates: true
9595

96-
# publish-mac-app:
97-
# runs-on: macos-latest
98-
# permissions:
99-
# contents: 'write'
100-
# steps:
101-
# - name: Checkout Code
102-
# uses: actions/checkout@v5
103-
#
104-
# - name: Setup java
105-
# uses: actions/setup-java@v5
106-
# with:
107-
# distribution: 'zulu'
108-
# java-version: 21
109-
#
110-
# - name: Setup Gradle
111-
# uses: gradle/actions/setup-gradle@v4
112-
#
113-
# - name: Cache KMP tooling
114-
# uses: actions/cache@v4
115-
# with:
116-
# path: |
117-
# ~/.konan
118-
# key: ${{ runner.os }}-v1-${{ hashFiles('gradle/libs.versions.toml') }}
119-
#
120-
# - name: Import signing certificate
121-
# uses: apple-actions/import-codesign-certs@v2
122-
# with:
123-
# p12-file-base64: ${{ secrets.SIGNING_CERTIFICATE_P12_DATA_MACOS }}
124-
# p12-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD_MACOS }}
125-
#
126-
# - name: Build release DMG
127-
# env:
128-
# HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }}
129-
# run: |
130-
# ./gradlew packageReleaseDmg \
131-
# -Pheron.versionCode=${{ github.run_number }} \
132-
# -Pheron.endpoint="$HERON_ENDPOINT"
133-
#
134-
# - name: Notarize DMG
135-
# env:
136-
# APPLE_ID_NOTARIZATION: ${{ secrets.APPLE_ID_NOTARIZATION }}
137-
# APPSTORE_TEAM_ID: ${{ secrets.APPSTORE_TEAM_ID }}
138-
# NOTARIZATION_PWD: ${{ secrets.NOTARIZATION_PWD }}
139-
# run: |
140-
# DMG_PATH=$(find composeApp/build/release/main-release/dmg -name "*.dmg" | head -1)
141-
# xcrun notarytool submit "$DMG_PATH" \
142-
# --apple-id "$APPLE_ID_NOTARIZATION" \
143-
# --password "$NOTARIZATION_PWD" \
144-
# --team-id "$APPSTORE_TEAM_ID" \
145-
# --wait
146-
# xcrun stapler staple "$DMG_PATH"
147-
# echo "DMG_PATH=$DMG_PATH" >> $GITHUB_ENV
148-
#
149-
# - name: Upload to GitHub releases
150-
# uses: softprops/action-gh-release@v2
151-
# with:
152-
# tag_name: v${{ github.run_number }}
153-
# name: Heron v${{ github.run_number }}
154-
# files: ${{ env.DMG_PATH }}
155-
# draft: true
156-
# allow_updates: true
96+
publish-mac-app:
97+
runs-on: macos-latest
98+
permissions:
99+
contents: 'write'
100+
steps:
101+
- name: Checkout Code
102+
uses: actions/checkout@v5
103+
104+
- name: Setup java
105+
uses: actions/setup-java@v5
106+
with:
107+
distribution: 'zulu'
108+
java-version: 21
109+
110+
- name: Setup Gradle
111+
uses: gradle/actions/setup-gradle@v4
112+
113+
- name: Cache KMP tooling
114+
uses: actions/cache@v4
115+
with:
116+
path: |
117+
~/.konan
118+
key: ${{ runner.os }}-v1-${{ hashFiles('gradle/libs.versions.toml') }}
119+
120+
- name: Import signing certificate
121+
uses: apple-actions/import-codesign-certs@v2
122+
with:
123+
p12-file-base64: ${{ secrets.MACOS_SIGNING_CERTIFICATE_P12_DATA }}
124+
p12-password: ${{ secrets.MACOS_SIGNING_CERTIFICATE_PASSWORD }}
125+
126+
- name: Build and sign release DMG
127+
env:
128+
HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }}
129+
run: |
130+
./gradlew packageReleaseDmg \
131+
-Pheron.versionCode=${{ github.run_number }} \
132+
-Pheron.endpoint="$HERON_ENDPOINT" \
133+
-Pheron.macOS.signing.identity="${{ secrets.MACOS_SIGNING_IDENTITY }}"
134+
135+
- name: Notarize and staple DMG
136+
env:
137+
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
138+
MACOS_NOTARIZATION_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
139+
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
140+
run: |
141+
DMG_PATH=$(find composeApp/build/release/main-release/dmg -name "*.dmg" | head -1)
142+
xcrun notarytool submit "$DMG_PATH" \
143+
--apple-id "$MACOS_NOTARIZATION_APPLE_ID" \
144+
--password "$MACOS_NOTARIZATION_PASSWORD" \
145+
--team-id "$MACOS_NOTARIZATION_TEAM_ID" \
146+
--wait
147+
xcrun stapler staple "$DMG_PATH"
148+
149+
- name: Upload to GitHub releases
150+
uses: softprops/action-gh-release@v2
151+
with:
152+
tag_name: v${{ github.run_number }}
153+
name: Heron v${{ github.run_number }}
154+
files: composeApp/build/release/main-release/dmg/*.dmg
155+
draft: true
156+
allow_updates: true

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,63 @@ persisted across app restarts.
117117
* All subtypes of `Action.Navigation` typically share the same key.
118118
* All subtypes of pagination actions, also share the same key and are processed with
119119
the [Tiling library](https://github.com/tunjid/Tiler).
120+
121+
## Building
122+
123+
### Gradle Properties
124+
125+
The following properties can be set in `~/.gradle/gradle.properties` or passed via `-P` flags.
126+
None are required for basic development builds.
127+
128+
| Property | Description |
129+
|---|---|
130+
| `heron.versionCode` | Integer version code. Managed by CI via `github.run_number`. |
131+
| `heron.endpoint` | Backend endpoint URL for the app. |
132+
| `heron.isPlayStore` | Set to `true` when building for Play Store distribution. |
133+
| `heron.releaseBranch` | Branch prefix (`bugfix/`, `feature/`, `release/`) controlling version increments. |
134+
| `heron.macOS.signing.identity` | Name of the Developer ID Application certificate in your Keychain (e.g. `Developer ID Application: Name (TEAM_ID)`). When present, the macOS DMG will be code signed. |
135+
macOS signing is only configured when `heron.macOS.signing.identity` is present,
136+
so contributors without an Apple Developer account can still build unsigned DMGs with
137+
`./gradlew packageReleaseDmg`.
138+
139+
Notarization is handled externally via `xcrun notarytool` (not a Gradle task) to maintain
140+
compatibility with the Gradle configuration cache. To notarize locally after building a signed DMG:
141+
142+
```bash
143+
./gradlew packageReleaseDmg
144+
xcrun notarytool submit <path-to-dmg> \
145+
--apple-id <your-apple-id> \
146+
--password <app-specific-password> \
147+
--team-id <team-id> \
148+
--wait
149+
xcrun stapler staple <path-to-dmg>
150+
```
151+
152+
### Publishing
153+
154+
Publishing is triggered manually via the `Publish` GitHub Actions workflow (`workflow_dispatch`).
155+
It runs two jobs in parallel:
156+
157+
**Android** (`publish-android-app`) builds a release AAB, signs it, uploads to the Play Store
158+
internal track, extracts a universal APK, and attaches it to a draft GitHub Release.
159+
160+
**macOS** (`publish-mac-app`) imports a signing certificate, builds a signed DMG
161+
via `packageReleaseDmg`, notarizes it with `xcrun notarytool`, staples the ticket,
162+
and attaches it to the same draft GitHub Release.
163+
164+
The following repository secrets are required for CI publishing:
165+
166+
| Secret | Used by |
167+
|---|---|
168+
| `HERON_ENDPOINT` | Both jobs |
169+
| `GOOGLE_SERVICES_BASE_64` | Android |
170+
| `SIGNING_KEY_BASE_64` | Android |
171+
| `ALIAS` | Android |
172+
| `KEY_STORE_PASSWORD` | Android |
173+
| `KEY_PASSWORD` | Android |
174+
| `MACOS_SIGNING_CERTIFICATE_P12_DATA` | macOS - base64-encoded Developer ID Application `.p12` file |
175+
| `MACOS_SIGNING_CERTIFICATE_PASSWORD` | macOS - password for the `.p12` file |
176+
| `MACOS_SIGNING_IDENTITY` | macOS - certificate identity string (e.g. `Developer ID Application: Name (TEAM_ID)`) |
177+
| `MACOS_NOTARIZATION_APPLE_ID` | macOS - Apple ID email |
178+
| `MACOS_NOTARIZATION_PASSWORD` | macOS - app-specific password |
179+
| `MACOS_NOTARIZATION_TEAM_ID` | macOS - Apple Developer Team ID |

composeApp/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,20 @@ compose.desktop {
210210

211211
val resourcesDir = project.file("src/desktopMain/resources")
212212
macOS {
213+
bundleID = "com.tunjid.heron"
213214
iconFile.set(resourcesDir.resolve("icon.icns"))
215+
216+
providers.gradleProperty("heron.macOS.signing.identity")
217+
.let { identityProperty ->
218+
if (identityProperty.isPresent) signing {
219+
sign.set(true)
220+
identity.set(identityProperty)
221+
}
222+
}
223+
224+
// Notarization is handled externally via xcrun notarytool
225+
// to maintain compatibility with Gradle configuration cache.
226+
// See the publish workflow and README for details.
214227
}
215228
windows {
216229
iconFile.set(resourcesDir.resolve("icon.ico"))

0 commit comments

Comments
 (0)