diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e77df25..93c2d12a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,14 +3,20 @@ on: push: branches: [main] jobs: - test: - name: Check and test on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + # Linux runner: analyze + test once (both are OS-independent) and compile a + # release APK to catch release-only breakage. Debug-signed via the build.gradle + # fallback (ALLOW_DEBUG_SIGNED_RELEASE) — no signing secrets needed. + android: + name: Analyze, test, and build Android + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + # Match the JDK the release workflows build with, so this compile check + # faithfully catches what would break a real release. + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 - uses: subosito/flutter-action@v2 with: flutter-version: 3.44.1 @@ -19,3 +25,19 @@ jobs: - run: flutter pub get - run: flutter analyze - run: flutter test + - run: flutter build apk --release + env: + ALLOW_DEBUG_SIGNED_RELEASE: '1' + # macOS runner: release-compile iOS (no codesign, no secrets) — the only thing + # that must run on macOS. analyze/test already covered on the Linux job. + ios: + name: Build iOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.1 + cache: true + - run: flutter pub get + - run: flutter build ios --release --no-codesign diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 132c632e..2b632d17 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,6 +8,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + # Match the JDK the release workflows build with, so the release-mode APK + # check below faithfully catches what would break a real release. + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 - uses: subosito/flutter-action@v2 with: flutter-version: 3.44.1 @@ -16,3 +22,10 @@ jobs: - run: flutter pub get - run: flutter analyze - run: flutter test + # Release-mode compile check (no signing secrets needed — Android falls back + # to debug signing via ALLOW_DEBUG_SIGNED_RELEASE). Catches release-only + # breakage that debug builds and tests miss (AOT compile, icon tree-shaking, + # plugin/Gradle release incompatibility). + - run: flutter build apk --release + env: + ALLOW_DEBUG_SIGNED_RELEASE: '1' diff --git a/.github/workflows/release-dryrun.yml b/.github/workflows/release-dryrun.yml new file mode 100644 index 00000000..7941f249 --- /dev/null +++ b/.github/workflows/release-dryrun.yml @@ -0,0 +1,118 @@ +# Dry-runs the full store release pipeline without publishing, to catch +# credential, signing, dependency, and store-validation breakage before an +# actual release. Mirrors store-release.yml but stops short of publishing: +# Android validates the AAB against Google Play (validate_only); iOS builds a +# fully signed archive via match (readonly) and skips the App Store upload. +name: Release dry-run +on: + # Run on demand before cutting a release. + workflow_dispatch: + # Weekly heartbeat so cert expiry, secret rotation, and dependency drift + # surface on their own between releases. + schedule: + - cron: '0 9 * * 1' # Mondays 09:00 UTC +# Setup environment variables for use in all jobs. +env: + # Gemfile path used for caching dependencies. + BUNDLE_GEMFILE: ${{ github.workspace }}/android/Gemfile + CLIENT_ID: ${{ secrets.CLIENT_ID }} + SECRET: ${{ secrets.SECRET }} + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 +jobs: + android: + name: Dry-run Android release + runs-on: ubuntu-latest + environment: release-android + env: + UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} + STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + KEY_FILE_PATH: android/key.properties + PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_CREDENTIALS }} + + steps: + - name: Checkout Frosty repo + uses: actions/checkout@v6 + + # Install Ruby and install/cache dependencies with bundler in order to use fastlane. + - name: Set up Ruby and Bundler + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + + # Setup Java so that we can build the appbundle. + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + # Setup and cache Flutter to install packages and build. + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.1 + cache: true + + # Decode and create the upload-keystore.jks file, used for signing the app. + - name: Decode and create upload-keystore.jks + run: echo $UPLOAD_KEYSTORE | base64 --decode > $GITHUB_WORKSPACE/android/upload-keystore.jks + + # Create the key.properties file, used for signing the app. + - name: Create key.properties + run: | + echo storePassword=$STORE_PASSWORD >> $KEY_FILE_PATH + echo keyPassword=$KEY_PASSWORD >> $KEY_FILE_PATH + echo keyAlias=upload >> $KEY_FILE_PATH + echo storeFile=$GITHUB_WORKSPACE/android/upload-keystore.jks >> $KEY_FILE_PATH + + # Build the Android app bundle (AAB) and pass the environment variables. + - name: Build the Android AAB + run: flutter build appbundle --dart-define CLIENT_ID=$CLIENT_ID --dart-define SECRET=$SECRET + + # Validate the AAB and credentials against Google Play without publishing. + - name: Validate release with fastlane (no publish) + run: cd android && bundle exec fastlane deploy dry_run:true + ios: + name: Dry-run iOS release + runs-on: macos-latest + environment: release-ios + env: + APP_STORE_KEY_ID: ${{ secrets.APP_STORE_KEY_ID }} + APP_STORE_ISSUER_ID: ${{ secrets.APP_STORE_ISSUER_ID }} + APP_STORE_CREDENTIALS: ${{ secrets.APP_STORE_CREDENTIALS }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + steps: + - name: Checkout Frosty repo + uses: actions/checkout@v6 + + # Always pick the newest production Xcode on the runner + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + # Install Ruby and install/cache dependencies with bundler in order to use fastlane. + - name: Set up Ruby and Bundler + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + + # Setup and cache Flutter to install packages and build. + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.1 + cache: true + + # Setup the iOS project with environment variables without signing (building and signing will happen in fastlane). + - name: Config the iOS build + run: flutter build ios --no-codesign --config-only --dart-define CLIENT_ID=$CLIENT_ID --dart-define SECRET=$SECRET + + # Authenticate ASC API key, resolve signing via match (readonly), and build a + # fully signed archive without uploading to the App Store. + - name: Sign and build with fastlane (no publish) + run: cd ios && bundle exec fastlane release dry_run:true diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 0390c22d..d2392565 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -17,11 +17,14 @@ default_platform(:android) platform :android do desc "Deploy a new version to the Google Play" - lane :deploy do + lane :deploy do |options| # Upload to the play store ussing the provided credentials and AAB from flutter build. + # With dry_run:true (release dry-run workflow), validate_only authenticates and + # validates the AAB against Google Play without committing the release. upload_to_play_store( json_key_data: Base64.decode64(ENV["PLAY_STORE_CREDENTIALS"]), - aab: "../build/app/outputs/bundle/release/app-release.aab" + aab: "../build/app/outputs/bundle/release/app-release.aab", + validate_only: options[:dry_run] || false ) end end diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 78bcb074..df241cb0 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -17,7 +17,9 @@ default_platform(:ios) platform :ios do desc 'Push a new release build to the App Store' - lane :release do + lane :release do |options| + dry_run = options[:dry_run] || false + # Setup for CI. Uses a keychain to prevent the CI from hanging due to password prompts. setup_ci @@ -29,8 +31,9 @@ platform :ios do is_key_content_base64: true ) - # Manage code signing (with match). - sync_code_signing + # Manage code signing (with match). readonly on a dry run so we only verify the + # existing cert/profile resolve and never regenerate or mutate the certificates repo. + sync_code_signing(readonly: dry_run) # Automatic signing is used during development, but fastlane needs to use manual signing. update_code_signing_settings( @@ -45,6 +48,11 @@ platform :ios do output_directory: '../build/ios/archive' ) + # On a dry run, stop here: the signed archive above exercises every step that + # actually fails (ASC key, match certs/profiles, signing, release build). The + # App Store upload itself is the only remaining unverified step. + next if dry_run + # Upload the app to the App Store. upload_to_app_store( submit_for_review: true,