iOS build fixes/prep for TestFlight

This commit is contained in:
Juan José Mata
2026-02-18 10:11:16 +01:00
parent 3b0b2f7ada
commit 65f1daa995
7 changed files with 250 additions and 4 deletions

172
.github/workflows/ios-testflight.yml vendored Normal file
View File

@@ -0,0 +1,172 @@
name: iOS TestFlight
on:
workflow_dispatch:
inputs:
notes:
description: "TestFlight release notes"
required: false
type: string
push:
tags:
- 'ios-v*'
permissions:
contents: read
jobs:
build-and-upload:
name: Build signed IPA and upload to TestFlight
runs-on: macos-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.32.4'
channel: 'stable'
cache: true
- name: Install Flutter dependencies
working-directory: mobile
run: flutter pub get
- name: Copy app icon source
run: cp public/android-chrome-512x512.png mobile/assets/icon/app_icon.png
- name: Generate app icons
working-directory: mobile
run: flutter pub run flutter_launcher_icons
- name: Install CocoaPods
working-directory: mobile/ios
run: pod install
- name: Configure iOS signing assets
id: signing
env:
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
DIST_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_P12_BASE64 }}
DIST_P12_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_P12_PASSWORD }}
PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/ios-signing.keychain-db"
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 3600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"
CERT_PATH="$RUNNER_TEMP/distribution.p12"
echo "$DIST_P12_BASE64" | base64 --decode > "$CERT_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$DIST_P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
if [ -n "$PROFILE_BASE64" ] && [ -n "$PROFILE_NAME" ]; then
mkdir -p "$PROFILE_DIR"
PROFILE_PATH="$PROFILE_DIR/${PROFILE_NAME}.mobileprovision"
echo "$PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH"
fi
rm -f "$CERT_PATH"
echo "keychain-path=$KEYCHAIN_PATH" >> "$GITHUB_OUTPUT"
- name: Build signed IPA
working-directory: mobile
env:
APP_BUNDLE_ID: am.sure.mobile
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
IOS_DISTRIBUTION_CERT_NAME: ${{ secrets.IOS_DISTRIBUTION_CERT_NAME }}
run: |
set -euo pipefail
EXPORT_PLIST="$RUNNER_TEMP/ExportOptions.plist"
cat > "$EXPORT_PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>${IOS_TEAM_ID}</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>${APP_BUNDLE_ID}</key>
<string>${PROFILE_NAME}</string>
</dict>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
</dict>
</plist>
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"
- name: Prepare TestFlight auth key
id: testflight_key
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
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"
echo "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > "$KEY_FILE"
echo "key-file=$KEY_FILE" >> "$GITHUB_OUTPUT"
- name: Upload IPA to TestFlight
working-directory: mobile
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_FILE: ${{ steps.testflight_key.outputs.key-file }}
run: |
set -euo pipefail
IPA_PATH="$(find build/ios/ipa -name '*.ipa' | head -n 1)"
if [ -z "$IPA_PATH" ]; then
echo "::error::No IPA found at build/ios/ipa"
exit 1
fi
xcrun altool \
--upload-app \
--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"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ios-ipa-testflight
path: mobile/build/ios/ipa/*.ipa
if-no-files-found: error
- name: Cleanup signing keychain
if: always()
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

View File

@@ -151,6 +151,9 @@ The app includes automated CI/CD via GitHub Actions (`.github/workflows/flutter-
- **Android Build**: Generates release APK and AAB artifacts - **Android Build**: Generates release APK and AAB artifacts
- **iOS Build**: Generates iOS release build (unsigned) - **iOS Build**: Generates iOS release build (unsigned)
- **Quality Checks**: Code analysis and tests run before building - **Quality Checks**: Code analysis and tests run before building
- **TestFlight**: Use `.github/workflows/ios-testflight.yml` for signed distribution uploads to App Store Connect
See [mobile/docs/iOS_TESTFLIGHT](mobile/docs/iOS_TESTFLIGHT.md) for required secrets and setup.
### Downloading Build Artifacts ### Downloading Build Artifacts

View File

@@ -0,0 +1,47 @@
# iOS TestFlight GitHub Action
This repository now includes `.github/workflows/ios-testflight.yml`, which builds a **signed** iOS IPA and uploads it to TestFlight.
## What must be in the repo
- The workflow file itself: `.github/workflows/ios-testflight.yml`
- Existing app signing identifiers in Xcode:
- Bundle ID `am.sure.mobile` (or your custom value in the workflow)
- Flutter assets and source already in `mobile/`
## Required GitHub Secrets
Set these in **Settings → Secrets and variables → Actions**:
- `APP_STORE_CONNECT_API_KEY_ID`
- `APP_STORE_CONNECT_API_ISSUER_ID`
- `APP_STORE_CONNECT_API_KEY_BASE64`
- Base64 version of the `.p8` private key content
- `IOS_TEAM_ID`
- `IOS_KEYCHAIN_PASSWORD`
- `IOS_DISTRIBUTION_P12_BASE64`
- `IOS_DISTRIBUTION_P12_PASSWORD`
- `IOS_DISTRIBUTION_CERT_NAME`
- Usually `iPhone Distribution`
- `IOS_PROVISIONING_PROFILE_NAME`
- `IOS_PROVISIONING_PROFILE_BASE64`
> Do **not** commit private keys, `.p12` files, or `.p8` files to the repository.
## Triggering
- Run manually: **Actions → iOS TestFlight → Run workflow**
- Push a tag that matches `ios-v*` (for example `ios-v1.2.3`)
## Recommended App Store Connect setup
- Use an API Key with App Manager or Developer role
- Create/download an iOS Distribution certificate (`.p12`), convert to base64 for `IOS_DISTRIBUTION_P12_BASE64`
- Create and distribute an **App Store** provisioning profile for `am.sure.mobile`
- Base64-encode the `.mobileprovision` file for `IOS_PROVISIONING_PROFILE_BASE64`
## Output
- IPA artifact: `ios-ipa-testflight`
- Uploaded IPA to TestFlight on successful upload step

View File

@@ -1,2 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"

View File

@@ -1,2 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View File

@@ -37,6 +37,7 @@
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8D7D5C9B2EAF8B6A00F0A1B3 /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Flutter/Profile.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -68,7 +69,6 @@
FE30D351EF409EF61606DE07 /* Pods-Runner.release.xcconfig */, FE30D351EF409EF61606DE07 /* Pods-Runner.release.xcconfig */,
1DC166D4710E23F2EDEDA52D /* Pods-Runner.profile.xcconfig */, 1DC166D4710E23F2EDEDA52D /* Pods-Runner.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -86,6 +86,7 @@
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
8D7D5C9B2EAF8B6A00F0A1B3 /* Profile.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */,
); );
name = Flutter; name = Flutter;
@@ -357,21 +358,28 @@
}; };
249021D4217E4FDB00AE95B9 /* Profile */ = { 249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 8D7D5C9B2EAF8B6A00F0A1B3 /* Profile.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = C8WT3YC922;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Sure Finances";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile; PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Profile; name = Profile;
@@ -494,17 +502,24 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = C8WT3YC922;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Sure Finances";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile; PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
@@ -516,16 +531,23 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = C8WT3YC922;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Sure Finances";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile; PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; name = Release;