From 6fb8d2c31531c3131e999a7896a7493f8b8833cd Mon Sep 17 00:00:00 2001 From: James Rich Date: Tue, 14 Apr 2026 15:11:48 -0500 Subject: [PATCH 1/2] feat(desktop): ship-readiness metadata & CI scaffolding - Set Windows upgradeUuid for stable MSI upgrades - Wire macOS signing/notarization behind SIGN_MACOS env flag - Fix VERSION_CODE passthrough to release-desktop CI job - Rename packageName to "Meshtastic Desktop" - Fix truncated macOS JVM property (apple.menu.about.name) - Add macOS appCategory, entitlements (network, USB) - Add Windows shortcut/menu/dirChooser options - Add Linux debMaintainer, appCategory, rpmLicenseType - Add desktop build attestation step in release workflow - Add Discord notification job for desktop releases - Add desktop category to release notes config - Update RELEASE_PROCESS.md with full desktop release docs - Add desktop download section to README.md - Update roadmap with packaging status and Flatpak reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/release.yml | 3 ++ .github/workflows/release.yml | 69 ++++++++++++++++++++++++++++++++++- README.md | 4 ++ RELEASE_PROCESS.md | 57 ++++++++++++++++++++++++----- desktop/build.gradle.kts | 51 +++++++++++++++++++------- desktop/entitlements.plist | 4 ++ docs/roadmap.md | 2 +- 7 files changed, 166 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/release.yml b/.github/workflows/release.yml index 40d8e40f3..7b460e775 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,16 @@ on: required: false INTERNAL_BUILDS_HOST_PAT: required: false + DISCORD_WEBHOOK_DESKTOP: + 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 +294,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 }} run: ./gradlew :desktop:packageReleaseDistributionForCurrentOS -PaboutLibraries.release=true --no-daemon - name: List Desktop Binaries @@ -306,10 +322,61 @@ 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 + + notify-desktop-release: + if: ${{ inputs.build_desktop && !cancelled() && !failure() }} + runs-on: ubuntu-24.04-arm + needs: [prepare-build-info, release-desktop] + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DESKTOP }} + VERSION: ${{ inputs.tag_name }} + steps: + - name: Notify Discord + run: | + if [[ -z "$DISCORD_WEBHOOK" ]]; then + echo "No DISCORD_WEBHOOK_DESKTOP secret provided. Skipping notification." + exit 0 + fi + + PAYLOAD=$(cat <](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 fdf7cee5c..f060e087e 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -118,8 +118,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", ) @@ -131,7 +131,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. @@ -148,8 +148,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", ) @@ -158,6 +158,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 = @@ -182,22 +183,46 @@ 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 for Gatekeeper acceptance on end-user machines (without this, + // macOS 12+ shows "app cannot be opened because it is from an unidentified developer"). + // + // 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 + // + // To enable, set SIGN_MACOS=true in the CI environment and uncomment the block below. + 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 d97995bb4..7e4f96aeb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -75,7 +75,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) From 0b97cb3b2c8f4410d4ad69d6c8c486d38821f8ec Mon Sep 17 00:00:00 2001 From: James Rich Date: Tue, 14 Apr 2026 15:24:00 -0500 Subject: [PATCH 2/2] ci: add 'desktop' to required PR labels list Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr_enforce_labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index fa68a597b..267ed0916 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']; + const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor', 'desktop']; console.log('Labels from payload:', latestLabels); const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label)); if (!hasRequiredLabel) {