release.yml 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. name: Release
  2. # Produces versioned distributables for ALL targets a host can build, into a GitHub Release.
  3. # Each target verifies its own required artifacts. Publishing uses every successful target, so
  4. # apk/exe are not held hostage by Linux-only packaging dependencies.
  5. # linux (deb/rpm/AppImage) + iso -> debian:trixie
  6. # android (apk) -> ubuntu-latest
  7. # windows (exe) -> windows-latest
  8. # macos (dmg) -> macos-latest
  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-linux-iso:
  24. name: Package (linux-iso)
  25. runs-on: ubuntu-latest
  26. container:
  27. image: debian:trixie
  28. options: --privileged
  29. steps:
  30. - name: Install Debian build dependencies
  31. shell: bash
  32. run: |
  33. set -euo pipefail
  34. apt-get update
  35. apt-get install -y --no-install-recommends \
  36. bash ca-certificates coreutils curl findutils git \
  37. tar unzip xz-utils zip zstd \
  38. build-essential pkg-config rpm fakeroot \
  39. debootstrap squashfs-tools xorriso mtools dpkg-dev \
  40. grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring \
  41. wayland-protocols libwayland-dev libwayland-bin \
  42. libxkbcommon-dev libcairo2-dev libcairo2 libpixman-1-dev libdrm-dev \
  43. libseat-dev libgbm-dev libegl-dev libgles-dev \
  44. libcap-dev liblcms2-dev libsystemd-dev libudev-dev libvulkan-dev \
  45. libxcb1-dev libxcb-composite0-dev libxcb-dri3-dev \
  46. libxcb-image0-dev libxcb-present-dev libxcb-render0-dev \
  47. libxcb-render-util0-dev libxcb-res0-dev libxcb-shm0-dev libxcb-xfixes0-dev \
  48. libxcb-ewmh-dev libxcb-icccm4-dev libxcb-xinput-dev libxcb-errors-dev \
  49. libx11-xcb-dev libwlroots-0.18-dev
  50. wayland-scanner --version
  51. pkg-config --modversion wlroots-0.18
  52. - uses: actions/checkout@v4
  53. - name: Set up JDK 17
  54. uses: actions/setup-java@v4
  55. with:
  56. java-version: '17'
  57. distribution: 'temurin'
  58. - name: Setup Gradle
  59. uses: gradle/actions/setup-gradle@v4
  60. with:
  61. cache-read-only: false
  62. gradle-home-cache-cleanup: true
  63. - name: Cache Kotlin/Native
  64. uses: actions/cache@v4
  65. with:
  66. path: ~/.konan
  67. key: ${{ runner.os }}-debian-trixie-konan-${{ hashFiles('gradle/libs.versions.toml', 'gradle.properties', 'settings.gradle.kts', 'build.gradle.kts', 'compositor/*.gradle.kts') }}
  68. restore-keys: |
  69. ${{ runner.os }}-debian-trixie-konan-
  70. - name: Cache AppImage tool
  71. uses: actions/cache@v4
  72. with:
  73. path: .gradle/tools
  74. key: ${{ runner.os }}-debian-trixie-appimagetool-${{ hashFiles('gradle/libs.versions.toml', 'build.gradle.kts') }}
  75. restore-keys: |
  76. ${{ runner.os }}-debian-trixie-appimagetool-
  77. - name: Make gradlew executable
  78. shell: bash
  79. run: chmod +x gradlew || true
  80. - name: Read app name + version
  81. id: meta
  82. shell: bash
  83. run: |
  84. echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  85. echo "appname=$(grep -E '^app-name' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  86. # One Gradle invocation keeps dependency resolution/configuration cache hot and avoids
  87. # re-entering packageFullDeb before makeIso. --continue keeps independent artifacts building
  88. # when one Linux format fails; artifact collection below publishes whatever succeeded.
  89. - name: Build linux artifacts
  90. shell: bash
  91. continue-on-error: true
  92. run: ./gradlew packageAppImageFile :desktopApp:packageReleaseRpm packageFullDeb makeIso -PdepCheck=false --stacktrace --continue
  93. # Primary source is releases/ (collectReleases + makeIso). Fall back to jpackage output
  94. # dirs for partial builds.
  95. - name: Collect artifacts
  96. if: always()
  97. shell: bash
  98. run: |
  99. set -euo pipefail
  100. VER='${{ steps.meta.outputs.version }}'
  101. APP='${{ steps.meta.outputs.appname }}'
  102. BASE="$APP-$VER"
  103. PKG=packages/main-release
  104. mkdir -p upload
  105. copy_pkg() {
  106. local dir="$1" ext="$2"
  107. for f in "$dir"/*."$ext"; do
  108. if [ -e "$f" ]; then
  109. cp -v "$f" "upload/$BASE.$ext"
  110. return 0
  111. fi
  112. done
  113. return 0
  114. }
  115. if compgen -G "releases/*" > /dev/null; then
  116. cp -av releases/* upload/
  117. fi
  118. [ -f "upload/$BASE.deb" ] || copy_pkg "$PKG/deb" deb
  119. [ -f "upload/$BASE.rpm" ] || copy_pkg "$PKG/rpm" rpm
  120. [ -f "upload/$BASE.AppImage" ] || copy_pkg "$PKG/appimage" AppImage
  121. if [ ! -f "upload/$BASE.iso" ]; then
  122. for f in releases/*.iso; do
  123. [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break
  124. done
  125. fi
  126. if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
  127. (cd reports && zip -r "../upload/$BASE-reports.zip" .)
  128. fi
  129. echo "=== produced for linux-iso ==="
  130. ls -la upload/
  131. require_artifact() {
  132. local file="upload/$1"
  133. if [ ! -s "$file" ]; then
  134. echo "::error::required release artifact is missing or empty: $1"
  135. exit 1
  136. fi
  137. }
  138. for required in "$BASE.deb" "$BASE.rpm" "$BASE.AppImage" "$BASE.iso"; do
  139. if [ ! -s "upload/$required" ]; then
  140. echo "::warning::linux artifact was not produced: $required"
  141. fi
  142. done
  143. require_artifact "$BASE.iso"
  144. - name: Upload build artifacts
  145. if: always()
  146. uses: actions/upload-artifact@v4
  147. with:
  148. name: dist-linux-iso
  149. path: upload/
  150. if-no-files-found: warn
  151. package:
  152. name: Package (${{ matrix.target }})
  153. strategy:
  154. fail-fast: false
  155. matrix:
  156. include:
  157. - os: ubuntu-latest
  158. target: android
  159. - os: windows-latest
  160. target: windows
  161. - os: macos-latest
  162. target: macos
  163. runs-on: ${{ matrix.os }}
  164. steps:
  165. - uses: actions/checkout@v4
  166. - name: Set up JDK 17
  167. uses: actions/setup-java@v4
  168. with:
  169. java-version: '17'
  170. distribution: 'temurin'
  171. - name: Setup Gradle
  172. uses: gradle/actions/setup-gradle@v4
  173. with:
  174. cache-read-only: false
  175. gradle-home-cache-cleanup: true
  176. - name: Make gradlew executable
  177. shell: bash
  178. run: chmod +x gradlew || true
  179. - name: Read app name + version
  180. id: meta
  181. shell: bash
  182. run: |
  183. echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  184. echo "appname=$(grep -E '^app-name' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  185. # Decode the Android release keystore from a base64 secret (optional). Without the
  186. # secret the APK is debug-signed by the build fallback — still installable.
  187. - name: Decode Android keystore
  188. if: matrix.target == 'android'
  189. shell: bash
  190. env:
  191. ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
  192. run: |
  193. if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
  194. echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore"
  195. echo "ANDROID_KEYSTORE_FILE=$RUNNER_TEMP/release.keystore" >> "$GITHUB_ENV"
  196. echo "Android release keystore decoded"
  197. else
  198. echo "::warning::no ANDROID_KEYSTORE_BASE64 secret — APK will be debug-signed"
  199. fi
  200. # ---------- android (.apk) ----------
  201. - name: Build android .apk
  202. if: matrix.target == 'android'
  203. shell: bash
  204. env:
  205. ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
  206. ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
  207. ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
  208. run: ./gradlew :androidApp:assembleRelease -PdepCheck=false --stacktrace
  209. # ---------- windows (.exe) ----------
  210. - name: Build windows .exe
  211. if: matrix.os == 'windows-latest'
  212. shell: bash
  213. run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace
  214. # ---------- macos (.dmg) ----------
  215. # Signing/notarization activate only when the Apple Developer ID secrets are present
  216. # (see desktopApp.gradle.kts macOS block); otherwise the .dmg is built unsigned.
  217. - name: Build macOS .dmg
  218. if: matrix.os == 'macos-latest'
  219. shell: bash
  220. env:
  221. MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
  222. MACOS_BUNDLE_ID: ${{ secrets.MACOS_BUNDLE_ID }}
  223. MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
  224. MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
  225. MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
  226. run: ./gradlew :desktopApp:packageReleaseDmg -PdepCheck=false --stacktrace
  227. # ---------- collect whatever got produced (versioned names) ----------
  228. # Primary source is releases/ (collectReleases + makeIso). Fall back to jpackage output
  229. # dirs for partial builds and for platform-specific runners (exe/dmg).
  230. - name: Collect artifacts
  231. if: always()
  232. shell: bash
  233. run: |
  234. set -euo pipefail
  235. VER='${{ steps.meta.outputs.version }}'
  236. APP='${{ steps.meta.outputs.appname }}'
  237. BASE="$APP-$VER"
  238. PKG=packages/main-release
  239. mkdir -p upload
  240. copy_pkg() {
  241. local dir="$1" ext="$2"
  242. for f in "$dir"/*."$ext"; do
  243. if [ -e "$f" ]; then
  244. cp -v "$f" "upload/$BASE.$ext"
  245. return 0
  246. fi
  247. done
  248. return 0
  249. }
  250. if compgen -G "releases/*" > /dev/null; then
  251. cp -av releases/* upload/
  252. fi
  253. copy_pkg "$PKG/exe" exe
  254. copy_pkg "$PKG/dmg" dmg
  255. if [ ! -f "upload/$BASE.apk" ]; then
  256. for f in androidApp/build/outputs/apk/release/*.apk; do
  257. [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break
  258. done
  259. fi
  260. if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
  261. (cd reports && zip -r "../upload/$BASE-reports.zip" .)
  262. fi
  263. echo "=== produced for ${{ matrix.target }} ==="
  264. ls -la upload/
  265. require_artifact() {
  266. local file="upload/$1"
  267. if [ ! -s "$file" ]; then
  268. echo "::error::required release artifact is missing or empty: $1"
  269. exit 1
  270. fi
  271. }
  272. case '${{ matrix.target }}' in
  273. android)
  274. require_artifact "$BASE.apk"
  275. ;;
  276. windows)
  277. require_artifact "$BASE.exe"
  278. ;;
  279. macos)
  280. require_artifact "$BASE.dmg"
  281. ;;
  282. esac
  283. - name: Upload build artifacts
  284. if: always()
  285. uses: actions/upload-artifact@v4
  286. with:
  287. name: dist-${{ matrix.target }}
  288. path: upload/
  289. if-no-files-found: warn
  290. release:
  291. name: Publish GitHub Release
  292. needs:
  293. - package-linux-iso
  294. - package
  295. if: always()
  296. runs-on: ubuntu-latest
  297. permissions:
  298. contents: write
  299. steps:
  300. - uses: actions/checkout@v4
  301. - name: Read version
  302. id: ver
  303. shell: bash
  304. run: echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
  305. - name: Download all artifacts
  306. uses: actions/download-artifact@v4
  307. with:
  308. path: dist
  309. merge-multiple: true
  310. - name: List collected
  311. shell: bash
  312. run: ls -la dist/ || echo "no artifacts produced"
  313. - name: Publish to GitHub Release
  314. uses: softprops/action-gh-release@v2
  315. with:
  316. tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}', steps.ver.outputs.version) }}
  317. name: mjdev-desktop v${{ steps.ver.outputs.version }}
  318. files: dist/**
  319. fail_on_unmatched_files: false
  320. env:
  321. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}