From ff3b77748b57369028da4eb121aba783d5156cb1 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Mon, 27 Apr 2026 15:23:44 -0500
Subject: [PATCH] feat(desktop): ship-readiness metadata & CI scaffolding
(#5255)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/release.yml | 3 ++
.github/workflows/pr_enforce_labels.yml | 2 +-
.github/workflows/release.yml | 26 +++++++++++
README.md | 4 ++
RELEASE_PROCESS.md | 57 +++++++++++++++++++++----
desktop/build.gradle.kts | 46 ++++++++++++++------
desktop/entitlements.plist | 4 ++
docs/roadmap.md | 2 +-
8 files changed, 120 insertions(+), 24 deletions(-)
diff --git a/.github/release.yml b/.github/release.yml
index 6ec1c03ba..a66aafea0 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -26,6 +26,9 @@ changelog:
labels:
- enhancement
- feature
+ - title: 🖥️ Desktop
+ labels:
+ - desktop
- title: 🛠️ Fixes
labels:
- bug
diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml
index 92ed400d2..bf876b347 100644
--- a/.github/workflows/pr_enforce_labels.yml
+++ b/.github/workflows/pr_enforce_labels.yml
@@ -29,7 +29,7 @@ jobs:
script: |
// Extract labels from the payload directly to avoid extra API calls
const latestLabels = context.payload.pull_request.labels.map(label => label.name);
- const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor', 'chore', 'ci', 'build', 'testing', 'documentation'];
+ const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor', 'desktop', 'chore', 'ci', 'build', 'testing', 'documentation'];
console.log('Labels from payload:', latestLabels);
const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label));
if (!hasRequiredLabel) {
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index df78d3306..8c97ffd26 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -53,6 +53,14 @@ on:
required: false
INTERNAL_BUILDS_HOST_PAT:
required: false
+ APPLE_SIGNING_IDENTITY:
+ required: false
+ APPLE_ID:
+ required: false
+ APPLE_APP_SPECIFIC_PASSWORD:
+ required: false
+ APPLE_TEAM_ID:
+ required: false
concurrency:
group: ${{ github.workflow }}-${{ inputs.tag_name }}
@@ -284,7 +292,13 @@ jobs:
- name: Package Native Distributions
env:
ORG_GRADLE_PROJECT_appVersionName: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
+ VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
APPIMAGE_EXTRACT_AND_RUN: 1
+ SIGN_MACOS: ${{ runner.os == 'macOS' && 'true' || 'false' }}
+ APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Quote the -P flag: PowerShell on Windows interprets the dot in
# `-PaboutLibraries.release=true` as member access on `-PaboutLibraries`,
# splitting the token and feeding `.release=true` to Gradle as a task name.
@@ -309,6 +323,18 @@ jobs:
retention-days: 1
if-no-files-found: ignore
+ - name: Attest Desktop artifact provenance
+ if: success()
+ uses: actions/attest-build-provenance@v4
+ with:
+ subject-path: |
+ desktop/build/compose/binaries/main-release/*/*.dmg
+ desktop/build/compose/binaries/main-release/*/*.msi
+ desktop/build/compose/binaries/main-release/*/*.exe
+ desktop/build/compose/binaries/main-release/*/*.deb
+ desktop/build/compose/binaries/main-release/*/*.rpm
+ desktop/build/compose/binaries/main-release/*/*.AppImage
+
github-release:
if: ${{ !cancelled() && !failure() }}
runs-on: ubuntu-24.04-arm
diff --git a/README.md b/README.md
index 2cc1ffe1c..5db9bb130 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,10 @@ width="24%">](https://play.google.com/store/apps/details?id=com.geeksville.mesh&
The play store is the last to update of these options, but if you want to join the Play Store testing program go to [this URL](https://play.google.com/apps/testing/com.geeksville.mesh) and opt-in to become a tester.
If you encounter any problems or have questions, [ask us on the discord](https://discord.gg/meshtastic), [create an issue](https://github.com/meshtastic/Meshtastic-Android/issues), or [post in the forum](https://github.com/orgs/meshtastic/discussions) and we'll help as we can.
+### Desktop
+
+**Meshtastic Desktop** installers (macOS DMG, Windows MSI/EXE, Linux DEB/RPM/AppImage) are available from [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases). A Flatpak package is maintained at [vidplace7/org.meshtastic.desktop](https://github.com/vidplace7/org.meshtastic.desktop).
+
## Documentation
The project's documentation is generated with [Dokka](https://kotlinlang.org/docs/dokka-introduction.html) and hosted on GitHub Pages. It is automatically updated on every push to the `main` branch.
diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md
index 8a1feb203..029b3edf8 100644
--- a/RELEASE_PROCESS.md
+++ b/RELEASE_PROCESS.md
@@ -1,21 +1,23 @@
-# Meshtastic-Android Release Process
+# Meshtastic Release Process
-This guide summarizes the steps for releasing a new version of Meshtastic-Android. The process is fully automated via GitHub Actions and Fastlane.
+This guide summarizes the steps for releasing new versions of Meshtastic Android and Desktop. The process is fully automated via GitHub Actions and Fastlane.
## Overview
The entire release process is managed by a single, manually-triggered GitHub Action: **`Create or Promote Release`**.
- **Trigger:** To start a new release or promote an existing one, a developer manually runs the workflow from the GitHub Actions tab.
-- **Inputs:** The workflow requires two inputs:
+- **Inputs:** The workflow requires the following inputs:
1. `version`: The base version number you are releasing (e.g., `2.4.0`).
2. `channel`: The release channel you are targeting (`internal`, `closed`, `open`, or `production`).
+ 3. `build_desktop`: Whether to build and attach Desktop native installers (default: `false`).
- **Automation:** The workflow handles everything automatically:
- **Syncs Assets:** Fetches the latest firmware/hardware lists, protobuf definitions, and translations (Crowdin).
- **Generates Changelog:** Creates a clean changelog from commits since the last production release and commits it to the repo.
- **Updates Config:** Automatically bumps the `VERSION_NAME_BASE` in `config.properties`.
- **Verifies & Tags:** Runs lint checks, builds the app, and *only* tags the release if successful.
- - **Deploys:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release.
+ - **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release.
+ - **Deploys Desktop** *(when enabled)*: Builds native installers (DMG, MSI, EXE, DEB, RPM, AppImage) on a matrix of runners and attaches them to the GitHub Release.
- **Changelog:** Release notes are auto-generated from PR labels. Ensure PRs are labeled correctly to maintain an accurate changelog.
## Release Steps
@@ -27,13 +29,15 @@ The entire release process is managed by a single, manually-triggered GitHub Act
3. Click the **"Run workflow"** dropdown.
4. Enter the base `version` (e.g., `2.4.0`).
5. Select the `internal` channel.
-6. Click **"Run workflow"**.
+6. Check **`build_desktop`** if you want Desktop installers included in this release.
+7. Click **"Run workflow"**.
The workflow will:
1. **Create a new commit** on the current branch containing updated assets, translations, and the new changelog.
2. **Tag** that commit with an incremental internal tag (e.g., `v2.4.0-internal.1`).
-3. **Build & Deploy** the verified artifact to the Play Store Internal track.
-4. Publish a **draft** pre-release on GitHub.
+3. **Build & Deploy** the verified Android artifact to the Play Store Internal track.
+4. **Build Desktop** *(if enabled)* native installers on macOS, Windows, and Linux runners.
+5. Publish a **draft** pre-release on GitHub with all artifacts attached.
### 2. Promote to the Next Channel
@@ -54,8 +58,43 @@ After testing is complete on all pre-release channels, you can create the final
### 4. Post-Release
-1. **Verify:** Check the Google Play Console to ensure the build is available on the correct track.
-2. **Merge:** Merge the release branch (if one was used for stabilization) back into `main`.
+1. **Verify Android:** Check the Google Play Console to ensure the build is available on the correct track.
+2. **Verify Desktop** *(if built)*: Download and smoke-test at least one installer (DMG, MSI, or AppImage) from the GitHub Release.
+3. **Merge:** Merge the release branch (if one was used for stabilization) back into `main`.
+
+## Desktop Release Details
+
+Desktop native installers are built as part of the main release pipeline when `build_desktop` is enabled. There is no separate promotion flow for Desktop — installers are built once during the `internal` release and attached to the GitHub Release alongside Android artifacts.
+
+### Artifacts Produced
+
+| Platform | Format | Runner |
+|---|---|---|
+| macOS | `.dmg` | `macos-latest` |
+| Windows | `.msi`, `.exe` | `windows-latest` |
+| Linux (x86_64) | `.deb`, `.rpm`, `.AppImage` | `ubuntu-24.04` |
+| Linux (ARM64) | `.deb`, `.rpm`, `.AppImage` | `ubuntu-24.04-arm` |
+
+### macOS Code Signing & Notarization
+
+macOS builds are signed and notarized when the following CI secrets are configured:
+
+| Secret | Source |
+|---|---|
+| `APPLE_SIGNING_IDENTITY` | Developer ID Application certificate (from Apple Developer account) |
+| `APPLE_ID` | Apple ID email used for notarization |
+| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password from [appleid.apple.com](https://appleid.apple.com) |
+| `APPLE_TEAM_ID` | 10-character Apple Developer Team ID |
+
+Without these secrets, macOS builds are produced unsigned. Unsigned DMGs will trigger Gatekeeper warnings on end-user machines.
+
+### Version Alignment
+
+Desktop uses the same version resolution chain as Android — both read `VERSION_CODE_OFFSET` and `VERSION_NAME_BASE` from `config.properties`, with CI passing the resolved values as environment variables. Version names are sanitized to strict `X.Y.Z` format for native installer compatibility.
+
+### Flatpak
+
+Flatpak packaging is maintained externally at [vidplace7/org.meshtastic.desktop](https://github.com/vidplace7/org.meshtastic.desktop). It builds `:desktop:packageUberJarForCurrentOS` (not the native distribution pipeline) and includes its own AppStream metainfo, `.desktop` entry, and JBR bundling.
## Build Attestations & Provenance
diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts
index 2e66d6012..f9c5b7276 100644
--- a/desktop/build.gradle.kts
+++ b/desktop/build.gradle.kts
@@ -124,8 +124,8 @@ compose.desktop {
mainClass = "org.meshtastic.desktop.MainKt"
jvmArgs(
"-Xmx2G",
- "-Dapple.awt.application.name=Meshtastic",
- "-Dcom.apple.mrj.application.apple.menu.about.name=Meshtastic",
+ "-Dapple.awt.application.name=Meshtastic Desktop",
+ "-Dcom.apple.mrj.application.apple.menu.about.name=Meshtastic Desktop",
"-Dcom.apple.bundle.identifier=org.meshtastic.desktop",
)
@@ -140,7 +140,7 @@ compose.desktop {
}
nativeDistributions {
- packageName = "Meshtastic"
+ packageName = "Meshtastic Desktop"
// Ensure critical JVM modules are included in the custom JRE bundled with the app.
// jdeps might miss some of these if they are loaded via reflection or JNI.
@@ -157,8 +157,8 @@ compose.desktop {
// Increase max heap size to prevent OOM issues on complex maps/data
jvmArgs(
"-Xmx2G",
- "-Dapple.awt.application.name=Meshtastic",
- "-Dcom.apple.mrj.application.apple.name=Meshtastic",
+ "-Dapple.awt.application.name=Meshtastic Desktop",
+ "-Dcom.apple.mrj.application.apple.menu.about.name=Meshtastic Desktop",
"-Dcom.apple.bundle.identifier=org.meshtastic.desktop",
)
@@ -167,6 +167,7 @@ compose.desktop {
iconFile.set(project.file("src/main/resources/icon.icns"))
minimumSystemVersion = "12.0"
bundleID = "org.meshtastic.desktop"
+ appCategory = "public.app-category.utilities"
entitlementsFile.set(project.file("entitlements.plist"))
infoPlist {
extraKeysRawXml =
@@ -191,22 +192,41 @@ compose.desktop {
"""
.trimIndent()
}
- // TODO: To prepare for real distribution on macOS, you'll need to sign and notarize.
- // You can inject these from CI environment variables.
- // sign = true
- // notarize = true
- // appleID = System.getenv("APPLE_ID")
- // appStorePassword = System.getenv("APPLE_APP_SPECIFIC_PASSWORD")
+ // macOS code signing and notarization.
+ // Required CI secrets:
+ // APPLE_SIGNING_IDENTITY – e.g. "Developer ID Application: Meshtastic LLC (TEAMID)"
+ // APPLE_ID – Apple ID email used for notarization
+ // APPLE_APP_SPECIFIC_PASSWORD – App-specific password from appleid.apple.com
+ // APPLE_TEAM_ID – 10-character Apple Developer Team ID
+ val signMacOs = System.getenv("SIGN_MACOS")?.toBoolean() ?: false
+ if (signMacOs) {
+ signing {
+ sign.set(true)
+ identity.set(System.getenv("APPLE_SIGNING_IDENTITY"))
+ }
+ notarization {
+ appleID.set(System.getenv("APPLE_ID"))
+ password.set(System.getenv("APPLE_APP_SPECIFIC_PASSWORD"))
+ teamID.set(System.getenv("APPLE_TEAM_ID"))
+ }
+ }
}
windows {
iconFile.set(project.file("src/main/resources/icon.ico"))
menuGroup = "Meshtastic"
- // TODO: Must generate and set a consistent UUID for Windows upgrades.
- // upgradeUuid = "YOUR-UPGRADE-UUID-HERE"
+ shortcut = true
+ menu = true
+ dirChooser = true
+ // Stable UUID ensures MSI upgrades replace the previous installation
+ // rather than installing side-by-side. NEVER change this value.
+ upgradeUuid = "4974EA87-98AA-470E-B590-0BD5CF9FAE8E"
}
linux {
iconFile.set(project.file("src/main/resources/icon.png"))
menuGroup = "Network"
+ debMaintainer = "developers@meshtastic.org"
+ appCategory = "Network"
+ rpmLicenseType = "GPLv3+"
}
// Define target formats based on the current host OS to avoid configuration errors
diff --git a/desktop/entitlements.plist b/desktop/entitlements.plist
index f799a66e9..3941c7f0b 100644
--- a/desktop/entitlements.plist
+++ b/desktop/entitlements.plist
@@ -10,5 +10,9 @@
com.apple.security.device.bluetooth
+ com.apple.security.network.client
+
+ com.apple.security.device.usb
+
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 8cff42c1f..02e7809e3 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -77,7 +77,7 @@ These items address structural gaps identified in the March 2026 architecture re
| Notifications | ✅ Desktop native notifications with system tray icon support |
| MenuBar | ✅ Removed — replaced with `onPreviewKeyEvent` keyboard shortcuts (⌘Q, ⌘,, ⌘⇧T, ⌘1-4, ⌘/) |
| About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) |
-| Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB) |
+| Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB). Windows `upgradeUuid` set; macOS signing/notarization wired behind `SIGN_MACOS` env flag; desktop build attestation in release CI. Flatpak packaging maintained externally at [vidplace7/org.meshtastic.desktop](https://github.com/vidplace7/org.meshtastic.desktop) (includes AppStream metainfo, `.desktop` entry, and JBR bundling); see [PR #4807](https://github.com/meshtastic/Meshtastic-Android/pull/4807) for `flatpakGradleGenerator` integration |
## Near-Term Priorities (30 days)