name: Build and Release on: release: types: [published] permissions: contents: write jobs: get-version: name: Get Version and Release Info runs-on: ubuntu-latest outputs: version: ${{ steps.set-version.outputs.version }} is_prerelease: ${{ steps.set-info.outputs.is_prerelease }} should_upload_to_stores: ${{ steps.set-info.outputs.should_upload_to_stores }} steps: - name: Checkout repository uses: actions/checkout@v5 - name: Get version from pubspec.yaml id: set-version run: | echo version=$(grep "version:" pubspec.yaml | head -1 | cut -d ':' -f 2 | tr -d ' ' | cut -d '+' -f 1) >> $GITHUB_OUTPUT - name: Determine release actions id: set-info run: | echo "is_prerelease=${{ github.event.release.prerelease }}" >> $GITHUB_OUTPUT if [ "${{ github.event.release.prerelease }}" = "true" ]; then echo "should_upload_to_stores=false" >> $GITHUB_OUTPUT echo "✅ Pre-release - will build and attach assets, no store uploads" else echo "should_upload_to_stores=true" >> $GITHUB_OUTPUT echo "✅ Full release - will build, attach assets, and upload to stores" fi build-android-apk: name: Build Android APK needs: get-version runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Install dependencies run: flutter pub get - name: Validate localizations run: dart run scripts/validate_localizations.dart - name: Generate icons and splash screens run: | dart run flutter_launcher_icons dart run flutter_native_splash:create - name: Decode Keystore run: | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks - name: Create key.properties run: | echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties echo "keyAlias=${{ vars.KEY_ALIAS }}" >> android/key.properties echo "storeFile=keystore.jks" >> android/key.properties - name: Build Android .apk run: flutter build apk --release --dart-define=OSM_PROD_CLIENTID='${{ secrets.OSM_PROD_CLIENTID }}' --dart-define=OSM_SANDBOX_CLIENTID='${{ secrets.OSM_SANDBOX_CLIENTID }}' - name: Upload .apk artifact uses: actions/upload-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.apk path: build/app/outputs/flutter-apk/app-release.apk if-no-files-found: 'error' build-android-aab: name: Build Android AAB needs: get-version runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Install dependencies run: flutter pub get - name: Validate localizations run: dart run scripts/validate_localizations.dart - name: Generate icons and splash screens run: | dart run flutter_launcher_icons dart run flutter_native_splash:create - name: Decode Keystore run: | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks - name: Create key.properties run: | echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties echo "keyAlias=${{ vars.KEY_ALIAS }}" >> android/key.properties echo "storeFile=keystore.jks" >> android/key.properties - name: Build Android appBundle run: flutter build appbundle --dart-define=OSM_PROD_CLIENTID='${{ secrets.OSM_PROD_CLIENTID }}' --dart-define=OSM_SANDBOX_CLIENTID='${{ secrets.OSM_SANDBOX_CLIENTID }}' - name: Upload .aab artifact uses: actions/upload-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.aab path: build/app/outputs/bundle/release/app-release.aab if-no-files-found: 'error' build-ios: name: Build iOS needs: get-version runs-on: macos-latest steps: - name: Checkout repository uses: actions/checkout@v5 - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Install dependencies run: flutter pub get - name: Validate localizations run: dart run scripts/validate_localizations.dart - name: Generate icons and splash screens run: | dart run flutter_launcher_icons dart run flutter_native_splash:create - name: Install Apple certificate and provisioning profile env: BUILD_CERTIFICATE_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_BASE64 }} P12_PASSWORD: "" BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_APPSTORE_PROVISIONING_PROFILE_BASE64 }} KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }} run: | # create variables CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db # import certificate and provisioning profile from secrets echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH # create temporary keychain security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH # import certificate to keychain security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH # Set this keychain as the default security list-keychain -d user -s $KEYCHAIN_PATH security default-keychain -s $KEYCHAIN_PATH # install provisioning profile mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/61f9fdb9-bf2d-4d94-b249-63155ee71e74.mobileprovision # Also install using the profile's internal UUID for better compatibility UUID=$(security cms -D -i $PP_PATH | plutil -extract UUID xml1 -o - - | xmllint --xpath "//string/text()" -) cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision # Debug: Check what we actually have echo "=== Certificates in keychain ===" security find-identity -v -p codesigning $KEYCHAIN_PATH echo "=== Provisioning profiles ===" ls -la ~/Library/MobileDevice/Provisioning\ Profiles/ echo "=== Profile UUID extracted: $UUID ===" - name: Create export options run: | cat > ios/exportOptions.plist << EOF destination export method app-store teamID 7XG8T28436 provisioningProfiles me.deflock.deflockapp 61f9fdb9-bf2d-4d94-b249-63155ee71e74 signingStyle manual stripSwiftSymbols EOF - name: Build iOS .ipa run: | flutter build ipa --release \ --export-options-plist=ios/exportOptions.plist \ --dart-define=OSM_PROD_CLIENTID='${{ secrets.OSM_PROD_CLIENTID }}' \ --dart-define=OSM_SANDBOX_CLIENTID='${{ secrets.OSM_SANDBOX_CLIENTID }}' cp build/ios/ipa/*.ipa Runner.ipa - name: Clean up keychain and provisioning profile run: | security delete-keychain $RUNNER_TEMP/app-signing.keychain-db rm ~/Library/MobileDevice/Provisioning\ Profiles/61f9fdb9-bf2d-4d94-b249-63155ee71e74.mobileprovision - name: Upload IPA artifact uses: actions/upload-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.ipa path: Runner.ipa if-no-files-found: 'error' attach-to-release: name: Attach Assets to Release needs: [get-version, build-android-apk, build-android-aab, build-ios] runs-on: ubuntu-latest steps: - name: Download APK artifact uses: actions/download-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.apk - name: Download AAB artifact uses: actions/download-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.aab - name: Download IPA artifact uses: actions/download-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.ipa - name: Rename files for release run: | mv app-release.apk deflock_v${{ needs.get-version.outputs.version }}.apk mv app-release.aab deflock_v${{ needs.get-version.outputs.version }}.aab mv Runner.ipa deflock_v${{ needs.get-version.outputs.version }}.ipa - name: Attach assets to release uses: softprops/action-gh-release@v2 with: files: | deflock_v${{ needs.get-version.outputs.version }}.apk deflock_v${{ needs.get-version.outputs.version }}.aab deflock_v${{ needs.get-version.outputs.version }}.ipa upload-to-stores: name: Upload to App Stores needs: [get-version, build-android-aab, build-ios] runs-on: macos-latest # Need macOS for iOS uploads if: needs.get-version.outputs.should_upload_to_stores == 'true' steps: - name: Download AAB artifact for Google Play uses: actions/download-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.aab - name: Download IPA artifact for App Store uses: actions/download-artifact@v4 with: name: deflock_v${{ needs.get-version.outputs.version }}.ipa # Temporarily disabled - uncomment when Google Play service account is ready # - name: Upload to Google Play Store # uses: r0adkll/upload-google-play@v1 # with: # serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} # packageName: me.deflock.deflockapp # releaseFiles: app-release.aab # track: internal # Uploads to Internal Testing track for review before production # status: completed # inAppUpdatePriority: 0 - name: Upload to App Store Connect env: APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} run: | # Create the private keys directory and decode API key mkdir -p ~/private_keys echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > ~/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8 # Upload to App Store Connect / TestFlight xcrun altool --upload-app \ --type ios \ --file Runner.ipa \ --apiKey $APP_STORE_CONNECT_API_KEY_ID \ --apiIssuer $APP_STORE_CONNECT_ISSUER_ID # Clean up sensitive files rm -rf ~/private_keys - name: Clean up artifacts run: | rm -f app-release.aab Runner.ipa