Quellcode durchsuchen

github actions fixes

Milan Jurkulak vor 3 Monaten
Ursprung
Commit
25bad2be7f
3 geänderte Dateien mit 158 neuen und 40 gelöschten Zeilen
  1. 102 40
      .github/workflows/release.yml
  2. 29 0
      androidApp/androidApp.gradle.kts
  3. 27 0
      desktopApp/desktopApp.gradle.kts

+ 102 - 40
.github/workflows/release.yml

@@ -1,13 +1,14 @@
 name: Release
 
-# Produces versioned distributables for all targets into a GitHub Release.
-# Targets that cannot be built on a given host (or fail) are skipped and logged
-# as ::warning:: — they never fail the whole release.
-#   linux   (deb/rpm/AppImage) + android (apk)  -> ubuntu-latest
-#   windows (exe)                               -> windows-latest
-#   macos   (dmg)  + ios                        -> macos-latest
-# NOTE: iOS currently cannot be produced — the project has no iOS Kotlin target
-#       (commonMain uses JVM-only libraries); the iOS step only logs a skip.
+# Produces versioned distributables for ALL targets a host can build, into a GitHub Release.
+# Each format is built independently so one failing format never blocks the others, and the
+# collect step uploads whatever actually got produced (partial success is still published).
+#   linux (deb/rpm/AppImage) + android (apk) + iso  -> ubuntu-latest
+#   windows (exe)                                   -> windows-latest
+#   macos   (dmg)                                   -> macos-latest
+# Skipped/failed formats are logged as ::warning:: and never fail the whole release.
+# NOTE: iOS cannot be produced — the project has no iOS Kotlin target (commonMain uses
+#       JVM-only libs); there is no iOS step.
 
 on:
   workflow_dispatch:
@@ -17,8 +18,10 @@ on:
     tags:
       - 'v*'
 
+# Build jobs only read the repo (least privilege — "read only gh" in the build step);
+# only the publish job below requests write to create the release.
 permissions:
-  contents: write
+  contents: read
 
 jobs:
   package:
@@ -28,11 +31,11 @@ jobs:
       matrix:
         include:
           - os: ubuntu-latest
-            target: linux-android
+            target: linux-android-iso
           - os: windows-latest
             target: windows
           - os: macos-latest
-            target: macos-ios
+            target: macos
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v4
@@ -50,62 +53,119 @@ jobs:
         shell: bash
         run: chmod +x gradlew || true
 
-      - name: Read version
-        id: ver
+      - name: Read app name + version
+        id: meta
         shell: bash
-        run: echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
+        run: |
+          echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
+          echo "appname=$(grep -E '^app-name' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
 
-      # ---------- linux + android + reports (ubuntu) ----------
-      - name: Build linux + android + reports
+      # Decode the Android release keystore from a base64 secret (optional). Without the
+      # secret the APK is debug-signed by the build fallback — still installable.
+      - name: Decode Android keystore
+        if: matrix.os == 'ubuntu-latest'
+        shell: bash
+        env:
+          ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
+        run: |
+          if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
+            echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore"
+            echo "ANDROID_KEYSTORE_FILE=$RUNNER_TEMP/release.keystore" >> "$GITHUB_ENV"
+            echo "Android release keystore decoded"
+          else
+            echo "::warning::no ANDROID_KEYSTORE_BASE64 secret — APK will be debug-signed"
+          fi
+
+      # ---------- linux desktop formats + android apk + reports (ubuntu) ----------
+      # One invocation with --continue so deb/rpm/AppImage/apk/reports each build to
+      # completion independently: a failure in one does NOT abort the others.
+      - name: Build linux desktop + android + reports
         if: matrix.os == 'ubuntu-latest'
         shell: bash
         continue-on-error: true
+        env:
+          # ANDROID_KEYSTORE_FILE is exported by the decode step above (if present)
+          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
+          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
+          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
         run: |
           sudo apt-get update
           sudo apt-get install -y rpm || echo "::warning::rpm tools missing — .rpm may be skipped"
