name: Release on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-*' permissions: contents: write jobs: prepare: name: Prepare release metadata runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} is_prerelease: ${{ steps.version.outputs.is_prerelease }} steps: - name: Extract version from tag id: version run: | set -euo pipefail tag="${GITHUB_REF##*/}" version="${tag#v}" if [[ "$version" == *-* ]]; then is_prerelease=true else is_prerelease=false fi { echo "tag=$tag" echo "version=$version" echo "is_prerelease=$is_prerelease" } >>"$GITHUB_OUTPUT" echo "Tag: $tag (version: $version, prerelease: $is_prerelease)" android: name: Android release runs-on: ubuntu-latest needs: prepare timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 1 - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - name: Make gradlew executable run: chmod +x ./gradlew - name: Configure Android release signing env: KEYSTORE_BASE64: ${{ secrets.NUVIO_RELEASE_KEYSTORE_BASE64 }} KEYSTORE_PASSWORD: ${{ secrets.NUVIO_RELEASE_STORE_PASSWORD }} KEY_ALIAS: ${{ secrets.NUVIO_RELEASE_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.NUVIO_RELEASE_KEY_PASSWORD }} run: | set -euo pipefail if [[ -n "${KEYSTORE_BASE64:-}" ]]; then keystore_path="$RUNNER_TEMP/nuvio-release.keystore" printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$keystore_path" store_password="$KEYSTORE_PASSWORD" key_alias="$KEY_ALIAS" key_password="$KEY_PASSWORD" echo "Using release keystore from secrets." else echo "::warning::Release signing secrets not set — generating an ephemeral debug-style keystore so the build can complete. APKs will not be installable over signed production builds." keystore_path="$RUNNER_TEMP/nuvio-fallback.keystore" store_password="android" key_alias="androiddebugkey" key_password="android" keytool -genkeypair -v \ -keystore "$keystore_path" \ -storepass "$store_password" \ -keypass "$key_password" \ -alias "$key_alias" \ -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=Android Debug,O=Android,C=US" fi { echo "" echo "NUVIO_RELEASE_STORE_FILE=$keystore_path" echo "NUVIO_RELEASE_STORE_PASSWORD=$store_password" echo "NUVIO_RELEASE_KEY_ALIAS=$key_alias" echo "NUVIO_RELEASE_KEY_PASSWORD=$key_password" } >> local.properties - name: Build per-ABI release APKs (full + playstore) run: | ./gradlew \ :composeApp:assembleFullRelease \ :composeApp:assemblePlaystoreRelease \ -Pnuvio.splitAbi=true \ --stacktrace - name: Stage and rename APK artifacts env: VERSION: ${{ needs.prepare.outputs.version }} run: | set -euo pipefail mkdir -p artifacts for flavor in full playstore; do for abi in arm64-v8a armeabi-v7a x86_64; do src=$(ls "composeApp/build/outputs/apk/${flavor}/release/composeApp-${flavor}-${abi}-release"*.apk 2>/dev/null | head -n1 || true) if [[ -z "$src" ]]; then echo "::error::Missing APK for ${flavor}/${abi}" ls -la "composeApp/build/outputs/apk/${flavor}/release/" || true exit 1 fi dest="artifacts/Nuvio-${VERSION}-android-${flavor}-${abi}.apk" cp "$src" "$dest" echo " $src -> $dest" done done ls -la artifacts/ - name: Upload Android APK artifacts uses: actions/upload-artifact@v4 with: name: android-apks path: artifacts/*.apk if-no-files-found: error retention-days: 7 ios: name: iOS release runs-on: macos-15 needs: prepare timeout-minutes: 90 env: NUVIO_IOS_DISTRIBUTION: full steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 1 - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - name: Select latest stable Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Make gradlew executable run: chmod +x ./gradlew # The Release Kotlin/Native link of the iOS framework regularly needs # 8–10 GB of heap (Compose + Ktor + Supabase). Override at the user # level so we don't have to raise the project default for local devs. - name: Raise Gradle heap for Kotlin/Native release link run: | set -euo pipefail mkdir -p "$HOME/.gradle" cat >> "$HOME/.gradle/gradle.properties" <<'EOF' org.gradle.jvmargs=-Xmx10g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g kotlin.daemon.jvmargs=-Xmx4g EOF # Make sure no stale daemon is hanging around with the old args. ./gradlew --stop || true - name: Archive iOS app (Release, unsigned) run: | set -euo pipefail xcodebuild \ -project iosApp/iosApp.xcodeproj \ -scheme iosApp \ -configuration Release \ -destination 'generic/platform=iOS' \ -archivePath build/Nuvio.xcarchive \ -skipPackagePluginValidation \ -skipMacroValidation \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY="" \ DEVELOPMENT_TEAM="" \ archive - name: Repackage .app into unsigned .ipa env: VERSION: ${{ needs.prepare.outputs.version }} run: | set -euo pipefail mkdir -p artifacts build/Payload app_path="build/Nuvio.xcarchive/Products/Applications/Nuvio.app" if [[ ! -d "$app_path" ]]; then echo "::error::Built .app not found at $app_path" ls -laR build/Nuvio.xcarchive || true exit 1 fi cp -R "$app_path" build/Payload/ ipa_name="Nuvio-${VERSION}-ios.ipa" ( cd build zip -ry "$ipa_name" Payload >/dev/null ) mv "build/$ipa_name" "artifacts/$ipa_name" ls -la artifacts/ - name: Upload iOS IPA artifact uses: actions/upload-artifact@v4 with: name: ios-ipa path: artifacts/*.ipa if-no-files-found: error retention-days: 7 release: name: Publish GitHub release runs-on: ubuntu-latest needs: [prepare, android, ios] timeout-minutes: 15 steps: - name: Download Android artifacts uses: actions/download-artifact@v4 with: name: android-apks path: release-assets/ - name: Download iOS artifact uses: actions/download-artifact@v4 with: name: ios-ipa path: release-assets/ - name: List release assets run: ls -la release-assets/ - name: Compose release notes env: VERSION: ${{ needs.prepare.outputs.version }} TAG: ${{ needs.prepare.outputs.tag }} REPO: ${{ github.repository }} run: | set -euo pipefail base="https://github.com/${REPO}/releases/download/${TAG}" link() { local file="$1" local label="$2" if [[ -f "release-assets/$file" ]]; then printf '[%s](%s/%s)' "$label" "$base" "$file" else printf '—' fi } { echo "## Nuvio ${VERSION}" echo echo "### Downloads" echo echo "| Architecture | Android (Full) | Android (Play Store) | iOS |" echo "| --- | --- | --- | --- |" printf '| arm64 | %s | %s | %s |\n' \ "$(link "Nuvio-${VERSION}-android-full-arm64-v8a.apk" "APK")" \ "$(link "Nuvio-${VERSION}-android-playstore-arm64-v8a.apk" "APK")" \ "$(link "Nuvio-${VERSION}-ios.ipa" "IPA")" printf '| armeabi-v7a | %s | %s | — |\n' \ "$(link "Nuvio-${VERSION}-android-full-armeabi-v7a.apk" "APK")" \ "$(link "Nuvio-${VERSION}-android-playstore-armeabi-v7a.apk" "APK")" printf '| x86_64 | %s | %s | — |\n' \ "$(link "Nuvio-${VERSION}-android-full-x86_64.apk" "APK")" \ "$(link "Nuvio-${VERSION}-android-playstore-x86_64.apk" "APK")" echo echo "### Notes" echo echo "- **Android (Full)** is the sideload-distribution flavor with the full feature set. **Android (Play Store)** matches the Play Store-distribution flavor." echo "- The iOS .ipa is **unsigned** — install with AltStore, Sideloadly, or another sideloading tool." echo "- Desktop builds (Windows / macOS / Linux) are not yet published from this workflow." } > release-notes.md echo "--- release notes ---" cat release-notes.md - name: Create GitHub release uses: softprops/action-gh-release@v2 with: name: "Nuvio ${{ needs.prepare.outputs.version }}" tag_name: ${{ needs.prepare.outputs.tag }} body_path: release-notes.md prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }} files: | release-assets/*.apk release-assets/*.ipa fail_on_unmatched_files: true