From 1584369c86134cf5ba54506b3bdcc6d6f5aa5eef Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Fri, 17 Oct 2025 10:24:09 +0100 Subject: [PATCH 1/6] chore(docs): add macOS desktop release instructions for sparkle --- docs/Release.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/Release.md b/docs/Release.md index 6d5525fc8..7a94fae90 100644 --- a/docs/Release.md +++ b/docs/Release.md @@ -138,6 +138,37 @@ successfully and download the generated apps (zipped artifact). We need to sign both the windows `.exe` and `.msix` files using our Extended Validation certificate. Follow the steps on our internal process to do so. +### Publish Desktop (macOS) App + +#### Package the App + +Run the following command to package the macOS app into a DMG file: + +``` +./gradlew packageDmg +``` + +This will generate the DMG and Application file in the `composeApp/build/compose/binaries/main/{dmg,app}` directory. + +#### Set Up Sparkle Updates + +To enable Sparkle updates for the macOS app, use the scripts provided in the Sparkle framework. These scripts are located in `composeApp/build/sparkle/extracted-/bin`. + +- Sign the appcast file: + ``` + ./composeApp/build/sparkle/extracted-/bin/sign_update --ed-key-file certificates/sparkle_eddsa_private.pem + ``` + Replace `` with the sparkle version. + Replace `` with the path to the generated DMG file. + +- Using the `sparkle:edSignature` output from the previous command, create the appcast XML file. + +- Upload the signed appcast file and the DMG to github release. + +- Ensure the app is configured to check for updates using the Sparkle framework. Verify that the appcast URL is correctly set in the app's configuration. + +Once these steps are complete, the macOS app will be ready for distribution with Sparkle updates enabled. + #### 2.8 Create Release **2.8.1** Create a new [Github release](https://github.com/ooni/probe-multiplatform/releases) @@ -152,7 +183,6 @@ file, and swapping the windows `.exe` and `.msix` files for their signed version **2.8.4** Publish release The new Github release post an internal Slack message warning of the new incoming release. - ## Monitoring We use Sentry to monitor for crashes and handled errors. We have specific views for: From 698062cf816781d0a8d346874417aee3e072d57a Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Tue, 21 Oct 2025 09:07:55 +0100 Subject: [PATCH 2/6] chore: update desktop build workflow --- .github/workflows/desktop_macos_make.yml | 33 ------------- .github/workflows/desktop_make.yml | 63 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 33 deletions(-) delete mode 100644 .github/workflows/desktop_macos_make.yml create mode 100644 .github/workflows/desktop_make.yml diff --git a/.github/workflows/desktop_macos_make.yml b/.github/workflows/desktop_macos_make.yml deleted file mode 100644 index dd4618444..000000000 --- a/.github/workflows/desktop_macos_make.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Desktop package apps -on: workflow_dispatch - -jobs: - package: - name: Package desktop apps - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@v4 - - - name: Setup - uses: ./.github/actions/setup - - - name: Package app(macOS) - if: runner.os == 'macOS' - run: ./gradlew copyBrandingToCommonResources packageDmg -Porganization=ooni - - - name: Package Exe(Windows) - if: runner.os == 'Windows' - run: ./gradlew copyBrandingToCommonResources packageExe -Porganization=ooni - - - name: Package Exe(Linux) - if: runner.os == 'Linux' - run: ./gradlew copyBrandingToCommonResources packageDeb packageAppImage -Porganization=ooni - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: desktopApps - path: composeApp/build/compose/binaries diff --git a/.github/workflows/desktop_make.yml b/.github/workflows/desktop_make.yml new file mode 100644 index 000000000..0591519f9 --- /dev/null +++ b/.github/workflows/desktop_make.yml @@ -0,0 +1,63 @@ +name: Desktop package apps +on: + workflow_dispatch: + push: + +concurrency: + group: desktop-packaging-${{ github.ref }} + cancel-in-progress: false + +jobs: + package: + name: Package desktop apps + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + platform: linux + organization: ooni + - runner: macos-latest + platform: macos + organization: ooni + - runner: windows-latest + platform: windows + organization: ooni + env: + ORGANIZATION: ${{ matrix.organization }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup (repo action) + uses: ./.github/actions/setup + + - name: Package app (macOS) + if: matrix.platform == 'macos' + run: ./gradlew copyBrandingToCommonResources packageDmg -Porganization=${{ env.ORGANIZATION }} + + - name: Package Exe (Windows) + if: matrix.platform == 'windows' + shell: pwsh + run: .\gradlew.bat copyBrandingToCommonResources packageExe -Porganization=${{ env.ORGANIZATION }} + + - name: Package Deb(Linux) + if: matrix.platform == 'linux' + run: ./gradlew copyBrandingToCommonResources packageDeb -Porganization=${{ env.ORGANIZATION }} + + - name: Package AppImage (Linux) + if: matrix.platform == 'linux' + run: ./gradlew copyBrandingToCommonResources packageAppImage -Porganization=${{ env.ORGANIZATION }} + + - name: Verify artifacts + run: | + echo "Contents of composeApp/build/compose/binaries:" + ls -la composeApp/build/compose/binaries || true + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: desktopApps-${{ matrix.platform }}-${{ github.run_id }} + path: composeApp/build/compose/binaries/** + retention-days: 7 From 46df56969e842087a07bf407d2c2d5e9b9452c33 Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Tue, 21 Oct 2025 09:21:16 +0100 Subject: [PATCH 3/6] chore: add app image build dependencies --- .github/workflows/desktop_make.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop_make.yml b/.github/workflows/desktop_make.yml index 0591519f9..c9cbef01f 100644 --- a/.github/workflows/desktop_make.yml +++ b/.github/workflows/desktop_make.yml @@ -46,15 +46,17 @@ jobs: if: matrix.platform == 'linux' run: ./gradlew copyBrandingToCommonResources packageDeb -Porganization=${{ env.ORGANIZATION }} + - name: Install libfuse2 (Linux) + if: matrix.platform == 'linux' + run: | + sudo add-apt-repository universe + sudo apt-get update + sudo apt-get install -y libfuse2 + - name: Package AppImage (Linux) if: matrix.platform == 'linux' run: ./gradlew copyBrandingToCommonResources packageAppImage -Porganization=${{ env.ORGANIZATION }} - - name: Verify artifacts - run: | - echo "Contents of composeApp/build/compose/binaries:" - ls -la composeApp/build/compose/binaries || true - - name: Upload artifacts uses: actions/upload-artifact@v4 with: From f6e1a169af2ed692c69173908a4ede32422e8305 Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Tue, 21 Oct 2025 09:39:33 +0100 Subject: [PATCH 4/6] chore: update artifact paths in desktop build configuration --- .github/workflows/desktop_make.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/desktop_make.yml b/.github/workflows/desktop_make.yml index c9cbef01f..fe44340f8 100644 --- a/.github/workflows/desktop_make.yml +++ b/.github/workflows/desktop_make.yml @@ -61,5 +61,10 @@ jobs: uses: actions/upload-artifact@v4 with: name: desktopApps-${{ matrix.platform }}-${{ github.run_id }} - path: composeApp/build/compose/binaries/** + path: | + composeApp/build/compose/binaries/main/exe/OONI Probe-*.exe + composeApp/build/compose/binaries/main/dmg/OONI Probe-*.dmg + composeApp/build/compose/binaries/main/app/OONI Probe.app + composeApp/build/compose/binaries/main/deb/ooni-probe_*_amd64.deb + composeApp/build/compose/binaries/main/appimage-workspace/OONI-Probe-*-x86_64.AppImage retention-days: 7 From 32cd177f68965c7a4c93a721e390fdd2047130d4 Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Tue, 21 Oct 2025 10:02:19 +0100 Subject: [PATCH 5/6] ci: cache/restore Gradle home for desktop packaging --- .github/workflows/desktop_make.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/desktop_make.yml b/.github/workflows/desktop_make.yml index fe44340f8..7fb7d5675 100644 --- a/.github/workflows/desktop_make.yml +++ b/.github/workflows/desktop_make.yml @@ -33,6 +33,28 @@ jobs: - name: Setup (repo action) uses: ./.github/actions/setup + - name: Cache Gradle (Linux/macOS) + if: matrix.platform != 'windows' + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/gradle.properties','**/*.gradle','**/*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Cache Gradle (Windows) + if: matrix.platform == 'windows' + uses: actions/cache@v4 + with: + path: | + %USERPROFILE%\\.gradle\\caches + %USERPROFILE%\\.gradle\\wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/gradle.properties','**/*.gradle','**/*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Package app (macOS) if: matrix.platform == 'macos' run: ./gradlew copyBrandingToCommonResources packageDmg -Porganization=${{ env.ORGANIZATION }} From eaf98c3264efb2b1a26bdd71a53b82d7f20efe78 Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Wed, 22 Oct 2025 11:54:35 +0100 Subject: [PATCH 6/6] chore(desktop): update desktop packaging workflow for macOS and add Sparkle appcast generation --- .github/workflows/desktop_make.yml | 110 ++++++++++++------ .gitignore | 1 + buildSrc/src/main/kotlin/TaskRegistration.kt | 21 +++- .../sparkle/GenerateSparkleAppCastTask.kt | 53 +++++++++ docs/Release.md | 39 +------ 5 files changed, 148 insertions(+), 76 deletions(-) create mode 100644 buildSrc/src/main/kotlin/ooni/sparkle/GenerateSparkleAppCastTask.kt diff --git a/.github/workflows/desktop_make.yml b/.github/workflows/desktop_make.yml index 7fb7d5675..cbd3faa27 100644 --- a/.github/workflows/desktop_make.yml +++ b/.github/workflows/desktop_make.yml @@ -8,24 +8,11 @@ concurrency: cancel-in-progress: false jobs: - package: - name: Package desktop apps - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - platform: linux - organization: ooni - - runner: macos-latest - platform: macos - organization: ooni - - runner: windows-latest - platform: windows - organization: ooni + package-macos: + name: Package macOS app + runs-on: macos-latest env: - ORGANIZATION: ${{ matrix.organization }} + ORGANIZATION: ooni steps: - name: Checkout uses: actions/checkout@v4 @@ -33,8 +20,7 @@ jobs: - name: Setup (repo action) uses: ./.github/actions/setup - - name: Cache Gradle (Linux/macOS) - if: matrix.platform != 'windows' + - name: Cache Gradle uses: actions/cache@v4 with: path: | @@ -44,8 +30,39 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Cache Gradle (Windows) - if: matrix.platform == 'windows' + - name: Setup signing and notarization + run: | + echo "Details are not clear" # Placeholder for actual setup steps + + - name: Package DMG + run: ./gradlew copyBrandingToCommonResources packageDmg -Porganization=${{ env.ORGANIZATION }} + + - name: Sparkle appcast generation + run: ./gradlew generateSparkleAppCast -Porganization=${{ env.ORGANIZATION }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: desktopApps-macos-${{ github.run_id }} + path: | + composeApp/build/compose/binaries/main/dmg/OONI Probe-*.dmg + composeApp/build/compose/binaries/main/app/OONI Probe.app + composeApp/macos-appcast.xml + retention-days: 7 + + package-windows: + name: Package Windows app + runs-on: windows-latest + env: + ORGANIZATION: ooni + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup (repo action) + uses: ./.github/actions/setup + + - name: Cache Gradle uses: actions/cache@v4 with: path: | @@ -55,38 +72,57 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Package app (macOS) - if: matrix.platform == 'macos' - run: ./gradlew copyBrandingToCommonResources packageDmg -Porganization=${{ env.ORGANIZATION }} - - - name: Package Exe (Windows) - if: matrix.platform == 'windows' + - name: Package Exe shell: pwsh run: .\gradlew.bat copyBrandingToCommonResources packageExe -Porganization=${{ env.ORGANIZATION }} - - name: Package Deb(Linux) - if: matrix.platform == 'linux' + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: desktopApps-windows-${{ github.run_id }} + path: | + composeApp/build/compose/binaries/main/exe/OONI Probe-*.exe + retention-days: 7 + + package-linux: + name: Package Linux app + runs-on: ubuntu-latest + env: + ORGANIZATION: ooni + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup (repo action) + uses: ./.github/actions/setup + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/gradle.properties','**/*.gradle','**/*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Package Deb run: ./gradlew copyBrandingToCommonResources packageDeb -Porganization=${{ env.ORGANIZATION }} - - name: Install libfuse2 (Linux) - if: matrix.platform == 'linux' + - name: Install libfuse2 run: | sudo add-apt-repository universe sudo apt-get update sudo apt-get install -y libfuse2 - - name: Package AppImage (Linux) - if: matrix.platform == 'linux' + - name: Package AppImage run: ./gradlew copyBrandingToCommonResources packageAppImage -Porganization=${{ env.ORGANIZATION }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: desktopApps-${{ matrix.platform }}-${{ github.run_id }} + name: desktopApps-linux-${{ github.run_id }} path: | - composeApp/build/compose/binaries/main/exe/OONI Probe-*.exe - composeApp/build/compose/binaries/main/dmg/OONI Probe-*.dmg - composeApp/build/compose/binaries/main/app/OONI Probe.app composeApp/build/compose/binaries/main/deb/ooni-probe_*_amd64.deb composeApp/build/compose/binaries/main/appimage-workspace/OONI-Probe-*-x86_64.AppImage retention-days: 7 diff --git a/.gitignore b/.gitignore index 93656fe16..6fc08ccd4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,6 @@ output composeApp/src/desktopMain/resources/windows/WinSparkle.* /composeApp/src/desktopMain/frameworks/ +composeApp/macos-appcast.xml certificates/*.pem diff --git a/buildSrc/src/main/kotlin/TaskRegistration.kt b/buildSrc/src/main/kotlin/TaskRegistration.kt index 3f44a157c..9e9817d66 100644 --- a/buildSrc/src/main/kotlin/TaskRegistration.kt +++ b/buildSrc/src/main/kotlin/TaskRegistration.kt @@ -1,13 +1,13 @@ +import ooni.appimage.PackageAppImageTask +import ooni.sparkle.GenerateSparkleAppCastTask +import ooni.sparkle.SetupSparkleTask +import ooni.sparkle.WinSparkleSetupTask import org.gradle.api.Project import org.gradle.api.tasks.Exec import org.gradle.api.tasks.JavaExec import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType import java.io.File -import kotlin.let -import ooni.sparkle.SetupSparkleTask -import ooni.sparkle.WinSparkleSetupTask -import ooni.appimage.PackageAppImageTask private fun isMac() = System.getProperty("os.name").lowercase().contains("mac") @@ -79,6 +79,19 @@ private fun Project.registerSparkleTask() { .orElse(layout.buildDirectory.dir("processedResources/desktop/main/macos/")), ) } + + tasks.register("generateSparkleAppCast", GenerateSparkleAppCastTask::class) { + group = "setup" + description = "Generates Sparkle appcast using the specified DMG file." + onlyIf { isMac() } + dependsOn("setupSparkle") + + sparkleVersion.set(providers.gradleProperty("sparkleVersion").orElse("2.8.0")) + edKeyFile.set(rootProject.file("certificates/sparkle_eddsa_private.pem")) + appCastFile.set("macos-appcast.xml") + downloadUrlPrefix.set("https://distribution.ooni.org") + + } } private fun Project.registerWinSparkleTask() { diff --git a/buildSrc/src/main/kotlin/ooni/sparkle/GenerateSparkleAppCastTask.kt b/buildSrc/src/main/kotlin/ooni/sparkle/GenerateSparkleAppCastTask.kt new file mode 100644 index 000000000..18f9f0647 --- /dev/null +++ b/buildSrc/src/main/kotlin/ooni/sparkle/GenerateSparkleAppCastTask.kt @@ -0,0 +1,53 @@ +package ooni.sparkle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class GenerateSparkleAppCastTask : DefaultTask() { + @get:Inject + abstract val execOperations: ExecOperations + + @get:Input + abstract val sparkleVersion: Property + + @get:InputFile + abstract val edKeyFile: RegularFileProperty + + @get:Input + abstract val appCastFile: Property + + @get:Input + abstract val downloadUrlPrefix: Property + + @TaskAction + fun run() { + val sparkleTool = sparkleVersion + .map { project.layout.buildDirectory.file("sparkle/extracted-$it/bin/generate_appcast") } + .get() + + val dmgFile = project.file("build/compose/binaries/main/dmg") + + val command = listOf( + sparkleTool.get().asFile.absolutePath, + "-o", + appCastFile.get(), + "--ed-key-file", + edKeyFile.get().asFile.absolutePath, + "--download-url-prefix", + downloadUrlPrefix.get(), + dmgFile.absolutePath + ) + + project.logger.lifecycle("Running Sparkle generate_appcast with command: ${command.joinToString(" ")}") + + execOperations.exec { + commandLine(command) + }.assertNormalExitValue() + } +} diff --git a/docs/Release.md b/docs/Release.md index 7a94fae90..c28a5b0f2 100644 --- a/docs/Release.md +++ b/docs/Release.md @@ -135,39 +135,8 @@ successfully and download the generated apps (zipped artifact). ##### 2.7.2 Sign windows app -We need to sign both the windows `.exe` and `.msix` files using our Extended Validation certificate. -Follow the steps on our internal process to do so. - -### Publish Desktop (macOS) App - -#### Package the App - -Run the following command to package the macOS app into a DMG file: - -``` -./gradlew packageDmg -``` - -This will generate the DMG and Application file in the `composeApp/build/compose/binaries/main/{dmg,app}` directory. - -#### Set Up Sparkle Updates - -To enable Sparkle updates for the macOS app, use the scripts provided in the Sparkle framework. These scripts are located in `composeApp/build/sparkle/extracted-/bin`. - -- Sign the appcast file: - ``` - ./composeApp/build/sparkle/extracted-/bin/sign_update --ed-key-file certificates/sparkle_eddsa_private.pem - ``` - Replace `` with the sparkle version. - Replace `` with the path to the generated DMG file. - -- Using the `sparkle:edSignature` output from the previous command, create the appcast XML file. - -- Upload the signed appcast file and the DMG to github release. - -- Ensure the app is configured to check for updates using the Sparkle framework. Verify that the appcast URL is correctly set in the app's configuration. - -Once these steps are complete, the macOS app will be ready for distribution with Sparkle updates enabled. +- We need to sign the windows `.exe` file using our Extended Validation certificate. Follow the steps on our internal process to do so. +- Generate the WinSparkle appcast for the signed `.exe` file. #### 2.8 Create Release @@ -177,12 +146,12 @@ based on the new tag. **2.8.2** Write our manual release notes and add at the bottom the automatic changelog using the `Generate release notes` button. -**2.8.3** Upload all the desktop files downloaded during step *2.7.1*, except the `download.html` -file, and swapping the windows `.exe` and `.msix` files for their signed versions (step *2.7.2*). +**2.8.3** Upload all the desktop files downloaded during step *2.7.1*, and swapping the windows `.exe` files for their signed versions (step *2.7.2*). **2.8.4** Publish release The new Github release post an internal Slack message warning of the new incoming release. + ## Monitoring We use Sentry to monitor for crashes and handled errors. We have specific views for: