release.yml 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. # Single apt transaction on ubuntu — avoids dpkg lock races from overlapping apt-get runs.
  70. - name: Install Linux build dependencies
  71. if: matrix.os == 'ubuntu-latest'
  72. shell: bash
  73. run: |
  74. sudo apt-get update
  75. sudo apt-get install -y rpm debootstrap squashfs-tools xorriso mtools dpkg-dev \
  76. grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring zip \
  77. || echo "::warning::some apt packages missing — some formats may be skipped"
  78. # ---------- linux + android + iso + reports (ubuntu) ----------
  79. # buildAll = collectReleases (deb/rpm/AppImage/apk -> releases/) + makeIso + reports.
  80. # --continue keeps going when one format fails; makeIso is warn-only so ISO never
  81. # blocks the formats that already succeeded.
  82. - name: Build linux + android + iso + reports
  83. if: matrix.os == 'ubuntu-latest'
  84. shell: bash
  85. continue-on-error: true
  86. env:
  87. ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
  88. ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
  89. ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
  90. run: |
  91. ./gradlew buildAll -PdepCheck=false --continue --stacktrace \
  92. || echo "::warning::buildAll had failures — collecting partial artifacts"
  93. echo "=== releases/ ==="
  94. ls -la releases/ 2>/dev/null || echo "(empty)"
  95. ls -la releases/*.iso 2>/dev/null && echo "ISO produced" \
  96. || echo "::warning::no .iso in releases/ — makeIso skipped or failed"
  97. # ---------- windows (.exe) ----------
  98. - name: Build windows .exe
  99. if: matrix.os == 'windows-latest'
  100. shell: bash
  101. continue-on-error: true
  102. run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace || echo "::warning::windows .exe failed — skipped"
  103. # ---------- macos (.dmg) ----------
  104. # Signing/notarization activate only when the Apple Developer ID secrets are present
  105. # (see desktopApp.gradle.kts macOS block); otherwise the .dmg is built unsigned.
  106. - name: Build macOS .dmg
  107. if: matrix.os == 'macos-latest'
  108. shell: bash
  109. continue-on-error: true
  110. env:
  111. MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
  112. MACOS_BUNDLE_ID: ${{ secrets.MACOS_BUNDLE_ID }}
  113. MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
  114. MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
  115. MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
  116. run: ./gradlew :desktopApp:packageReleaseDmg -PdepCheck=false --stacktrace || echo "::warning::macOS .dmg failed — skipped"
  117. # ---------- collect whatever got produced (versioned names) ----------
  118. # Primary source is releases/ (collectReleases + makeIso). Fall back to jpackage output
  119. # dirs for partial builds and for platform-specific runners (exe/dmg).
  120. - name: Collect artifacts
  121. shell: bash
  122. continue-on-error: true
  123. run: |
  124. set +e
  125. VER='${{ steps.meta.outputs.version }}'
  126. APP='${{ steps.meta.outputs.appname }}'
  127. BASE="$APP-$VER"
  128. PKG=packages/main-release
  129. mkdir -p upload
  130. copy_pkg() {
  131. local dir="$1" ext="$2"
  132. for f in "$dir"/*."$ext"; do
  133. if [ -e "$f" ]; then
  134. cp -v "$f" "upload/$BASE.$ext"
  135. return 0
  136. fi
  137. done
  138. return 0
  139. }
  140. if compgen -G "releases/*" > /dev/null; then
  141. cp -av releases/* upload/
  142. fi
  143. copy_pkg "$PKG/exe" exe
  144. copy_pkg "$PKG/dmg" dmg
  145. [ -f "upload/$BASE.deb" ] || copy_pkg "$PKG/deb" deb
  146. [ -f "upload/$BASE.rpm" ] || copy_pkg "$PKG/rpm" rpm
  147. [ -f "upload/$BASE.AppImage" ] || copy_pkg "$PKG/appimage" AppImage
  148. if [ ! -f "upload/$BASE.apk" ]; then
  149. for f in androidApp/build/outputs/apk/release/*.apk; do
  150. [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break
  151. done
  152. fi
  153. if [ ! -f "upload/$BASE.iso" ]; then
  154. for f in releases/*.iso; do
  155. [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break
  156. done
  157. fi
  158. if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
  159. (cd reports && zip -r "../upload/$BASE-reports.zip" .) || echo "::warning::reports zip failed"
  160. fi
  161. echo "=== produced for ${{ matrix.target }} ==="
  162. ls -la upload/ || true
  163. - name: Upload build artifacts
  164. if: always()
  165. uses: actions/upload-artifact@v4
  166. with:
  167. name: dist-${{ matrix.target }}
  168. path: upload/
  169. if-no-files-found: warn
  170. release:
  171. name: Publish GitHub Release
  172. needs: package
  173. if: always()
  174. runs-on: ubuntu-latest
  175. permissions:
  176. contents: write
  177. steps:
  178. - uses: actions/checkout@v4
  179. - name: Read version
  180. id: ver
  181. shell: bash
  182. run: echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  183. - name: Download all artifacts
  184. uses: actions/download-artifact@v4
  185. with:
  186. path: dist
  187. merge-multiple: true
  188. - name: List collected
  189. shell: bash
  190. run: ls -la dist/ || echo "no artifacts produced"
  191. - name: Publish to GitHub Release
  192. uses: softprops/action-gh-release@v2
  193. with:
  194. tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}', steps.ver.outputs.version) }}
  195. name: mjdev-desktop v${{ steps.ver.outputs.version }}
  196. files: dist/**
  197. fail_on_unmatched_files: false
  198. env:
  199. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}