fix: run TestFlight upload on v* tags (#1582)

* fix(ci): trigger TestFlight upload from v tags

* fix(ci): archive iOS app with manual signing

* fix(ci): avoid applying iOS profile to CocoaPods targets

* fix(ci): apply provisioning profile to Runner target only

* fix(ci): fix Runner signing patch script path

* fix(ci): place App Store auth key where altool expects it

* fix(ci): build TestFlight uploads with Xcode 26.4 on macOS 26

* fix(ci): generate unique iOS build number for TestFlight uploads

* fix(ci): read iOS marketing version from .sure-version

* refactor(ci): remove hardcoded iOS team id anchor

* fix(ci): strip prerelease suffix from iOS marketing version

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
This commit is contained in:
Sure Admin (bot)
2026-04-29 13:26:16 +02:00
committed by GitHub
parent 7c14c80444
commit 9b2c80768c
2 changed files with 90 additions and 12 deletions

View File

@@ -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" <<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">
@@ -181,11 +226,37 @@ jobs:
</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"
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"

View File

@@ -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: