From af050e2b009d67c59ba110f208511e0d485373e8 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:06:40 +0200 Subject: [PATCH] ci: add github actions for build check and release --- .github/workflows/build-check.yml | 99 +++++++++++ .github/workflows/release.yml | 274 ++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 .github/workflows/build-check.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 00000000..55d242b5 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,99 @@ +name: Build check + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +# Cancel superseded runs on the same PR. +concurrency: + group: build-check-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + android: + name: Android (compile) + runs-on: ubuntu-latest + timeout-minutes: 45 + 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: Compile Android (full + playstore, debug) + run: ./gradlew :composeApp:assembleFullDebug :composeApp:assemblePlaystoreDebug --stacktrace + + ios: + name: iOS (compile) + runs-on: macos-15 + timeout-minutes: 60 + 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 + + # Both iOS distributions (appstore / full) swap source dirs and deps, + # so each must be type-checked independently. + - name: Compile shared Kotlin/Native (appstore distribution) + run: ./gradlew :composeApp:compileKotlinIosArm64 :composeApp:compileKotlinIosSimulatorArm64 -Pnuvio.ios.distribution=appstore --stacktrace + + - name: Compile shared Kotlin/Native (full distribution) + run: ./gradlew :composeApp:compileKotlinIosArm64 :composeApp:compileKotlinIosSimulatorArm64 -Pnuvio.ios.distribution=full --stacktrace + + - name: Build iOS app (Debug, simulator destination, unsigned) + run: | + set -euo pipefail + xcodebuild \ + -project iosApp/iosApp.xcodeproj \ + -scheme iosApp \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/ios-derived-pr \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + DEVELOPMENT_TEAM="" \ + build + + # Desktop targets (macOS .dmg, Windows .exe, Linux) are not yet wired up in + # composeApp/build.gradle.kts. Add a `jvm("desktop")` target with the Compose + # Desktop plugin and matching `desktop` matrix jobs here once that lands. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f6dbf5c5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,274 @@ +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 signing if secrets are present + 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 [[ -z "${KEYSTORE_BASE64:-}" ]]; then + echo "::warning::Android release signing secrets are not set — APKs will be unsigned." + exit 0 + fi + keystore_path="$RUNNER_TEMP/nuvio-release.keystore" + printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$keystore_path" + { + echo "" + echo "NUVIO_RELEASE_STORE_FILE=$keystore_path" + echo "NUVIO_RELEASE_STORE_PASSWORD=$KEYSTORE_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 + + - 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