-          # ISO toolchain (debootstrap/chroot/mksquashfs/grub+mtools) so buildAll's makeIso can
-          # assemble the bootable live image; missing tools would just skip the ISO.
+          ./gradlew packageFullDeb :desktopApp:packageReleaseRpm packageAppImageFile \
+            :androidApp:assembleRelease postBuildCodeCheck \
+            -PdepCheck=false --continue --stacktrace \
+            || echo "::warning::one or more linux/android formats failed — others still collected"
+
+      # ---------- bootable live ISO (ubuntu, needs root) ----------
+      # Separate step because makeIso needs root (debootstrap/chroot/mksquashfs); on CI it
+      # runs via passwordless sudo. Isolated so a failed/slow ISO can't drop the other formats.
+      - name: Build live ISO
+        if: matrix.os == 'ubuntu-latest'
+        shell: bash
+        continue-on-error: true
+        run: |
           sudo apt-get install -y debootstrap squashfs-tools xorriso mtools dpkg-dev \
             grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring \
             || echo "::warning::iso toolchain missing — .iso may be skipped"
-          ./gradlew buildAll -PdepCheck=false --stacktrace || echo "::warning::linux/android buildAll failed — skipped"
-          # makeIso is part of buildAll; surface whether the .iso was actually produced
-          ls -la releases/*.iso 2>/dev/null && echo "ISO produced" || echo "::warning::no .iso in releases/ — makeIso skipped or failed"
+          ./gradlew makeIso -PdepCheck=false --stacktrace \
+            || echo "::warning::makeIso failed — .iso skipped"
+          ls -la releases/*.iso 2>/dev/null && echo "ISO produced" \
+            || echo "::warning::no .iso in releases/ — makeIso skipped or failed"
 
       # ---------- windows (.exe) ----------
       - name: Build windows .exe
         if: matrix.os == 'windows-latest'
         shell: bash
         continue-on-error: true
-        run: ./gradlew :desktopApp:packageReleaseExe --stacktrace || echo "::warning::windows .exe failed — skipped"
+        run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace || echo "::warning::windows .exe failed — skipped"
 
       # ---------- macos (.dmg) ----------
+      # Signing/notarization activate only when the Apple Developer ID secrets are present
+      # (see desktopApp.gradle.kts macOS block); otherwise the .dmg is built unsigned.
       - name: Build macOS .dmg
         if: matrix.os == 'macos-latest'
         shell: bash
         continue-on-error: true
-        run: ./gradlew :desktopApp:packageReleaseDmg --stacktrace || echo "::warning::macOS .dmg failed — skipped"
-
-      # ---------- ios (not available) ----------
-      - name: iOS (skipped — no iOS target)
-        if: matrix.os == 'macos-latest'
-        shell: bash
-        run: |
-          echo "::warning::iOS skipped — project has no iOS Kotlin target (commonMain uses JVM-only libs vlcj/jna)"
-
-      # ---------- collect whatever got produced ----------
+        env:
+          MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
+          MACOS_BUNDLE_ID: ${{ secrets.MACOS_BUNDLE_ID }}
+          MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
+          MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
+          MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
+        run: ./gradlew :desktopApp:packageReleaseDmg -PdepCheck=false --stacktrace || echo "::warning::macOS .dmg failed — skipped"
+
+      # ---------- collect whatever got produced (versioned names) ----------
+      # Copies from every known jpackage/build output dir; dirs absent on this OS are simply
+      # skipped. Mirrors the rename scheme of the :collectReleases Gradle task but tolerates
+      # partial builds so we never lose the formats that DID succeed.
       - name: Collect artifacts
         shell: bash
         run: |
-          VER='${{ steps.ver.outputs.version }}'
+          VER='${{ steps.meta.outputs.version }}'
+          APP='${{ steps.meta.outputs.appname }}'
+          BASE="$APP-$VER"
+          PKG=packages/main-release
           mkdir -p upload
-          # linux/android: buildAll already wrote versioned files into releases/
-          cp -v releases/* upload/ 2>/dev/null || true
-          # desktop installers from jpackage (windows/mac, also deb/rpm if present)
-          find packages -type f \( -name '*.exe' -o -name '*.msi' -o -name '*.dmg' -o -name '*.pkg' \) -exec cp -v {} upload/ \; 2>/dev/null || true
-          # reports copied into the release (zipped tree)
+          copy() { # <glob-dir> <ext>
+            for f in "$1"/*."$2"; do
+              [ -e "$f" ] && cp -v "$f" "upload/$BASE.$2" && return 0
+            done
+            return 0
+          }
+          copy "$PKG/deb" deb
+          copy "$PKG/rpm" rpm
+          copy "$PKG/appimage" AppImage
+          copy "$PKG/exe" exe
+          copy "$PKG/dmg" dmg
+          for f in androidApp/build/outputs/apk/release/*.apk; do
+            [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break
+          done
+          for f in releases/*.iso; do
+            [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break
+          done
+          # reports tree zipped into the release
           if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
-            (cd reports && zip -r "../upload/mjdev-desktop-${VER}-reports.zip" .) || echo "::warning::reports zip failed"
+            (cd reports && zip -r "../upload/$BASE-reports.zip" .) || echo "::warning::reports zip failed"
           fi
           echo "=== produced for ${{ matrix.target }} ==="; ls -la upload/ || true
 
@@ -122,6 +182,8 @@ jobs:
     needs: package
     if: always()
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
     steps:
       - uses: actions/checkout@v4
 
@@ -148,4 +210,4 @@ jobs:
           files: dist/**
           fail_on_unmatched_files: false
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 29 - 0
androidApp/androidApp.gradle.kts

@@ -29,6 +29,33 @@ android {
             pickFirsts += "META-INF/native-image/**"
         }
     }
+    // Release signing. Keystore + credentials come from env vars (CI secrets) or Gradle
+    // properties (local ~/.gradle/gradle.properties) — never hardcoded. When no release
+    // keystore is configured we fall back to the debug key so the release APK is still
+    // *signed* and installable (an unsigned APK is rejected by Android on install).
+    val releaseStoreFile = (providers.environmentVariable("ANDROID_KEYSTORE_FILE").orNull
+        ?: providers.gradleProperty("android.keystore.file").orNull)?.let(::file)
+    val releaseStorePassword = providers.environmentVariable("ANDROID_KEYSTORE_PASSWORD").orNull
+        ?: providers.gradleProperty("android.keystore.password").orNull
+    val releaseKeyAlias = providers.environmentVariable("ANDROID_KEY_ALIAS").orNull
+        ?: providers.gradleProperty("android.key.alias").orNull
+    val releaseKeyPassword = providers.environmentVariable("ANDROID_KEY_PASSWORD").orNull
+        ?: providers.gradleProperty("android.key.password").orNull
+    val hasReleaseKeystore = releaseStoreFile?.exists() == true &&
+        !releaseStorePassword.isNullOrBlank() &&
+        !releaseKeyAlias.isNullOrBlank() &&
+        !releaseKeyPassword.isNullOrBlank()
+
+    signingConfigs {
+        if (hasReleaseKeystore) {
+            create("release") {
+                storeFile = releaseStoreFile
+                storePassword = releaseStorePassword
+                keyAlias = releaseKeyAlias
+                keyPassword = releaseKeyPassword
+            }
+        }
+    }
     buildTypes {
         getByName("release") {
             isDebuggable = true
@@ -36,6 +63,8 @@ android {
             isMinifyEnabled = false
             isShrinkResources = false
             isPseudoLocalesEnabled = true
+            // real keystore when provided, otherwise debug key (still a signed, installable APK)
+            signingConfig = signingConfigs.getByName(if (hasReleaseKeystore) "release" else "debug")
         }
     }
     compileOptions {

+ 27 - 0
desktopApp/desktopApp.gradle.kts

@@ -49,6 +49,33 @@ compose.desktop {
                 menuGroup = libs.versions.app.menu.group.get()
                 vendor = libs.versions.app.vendor.get()
             }
+            // macOS code signing + notarization. Requires a paid Apple Developer ID certificate
+            // installed in the build keychain — credentials come from env (CI secrets), never
+            // hardcoded. With no MACOS_SIGN_IDENTITY the .dmg is built unsigned exactly as before
+            // (Gatekeeper "open anyway"), so nothing changes until a real certificate is provided.
+            val macSignIdentity = System.getenv("MACOS_SIGN_IDENTITY")
+            if (!macSignIdentity.isNullOrBlank()) {
+                macOS {
+                    bundleID = System.getenv("MACOS_BUNDLE_ID") ?: "org.mjdev.desktop"
+                    signing {
+                        sign.set(true)
+                        identity.set(macSignIdentity)
+                    }
+                    val notaryAppleId = System.getenv("MACOS_NOTARY_APPLE_ID")
+                    val notaryPassword = System.getenv("MACOS_NOTARY_PASSWORD")
+                    val notaryTeamId = System.getenv("MACOS_NOTARY_TEAM_ID")
+                    if (!notaryAppleId.isNullOrBlank() &&
+                        !notaryPassword.isNullOrBlank() &&
+                        !notaryTeamId.isNullOrBlank()
+                    ) {
+                        notarization {
+                            appleID.set(notaryAppleId)
+                            password.set(notaryPassword)
+                            teamID.set(notaryTeamId)
+                        }
+                    }
+                }
+            }
             targetFormats(
                 TargetFormat.Deb,
                 TargetFormat.Rpm,