name: Release # 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: push: branches: - main 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: read jobs: package: name: Package (${{ matrix.target }}) strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: linux-android-iso - os: windows-latest target: windows - os: macos-latest target: macos runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Make gradlew executable shell: bash run: chmod +x gradlew || true - 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" echo "appname=$(grep -E '^app-name' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT" # 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" ./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 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 -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 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.meta.outputs.version }}' APP='${{ steps.meta.outputs.appname }}' BASE="$APP-$VER" PKG=packages/main-release mkdir -p upload copy() { # 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/$BASE-reports.zip" .) || echo "::warning::reports zip failed" fi echo "=== produced for ${{ matrix.target }} ==="; ls -la upload/ || true - name: Upload build artifacts if: always() uses: actions/upload-artifact@v4 with: name: dist-${{ matrix.target }} path: upload/ if-no-files-found: warn release: name: Publish GitHub Release needs: package if: always() runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: Read version id: ver shell: bash run: echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT" - name: Download all artifacts uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: List collected shell: bash run: ls -la dist/ || echo "no artifacts produced" - name: Publish to GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}', steps.ver.outputs.version) }} name: mjdev-desktop v${{ steps.ver.outputs.version }} files: dist/** fail_on_unmatched_files: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}