release.yml 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. name: Release
  2. # Produces versioned distributables for ALL targets a host can build, into a GitHub Release.
  3. # Each format is built independently so one failing format never blocks the others, and the
  4. # collect step uploads whatever actually got produced (partial success is still published).
  5. # linux (deb/rpm/AppImage) + android (apk) + iso -> ubuntu-latest
  6. # windows (exe) -> windows-latest
  7. # macos (dmg) -> macos-latest
  8. # Skipped/failed formats are logged as ::warning:: and never fail the whole release.
  9. # NOTE: iOS cannot be produced — the project has no iOS Kotlin target (commonMain uses
  10. # JVM-only libs); there is no iOS step.
  11. on:
  12. workflow_dispatch:
  13. push:
  14. branches:
  15. - main
  16. tags:
  17. - 'v*'
  18. # Build jobs only read the repo (least privilege — "read only gh" in the build step);
  19. # only the publish job below requests write to create the release.
  20. permissions:
  21. contents: read
  22. jobs:
  23. package:
  24. name: Package (${{ matrix.target }})
  25. strategy:
  26. fail-fast: false
  27. matrix:
  28. include:
  29. - os: ubuntu-latest
  30. target: linux-android-iso
  31. - os: windows-latest
  32. target: windows
  33. - os: macos-latest
  34. target: macos
  35. runs-on: ${{ matrix.os }}
  36. steps:
  37. - uses: actions/checkout@v4
  38. - name: Set up JDK 17
  39. uses: actions/setup-java@v4
  40. with:
  41. java-version: '17'
  42. distribution: 'temurin'
  43. - name: Setup Gradle
  44. uses: gradle/actions/setup-gradle@v4
  45. - name: Make gradlew executable
  46. shell: bash
  47. run: chmod +x gradlew || true
  48. - name: Read app name + version
  49. id: meta
  50. shell: bash
  51. run: |
  52. echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  53. echo "appname=$(grep -E '^app-name' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  54. # Decode the Android release keystore from a base64 secret (optional). Without the
  55. # secret the APK is debug-signed by the build fallback — still installable.
  56. - name: Decode Android keystore
  57. if: matrix.os == 'ubuntu-latest'
  58. shell: bash
  59. env:
  60. ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
  61. run: |
  62. if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
  63. echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore"
  64. echo "ANDROID_KEYSTORE_FILE=$RUNNER_TEMP/release.keystore" >> "$GITHUB_ENV"
  65. echo "Android release keystore decoded"
  66. else
  67. echo "::warning::no ANDROID_KEYSTORE_BASE64 secret — APK will be debug-signed"
  68. fi
  69. # ---------- linux desktop formats + android apk + reports (ubuntu) ----------
  70. # One invocation with --continue so deb/rpm/AppImage/apk/reports each build to
  71. # completion independently: a failure in one does NOT abort the others.
  72. - name: Build linux desktop + android + reports
  73. if: matrix.os == 'ubuntu-latest'
  74. shell: bash
  75. continue-on-error: true
  76. env:
  77. # ANDROID_KEYSTORE_FILE is exported by the decode step above (if present)
  78. ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
  79. ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
  80. ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
  81. run: |
  82. sudo apt-get update
  83. sudo apt-get install -y rpm || echo "::warning::rpm tools missing — .rpm may be skipped"
  84. ./gradlew packageFullDeb :desktopApp:packageReleaseRpm packageAppImageFile \
  85. :androidApp:assembleRelease postBuildCodeCheck \
  86. -PdepCheck=false --continue --stacktrace \
  87. || echo "::warning::one or more linux/android formats failed — others still collected"
  88. # ---------- bootable live ISO (ubuntu, needs root) ----------
  89. # Separate step because makeIso needs root (debootstrap/chroot/mksquashfs); on CI it
  90. # runs via passwordless sudo. Isolated so a failed/slow ISO can't drop the other formats.
  91. - name: Build live ISO
  92. if: matrix.os == 'ubuntu-latest'
  93. shell: bash
  94. continue-on-error: true
  95. run: |
  96. sudo apt-get install -y debootstrap squashfs-tools xorriso mtools dpkg-dev \
  97. grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring \
  98. || echo "::warning::iso toolchain missing — .iso may be skipped"
  99. ./gradlew makeIso -PdepCheck=false --stacktrace \
  100. || echo "::warning::makeIso failed — .iso skipped"
  101. ls -la releases/*.iso 2>/dev/null && echo "ISO produced" \
  102. || echo "::warning::no .iso in releases/ — makeIso skipped or failed"
  103. # ---------- windows (.exe) ----------
  104. - name: Build windows .exe
  105. if: matrix.os == 'windows-latest'
  106. shell: bash
  107. continue-on-error: true
  108. run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace || echo "::warning::windows .exe failed — skipped"
  109. # ---------- macos (.dmg) ----------
  110. # Signing/notarization activate only when the Apple Developer ID secrets are present
  111. # (see desktopApp.gradle.kts macOS block); otherwise the .dmg is built unsigned.
  112. - name: Build macOS .dmg
  113. if: matrix.os == 'macos-latest'
  114. shell: bash
  115. continue-on-error: true
  116. env:
  117. MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
  118. MACOS_BUNDLE_ID: ${{ secrets.MACOS_BUNDLE_ID }}
  119. MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
  120. MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
  121. MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
  122. run: ./gradlew :desktopApp:packageReleaseDmg -PdepCheck=false --stacktrace || echo "::warning::macOS .dmg failed — skipped"
  123. # ---------- collect whatever got produced (versioned names) ----------
  124. # Copies from every known jpackage/build output dir; dirs absent on this OS are simply
  125. # skipped. Mirrors the rename scheme of the :collectReleases Gradle task but tolerates
  126. # partial builds so we never lose the formats that DID succeed.
  127. - name: Collect artifacts
  128. shell: bash
  129. run: |
  130. VER='${{ steps.meta.outputs.version }}'
  131. APP='${{ steps.meta.outputs.appname }}'
  132. BASE="$APP-$VER"
  133. PKG=packages/main-release
  134. mkdir -p upload
  135. copy() { # <glob-dir> <ext>
  136. for f in "$1"/*."$2"; do
  137. [ -e "$f" ] && cp -v "$f" "upload/$BASE.$2" && return 0
  138. done
  139. return 0
  140. }
  141. copy "$PKG/deb" deb
  142. copy "$PKG/rpm" rpm
  143. copy "$PKG/appimage" AppImage
  144. copy "$PKG/exe" exe
  145. copy "$PKG/dmg" dmg
  146. for f in androidApp/build/outputs/apk/release/*.apk; do
  147. [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break
  148. done
  149. for f in releases/*.iso; do
  150. [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break
  151. done
  152. # reports tree zipped into the release
  153. if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
  154. (cd reports && zip -r "../upload/$BASE-reports.zip" .) || echo "::warning::reports zip failed"
  155. fi
  156. echo "=== produced for ${{ matrix.target }} ==="; ls -la upload/ || true
  157. - name: Upload build artifacts
  158. if: always()
  159. uses: actions/upload-artifact@v4
  160. with:
  161. name: dist-${{ matrix.target }}
  162. path: upload/
  163. if-no-files-found: warn
  164. release:
  165. name: Publish GitHub Release
  166. needs: package
  167. if: always()
  168. runs-on: ubuntu-latest
  169. permissions:
  170. contents: write
  171. steps:
  172. - uses: actions/checkout@v4
  173. - name: Read version
  174. id: ver
  175. shell: bash
  176. run: echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  177. - name: Download all artifacts
  178. uses: actions/download-artifact@v4
  179. with:
  180. path: dist
  181. merge-multiple: true
  182. - name: List collected
  183. shell: bash
  184. run: ls -la dist/ || echo "no artifacts produced"
  185. - name: Publish to GitHub Release
  186. uses: softprops/action-gh-release@v2
  187. with:
  188. tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}', steps.ver.outputs.version) }}
  189. name: mjdev-desktop v${{ steps.ver.outputs.version }}
  190. files: dist/**
  191. fail_on_unmatched_files: false
  192. env:
  193. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}