mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
CI: iOS TestFlight build workflow (signed + upload via altool)
Phase 2 of iOS CI. Full signed pipeline that: 1) Decodes secrets: - APPLE_DIST_CERT_P12_BASE64 + password → temp keychain - APPLE_PROVISIONING_PROFILE_BASE64 → ~/Library/MobileDevice/... - APPLE_API_KEY_P8_BASE64 → ~/.appstoreconnect/private_keys/ 2) Parses the .mobileprovision for its UUID + Name (used in ExportOptions). 3) Generates ios/ExportOptions.plist dynamically with the right Team ID + profile name + 'app-store' method. 4) Runs flutter build ipa --release --export-options-plist=... 5) Uploads the .ipa to TestFlight via xcrun altool with the API key. 6) Always cleans up keychain + temp credentials, even on failure. Trigger: workflow_dispatch ONLY (manual). TestFlight uploads consume a build number with every push — we don't want auto-trigger spamming TestFlight. Bump the build number in scripts/ios/app_env.sh before each TestFlight push (or wire a workflow_dispatch input later). All Apple secrets (and the Trocador ones reused from Linux/Android) are already configured in Gitea Actions. If signing fails on first run: most likely the profile name extracted from the .mobileprovision doesn't match what Xcode expects, OR the distribution cert in the .p12 doesn't match what the profile pins. The keychain identity sanity-check + ExportOptions.plist echo in the logs will tell us which.
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
name: Hash Wallet iOS TestFlight build
|
||||
|
||||
# Phase 2 of iOS CI: full signed build that uploads to TestFlight.
|
||||
# Phase 1 (build-ios-sim.yml) validates the Mac runner with no signing —
|
||||
# run that first if TestFlight blows up unexpectedly.
|
||||
#
|
||||
# Required Gitea Actions secrets (all account/app credentials):
|
||||
# APPLE_TEAM_ID — 10-char (D8PL9F7X33)
|
||||
# APPLE_KEY_ID — 10-char (CHK6B69G58)
|
||||
# APPLE_ISSUER_ID — UUID
|
||||
# APPLE_API_KEY_P8_BASE64 — base64 of AuthKey_<KeyID>.p8
|
||||
# APPLE_DIST_CERT_P12_BASE64 — base64 of distribution.p12
|
||||
# APPLE_DIST_CERT_P12_PASSWORD — password set during openssl pkcs12 export
|
||||
# APPLE_PROVISIONING_PROFILE_BASE64 — base64 of Hash_Wallet.mobileprovision
|
||||
# APPLE_KEYCHAIN_PASSWORD — any string, used to create the temp keychain
|
||||
# TROCADOR_* — reused from Linux/Android
|
||||
|
||||
on:
|
||||
# Don't auto-trigger on every push — TestFlight uploads are slow and use
|
||||
# build numbers. Manual trigger only; later we can add a workflow_dispatch
|
||||
# input for build number or auto-bump.
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
APP_IOS_TYPE: cakewallet
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Show toolchain
|
||||
run: |
|
||||
set -x
|
||||
flutter --version
|
||||
xcodebuild -version
|
||||
pod --version
|
||||
uname -m
|
||||
|
||||
# ---- External prebuilt deps -----------------------------------------
|
||||
- name: Fetch prebuilt torch_dart
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts
|
||||
rm -rf torch_dart torch_dart.tar.gz
|
||||
wget -q https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz
|
||||
mkdir torch_dart
|
||||
tar -xzf torch_dart.tar.gz -C torch_dart
|
||||
rm torch_dart.tar.gz
|
||||
popd
|
||||
|
||||
- name: Fetch prebuilt reown_flutter
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts
|
||||
rm -rf reown_flutter reown_flutter.tar.gz
|
||||
wget -q https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz
|
||||
mkdir reown_flutter
|
||||
tar -xzf reown_flutter.tar.gz -C reown_flutter
|
||||
rm reown_flutter.tar.gz
|
||||
popd
|
||||
|
||||
- name: Clone BitBox Flutter
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts
|
||||
./build_bitbox_flutter.sh
|
||||
popd
|
||||
|
||||
- name: Fetch prebuilt monero_c bundle
|
||||
run: |
|
||||
set -x -e
|
||||
./scripts/prepare_moneroc.sh
|
||||
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
|
||||
mkdir -p "scripts/monero_c/release/$MONERO_C_TAG"
|
||||
pushd "scripts/monero_c/release/$MONERO_C_TAG"
|
||||
wget -q https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip
|
||||
unzip -q release-bundle.zip
|
||||
rm release-bundle.zip
|
||||
popd
|
||||
|
||||
# ---- Configure: pubspec.yaml, Info.plist, etc. ----------------------
|
||||
- name: Run iOS configure (cakewallet profile)
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts/ios
|
||||
source ./app_env.sh cakewallet
|
||||
./app_config.sh
|
||||
popd
|
||||
|
||||
# ---- Secrets ---------------------------------------------------------
|
||||
- name: Generate per-module secrets.g.dart files (empty defaults)
|
||||
run: dart run tool/generate_new_secrets.dart
|
||||
|
||||
- name: Inject Trocador affiliate secrets
|
||||
env:
|
||||
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
||||
TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }}
|
||||
TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }}
|
||||
run: |
|
||||
sed -i '' \
|
||||
-e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \
|
||||
-e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \
|
||||
-e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \
|
||||
lib/.secrets.g.dart
|
||||
|
||||
# ---- Apple signing setup --------------------------------------------
|
||||
- name: Create temp keychain + import distribution cert
|
||||
env:
|
||||
APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }}
|
||||
APPLE_DIST_CERT_P12_PASSWORD: ${{ secrets.APPLE_DIST_CERT_P12_PASSWORD }}
|
||||
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
set -e
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
|
||||
# Create + unlock dedicated keychain so we don't touch the user's login keychain
|
||||
security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" # 6 hours
|
||||
security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
# Add to user search list so codesign can find it
|
||||
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
|
||||
# Import the .p12
|
||||
P12="$RUNNER_TEMP/dist.p12"
|
||||
echo "$APPLE_DIST_CERT_P12_BASE64" | base64 -d > "$P12"
|
||||
security import "$P12" -k "$KEYCHAIN_PATH" -P "$APPLE_DIST_CERT_P12_PASSWORD" \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
# Sanity-check: list identities the keychain offers
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
rm -f "$P12"
|
||||
|
||||
- name: Install provisioning profile + parse name/UUID for ExportOptions
|
||||
env:
|
||||
APPLE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
|
||||
run: |
|
||||
set -e
|
||||
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_DIR"
|
||||
PROFILE="$RUNNER_TEMP/Hash_Wallet.mobileprovision"
|
||||
echo "$APPLE_PROVISIONING_PROFILE_BASE64" | base64 -d > "$PROFILE"
|
||||
|
||||
# Parse the .mobileprovision to get its UUID + Name (CMS-decoded plist).
|
||||
PROFILE_UUID=$(security cms -D -i "$PROFILE" | plutil -extract UUID raw -o - -)
|
||||
PROFILE_NAME=$(security cms -D -i "$PROFILE" | plutil -extract Name raw -o - -)
|
||||
echo "Profile UUID: $PROFILE_UUID"
|
||||
echo "Profile name: $PROFILE_NAME"
|
||||
echo "PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV"
|
||||
echo "PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV"
|
||||
|
||||
# Xcode looks profiles up by UUID filename.
|
||||
cp "$PROFILE" "$PROFILES_DIR/$PROFILE_UUID.mobileprovision"
|
||||
ls -la "$PROFILES_DIR" | head -10
|
||||
|
||||
- name: Install App Store Connect API key for altool
|
||||
env:
|
||||
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
|
||||
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p "$HOME/.appstoreconnect/private_keys"
|
||||
echo "$APPLE_API_KEY_P8_BASE64" | base64 -d > "$HOME/.appstoreconnect/private_keys/AuthKey_${APPLE_KEY_ID}.p8"
|
||||
ls -la "$HOME/.appstoreconnect/private_keys/"
|
||||
|
||||
# ---- Flutter init + codegen + pod install ---------------------------
|
||||
- name: Initialize Flutter SDK (iOS precache)
|
||||
run: |
|
||||
flutter --version
|
||||
flutter precache --ios --no-linux --no-android --no-macos --no-windows --no-fuchsia --no-web || true
|
||||
|
||||
- name: Build generated code
|
||||
run: bash model_generator.sh
|
||||
|
||||
- name: Generate localization
|
||||
run: dart run tool/generate_localization.dart
|
||||
|
||||
- name: pod install
|
||||
run: |
|
||||
cd ios
|
||||
pod install --repo-update
|
||||
|
||||
# ---- Generate ExportOptions.plist + build IPA -----------------------
|
||||
- name: Write ExportOptions.plist
|
||||
env:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
cat > ios/ExportOptions.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>${APPLE_TEAM_ID}</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>signingCertificate</key>
|
||||
<string>Apple Distribution</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>com.suchsoftware.hashwallet</key>
|
||||
<string>${PROFILE_NAME}</string>
|
||||
</dict>
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
cat ios/ExportOptions.plist
|
||||
|
||||
- name: Build IPA
|
||||
run: |
|
||||
set -x -e
|
||||
flutter build ipa --release \
|
||||
--dart-define-from-file=env.json \
|
||||
--export-options-plist=ios/ExportOptions.plist
|
||||
ls -la build/ios/ipa/
|
||||
|
||||
- name: Upload IPA artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: hash-wallet-ios-${{ github.sha }}
|
||||
path: build/ios/ipa/*.ipa
|
||||
retention-days: 30
|
||||
|
||||
# ---- Upload to TestFlight -------------------------------------------
|
||||
- name: Upload to TestFlight via altool
|
||||
env:
|
||||
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
|
||||
run: |
|
||||
set -e
|
||||
IPA=$(ls build/ios/ipa/*.ipa | head -1)
|
||||
echo "Uploading $IPA to TestFlight..."
|
||||
xcrun altool --upload-app \
|
||||
--type ios \
|
||||
--file "$IPA" \
|
||||
--apiKey "$APPLE_KEY_ID" \
|
||||
--apiIssuer "$APPLE_ISSUER_ID"
|
||||
|
||||
# ---- Cleanup keychain so we don't leak it across builds -------------
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
env:
|
||||
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
set +e
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
|
||||
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null
|
||||
rm -rf "$HOME/.appstoreconnect/private_keys"
|
||||
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles"/*.mobileprovision
|
||||
true
|
||||
Reference in New Issue
Block a user