From 4779406d6210531960f9ca3a16bce15c9bf26a2e Mon Sep 17 00:00:00 2001 From: robot Date: Sat, 1 Aug 2026 05:25:57 +0000 Subject: [PATCH] ci: implement Android build and test stage - Add .gitea/workflows/ci.yml: lint -> test -> build-android -> build-ios pipeline - Flutter 3.41.x aligned with devcontainer (was 3.27.x) - Java 17 (temurin) with Gradle build cache - flutter precache --android + SDK license acceptance (CI crash fixes) - Conditional release signing via ANDROID_KEYSTORE_* secrets - Pre-build gates (analyze + test) skip expensive build on failure - Conditional whitelabel build script (only when theme.json exists) - APK + AAB artifact upload (90-day retention) - Add .gitea/workflows/release.yml: tag-triggered release pipeline - Lint -> test -> build-android -> build-ios -> publish-release - Gitea Release creation with APK attachment - Fixed Gitea API auth (was leaking placeholder '***' token) - Apple signing support via APPLE_CERTIFICATE_* secrets - Update android/app/build.gradle.kts: conditional release signing - signingConfigs.release block loads from key.properties when present - Falls back to debug signing when key.properties absent - Backward compatible with local dev (no secrets required) --- .gitea/workflows/ci.yml | 210 +++++++++++++++++++++++++++++ .gitea/workflows/release.yml | 252 +++++++++++++++++++++++++++++++++++ android/app/build.gradle.kts | 27 +++- 3 files changed, 486 insertions(+), 3 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..892d929 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,210 @@ +# kryz-go-flutter CI Pipeline +# Triggers on push and pull_request +# Stages: lint -> test -> build (Android + iOS) -> artifacts + +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +# Cancel in-progress runs on new push to same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FLUTTER_VERSION: "3.41.x" + +jobs: + # ── Stage 1: Lint & static analysis ────────────────────────────── + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Analyze Dart code + run: flutter analyze --fatal-infos + + - name: Check formatting + run: dart format --set-exit-if-changed lib/ test/ + + + # ── Stage 2: Unit & widget tests ───────────────────────────────── + test: + name: Test + runs-on: ubuntu-latest + needs: [lint] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Run tests + run: flutter test --reporter=expanded + + - name: Run tests with coverage + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: flutter test --coverage + + - name: Upload coverage report + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/lcov.info + retention-days: 14 + + + # ── Stage 3: Android build ────────────────────────────────────── + build-android: + name: Build Android + runs-on: ubuntu-latest + needs: [test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Pre-cache Android platform artifacts + run: flutter precache --android + + - name: Accept Android SDK licenses + run: | + yes | sdkmanager --licenses || true + + - name: Setup Python (for whitelabel build script) + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install Flutter dependencies + run: flutter pub get + + - name: Analyze (gate before build) + run: flutter analyze --fatal-warnings + + - name: Run tests (gate before build) + run: flutter test --reporter=expanded + + # Optional: run whitelabel build script to regenerate lib/generated/ + # Skip gracefully if theme.json is missing or script fails + - name: Run whitelabel build script + if: hashFiles('theme.json') != '' + run: python3 build_whitelabel.py + + - name: Configure release signing + if: env.ANDROID_KEYSTORE_BASE64 != '' + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASS: ${{ secrets.ANDROID_KEY_PASS }} + run: | + # Decode keystore + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/release.keystore + + # Write key.properties for Gradle to consume + cat > android/key.properties < android/app/release.keystore + cat > android/key.properties < "$CERTIFICATE_PATH" + echo "$APPLE_PROVISION_PROFILE" | base64 -d > "$PROVISION_PATH" + + security create-keychain -p "" "$KEYCHAIN_PATH" + security default-keychain -s "$KEYCHAIN_PATH" + security unlock-keychain -p "" "$KEYCHAIN_PATH" + security set-keychain-settings -t 3600 -l "$KEYCHAIN_PATH" + security import "$CERTIFICATE_PATH" -P "$APPLE_CERTIFICATE_PASS" \ + -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "" "$KEYCHAIN_PATH" + + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp "$PROVISION_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/ + + - name: Build iOS (release) + if: env.APPLE_CERTIFICATE_P12 != '' + run: flutter build ios --release + + - name: Build iOS (release, no-codesign) + if: env.APPLE_CERTIFICATE_P12 == '' + run: flutter build ios --release --no-codesign + + - name: Upload iOS artifact + uses: actions/upload-artifact@v4 + with: + name: release-ios-app + path: build/ios/iphoneos/ + retention-days: 90 + + + # ── Publish Gitea Release ──────────────────────────────────── + publish-release: + name: Publish Release + runs-on: ubuntu-latest + needs: [build-android, build-ios] + steps: + - name: Download Android APK + uses: actions/download-artifact@v4 + with: + name: release-android-apk + + - name: Download Android AAB + uses: actions/download-artifact@v4 + with: + name: release-android-aab + + - name: Create Gitea Release + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + TAG: ${{ github.ref_name }} + GITEA_BASE: https://git.westerntechnologies.duckdns.org + run: | + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"body\": \"Automated release build for $TAG\"}" \ + "$GITEA_BASE/api/v1/repos/kfj001/kryz-go-flutter/releases" + + - name: Upload APK to Release + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + TAG: ${{ github.ref_name }} + GITEA_BASE: https://git.westerntechnologies.duckdns.org + run: | + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/vnd.android.package-archive" \ + --data-binary @app-release.apk \ + "$GITEA_BASE/api/v1/repos/kfj001/kryz-go-flutter/releases/assets?name=app-release.apk&tag_name=$TAG" diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9a4b8cf..ba9c21a 100755 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -12,6 +12,13 @@ dependencies { implementation("androidx.media:media:1.7.0") } +// Load release signing properties if key.properties exists (set by CI pipeline) +val keystorePropertiesFile = rootProject.file("key.properties") +val keystoreProperties = java.util.Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(java.io.FileInputStream(keystorePropertiesFile)) +} + android { namespace = "com.example.kryz_go_flutter" compileSdk = flutter.compileSdkVersion @@ -37,11 +44,25 @@ android { versionName = flutter.versionName } + signingConfigs { + create("release") { + if (keystorePropertiesFile.exists()) { + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + } + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + // Use release signing if key.properties exists, otherwise fall back to debug. + if (keystorePropertiesFile.exists()) { + signingConfig = signingConfigs.getByName("release") + } else { + signingConfig = signingConfigs.getByName("debug") + } } } }