name: Release # Produces versioned distributables for ALL targets a host can build, into a GitHub Release. # Each target verifies its own required artifacts. Publishing uses every successful target, so # apk/exe are not held hostage by Linux-only packaging dependencies. # linux (deb/rpm/AppImage) + iso -> ubuntu-latest # android (apk) -> ubuntu-latest # windows (exe) -> windows-latest # macos (dmg) -> macos-latest # 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-iso - os: ubuntu-latest target: android - 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.target == 'android' 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 # Compositor build headers/tools are required for :compositor:stageSession -> # packageFullDeb -> collectReleases. Ubuntu 24.04 has wayland-scanner in # libwayland-bin but does not ship wlroots 0.18, so install that ABI-pinned package # from the catalog URL instead of letting apt fail the whole dependency transaction. - name: Install Linux build dependencies if: matrix.target == 'linux-iso' shell: bash run: | set -euo pipefail sudo apt-get update sudo apt-get install -y \ ca-certificates curl rpm debootstrap squashfs-tools xorriso mtools dpkg-dev \ grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring zip \ pkg-config libwayland-dev libwayland-bin wayland-protocols \ libxkbcommon-dev libcairo2-dev libcairo2 libpixman-1-dev libdrm-dev \ libinput-dev libseat-dev libgbm-dev libegl1-mesa-dev libgles2-mesa-dev \ libcap-dev liblcms2-dev libsystemd-dev libudev-dev libvulkan-dev \ libxcb1-dev libxcb-composite0-dev libxcb-dri3-dev libxcb-ewmh-dev \ libxcb-icccm4-dev libxcb-image0-dev libxcb-present-dev libxcb-render0-dev \ libxcb-render-util0-dev libxcb-res0-dev libxcb-shm0-dev libxcb-xfixes0-dev \ libxcb-xinput-dev libx11-xcb-dev read_catalog_url() { grep -E "^$1" gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/' } mkdir -p "$RUNNER_TEMP/wlroots" for key in \ ci-ubuntu-libdisplay-info-runtime-deb-url \ ci-ubuntu-libdisplay-info-dev-deb-url \ ci-ubuntu-libliftoff-runtime-deb-url \ ci-ubuntu-libliftoff-dev-deb-url \ ci-ubuntu-libxcb-errors-runtime-deb-url \ ci-ubuntu-libxcb-errors-dev-deb-url \ ci-ubuntu-wlroots-runtime-deb-url \ ci-ubuntu-wlroots-dev-deb-url do url="$(read_catalog_url "$key")" curl -fsSL "$url" -o "$RUNNER_TEMP/wlroots/${key}.deb" done sudo apt-get install -y "$RUNNER_TEMP"/wlroots/*.deb wayland-scanner --version pkg-config --modversion wlroots-0.18 # ---------- linux + iso + reports (ubuntu) ---------- - name: Build linux + iso + reports if: matrix.target == 'linux-iso' shell: bash run: | ./gradlew :desktopApp:packageReleaseDistributionForCurrentOS packageAppImageFile packageFullDeb makeIso -PdepCheck=false --stacktrace echo "=== releases/ ===" ls -la releases/ # ---------- android (.apk) ---------- - name: Build android .apk if: matrix.target == 'android' shell: bash env: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: ./gradlew :androidApp:assembleRelease -PdepCheck=false --stacktrace # ---------- windows (.exe) ---------- - name: Build windows .exe if: matrix.os == 'windows-latest' shell: bash run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace # ---------- 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 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 # ---------- collect whatever got produced (versioned names) ---------- # Primary source is releases/ (collectReleases + makeIso). Fall back to jpackage output # dirs for partial builds and for platform-specific runners (exe/dmg). - name: Collect artifacts shell: bash run: | set -euo pipefail VER='${{ steps.meta.outputs.version }}' APP='${{ steps.meta.outputs.appname }}' BASE="$APP-$VER" PKG=packages/main-release mkdir -p upload copy_pkg() { local dir="$1" ext="$2" for f in "$dir"/*."$ext"; do if [ -e "$f" ]; then cp -v "$f" "upload/$BASE.$ext" return 0 fi done return 0 } if compgen -G "releases/*" > /dev/null; then cp -av releases/* upload/ fi copy_pkg "$PKG/exe" exe copy_pkg "$PKG/dmg" dmg [ -f "upload/$BASE.deb" ] || copy_pkg "$PKG/deb" deb [ -f "upload/$BASE.rpm" ] || copy_pkg "$PKG/rpm" rpm [ -f "upload/$BASE.AppImage" ] || copy_pkg "$PKG/appimage" AppImage if [ ! -f "upload/$BASE.apk" ]; then for f in androidApp/build/outputs/apk/release/*.apk; do [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break done fi if [ ! -f "upload/$BASE.iso" ]; then for f in releases/*.iso; do [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break done fi if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then (cd reports && zip -r "../upload/$BASE-reports.zip" .) fi echo "=== produced for ${{ matrix.target }} ===" ls -la upload/ require_artifact() { local file="upload/$1" if [ ! -s "$file" ]; then echo "::error::required release artifact is missing or empty: $1" exit 1 fi } case '${{ matrix.target }}' in linux-iso) require_artifact "$BASE.deb" require_artifact "$BASE.rpm" require_artifact "$BASE.AppImage" require_artifact "$BASE.iso" ;; android) require_artifact "$BASE.apk" ;; windows) require_artifact "$BASE.exe" ;; macos) require_artifact "$BASE.dmg" ;; esac - name: Upload build artifacts 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 }}