diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index 1852a2c52..27a76bc99 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -15,7 +15,7 @@ on: type: string push: tags: - - 'ios-v*' + - 'v*' permissions: contents: read @@ -23,7 +23,7 @@ permissions: jobs: build-and-upload: name: Build signed IPA and upload to TestFlight - runs-on: macos-latest + runs-on: macos-26 timeout-minutes: 120 steps: @@ -82,6 +82,14 @@ jobs: run: | echo "Skipping TestFlight upload because required credentials are not configured." + - name: Select Xcode 26 + if: ${{ steps.check_prereqs.outputs.enabled == 'true' }} + run: | + set -euo pipefail + sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer + xcodebuild -version + xcrun --sdk iphoneos --show-sdk-version + - name: Set up Flutter uses: subosito/flutter-action@v2 if: ${{ steps.check_prereqs.outputs.enabled == 'true' }} @@ -154,7 +162,44 @@ jobs: run: | set -euo pipefail + IOS_VERSION="$(tr -d '[:space:]' < ../.sure-version | sed 's/-.*$//')" + IOS_BUILD_NUMBER="$(date -u +%Y%m%d%H%M)" + ARCHIVE_PATH="$RUNNER_TEMP/Runner.xcarchive" + EXPORT_PATH="$PWD/build/ios/ipa" EXPORT_PLIST="$RUNNER_TEMP/ExportOptions.plist" + python3 <<'PY' + import os + import re + from pathlib import Path + + path = Path("ios/Runner.xcodeproj/project.pbxproj") + text = path.read_text() + + team = os.environ["IOS_TEAM_ID"] + profile = os.environ["PROFILE_NAME"] + identity = os.environ["IOS_DISTRIBUTION_CERT_NAME"] + + def patch_block(match): + block = match.group(0) + if "PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile;" not in block: + return block + if "CODE_SIGN_STYLE = Manual;" not in block: + block = block.replace("CURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";", "CURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;") + if '"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution";' not in block: + block = re.sub(r'DEVELOPMENT_TEAM = .*?;', f'"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "{identity}";\n\t\t\t\tDEVELOPMENT_TEAM = {team};', block, count=1) + else: + block = re.sub(r'"CODE_SIGN_IDENTITY\[sdk=iphoneos\*\]" = ".*?";', f'"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "{identity}";', block) + block = re.sub(r'DEVELOPMENT_TEAM = .*?;', f'DEVELOPMENT_TEAM = {team};', block) + if "PROVISIONING_PROFILE_SPECIFIER = " in block: + block = re.sub(r'PROVISIONING_PROFILE_SPECIFIER = .*?;', f'PROVISIONING_PROFILE_SPECIFIER = "{profile}";', block) + else: + block = block.replace(f'DEVELOPMENT_TEAM = {team};', f'DEVELOPMENT_TEAM = {team};\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = "{profile}";') + return block + + text = re.sub(r'isa = XCBuildConfiguration;\n\s+baseConfigurationReference = .*?;\n\s+buildSettings = \{.*?\n\s+name = (?:Debug|Release|Profile);\n\s+\};', patch_block, text, flags=re.S) + path.write_text(text) + PY + cat > "$EXPORT_PLIST" < @@ -181,11 +226,37 @@ jobs: EOF - CODE_SIGN_IDENTITY="${IOS_DISTRIBUTION_CERT_NAME}" \ - CODE_SIGN_STYLE=Manual \ - DEVELOPMENT_TEAM="${IOS_TEAM_ID}" \ - PROVISIONING_PROFILE_SPECIFIER="${PROFILE_NAME}" \ - flutter build ipa --release --export-options-plist="$EXPORT_PLIST" + if [ -z "$IOS_VERSION" ]; then + echo "::error::.sure-version is empty or unreadable" + exit 1 + fi + + echo "Using iOS version: $IOS_VERSION" + echo "Using iOS build number: $IOS_BUILD_NUMBER" + + flutter build ios \ + --release \ + --no-codesign \ + --build-name="$IOS_VERSION" \ + --build-number="$IOS_BUILD_NUMBER" + + xcodebuild \ + -workspace ios/Runner.xcworkspace \ + -scheme Runner \ + -configuration Release \ + -archivePath "$ARCHIVE_PATH" \ + -destination 'generic/platform=iOS' \ + MARKETING_VERSION="$IOS_VERSION" \ + CURRENT_PROJECT_VERSION="$IOS_BUILD_NUMBER" \ + archive + + mkdir -p "$EXPORT_PATH" + + xcodebuild \ + -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportPath "$EXPORT_PATH" \ + -exportOptionsPlist "$EXPORT_PLIST" - name: Prepare TestFlight auth key id: testflight_key @@ -195,8 +266,11 @@ jobs: APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} run: | set -euo pipefail - KEY_FILE="$RUNNER_TEMP/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" + KEY_DIR="$HOME/.appstoreconnect/private_keys" + mkdir -p "$KEY_DIR" + KEY_FILE="$KEY_DIR/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" echo "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > "$KEY_FILE" + chmod 600 "$KEY_FILE" echo "key-file=$KEY_FILE" >> "$GITHUB_OUTPUT" - name: Upload IPA to TestFlight @@ -219,8 +293,7 @@ jobs: --file "$IPA_PATH" \ --type ios \ --apiKey "$APP_STORE_CONNECT_API_KEY_ID" \ - --apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID" \ - --apiPrivateKey "$APP_STORE_CONNECT_API_KEY_FILE" + --apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID" - name: Upload build artifact if: ${{ steps.check_prereqs.outputs.enabled == 'true' }} @@ -232,9 +305,14 @@ jobs: - name: Cleanup signing keychain if: ${{ always() && steps.check_prereqs.outputs.enabled == 'true' }} + env: + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} run: | set -euo pipefail KEYCHAIN_PATH="${{ steps.signing.outputs.keychain-path }}" if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then security delete-keychain "$KEYCHAIN_PATH" fi + + KEY_FILE="$HOME/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" + rm -f "$KEY_FILE" diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index b07959fbb..633a2bc71 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -83,10 +83,10 @@ jobs: testflight: name: Upload iOS to TestFlight - needs: [build, release] + needs: [build, prepare_release] uses: ./.github/workflows/ios-testflight.yml with: - notes: "Mobile release ${{ needs.release.outputs.tag_name }}" + notes: "Mobile release ${{ needs.prepare_release.outputs.tag_name }}" secrets: inherit release: