Files
hash-wallet/.github/workflows/build-ios-testflight.yml
T
jwinterm 5a43cc1114 iOS TestFlight: re-export .p12 as legacy PKCS12 before security import
macOS Keychain Access on Sonoma+ exports .p12 with modern crypto
(AES-256-GCM + PBKDF2 + SHA-256). The `security import` binary on
some macOS releases — including ours apparently — rejects it with:
  SecKeychainItemImport: Unknown format in import.

The openssl decode itself was fine (now using openssl base64 -d -A),
but security can't import the resulting PKCS12 envelope.

Pipe the .p12 through openssl: unpack with the user's password, then
re-export using legacy PBE-SHA1-3DES for both certs and keys, plus
SHA1 MAC. The resulting .p12 uses 90s-era crypto for the OUTER
envelope, which is what security import wants — the actual signing
keys + cert inside are unchanged. No security implication; the legacy
format encryption only matters for the few seconds the .p12 lives in
$RUNNER_TEMP before being imported and deleted.

Also added diagnostic output (file size, magic bytes, file(1)) so the
next failure mode (if any) is easier to characterize.
2026-06-01 19:17:04 -04:00

442 lines
18 KiB
YAML

name: Hash Bags 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:
concurrency:
group: ios-testflight-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
build:
runs-on: macos-latest
env:
APP_IOS_TYPE: cakewallet
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# CocoaPods needs UTF-8; Gitea runner shell defaults to ASCII-8BIT.
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
# See note in build-ios-sim.yml — fallback if /etc/hosts trick
# isn't in place. Safe no-op once it is.
NODE_TLS_REJECT_UNAUTHORIZED: '0'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Check Flutter 3.32.0
id: flutter_check
run: |
if command -v flutter >/dev/null && flutter --version 2>/dev/null | grep -q "3\.32\.0"; then
echo "installed=true" >> "$GITHUB_OUTPUT"
flutter --version
else
echo "installed=false" >> "$GITHUB_OUTPUT"
fi
- name: Install Flutter 3.32.0 (if missing)
if: steps.flutter_check.outputs.installed != 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: '3.32.0'
channel: stable
cache: true
- name: Install CocoaPods (if missing)
run: |
if command -v pod >/dev/null; then
pod --version
exit 0
fi
if command -v brew >/dev/null; then
brew install cocoapods
else
export GEM_HOME="$HOME/.gem"
export PATH="$GEM_HOME/bin:$PATH"
echo "GEM_HOME=$HOME/.gem" >> "$GITHUB_ENV"
echo "$HOME/.gem/bin" >> "$GITHUB_PATH"
gem install --user-install cocoapods --no-document
fi
pod --version
- name: Install Rust + iOS targets (if missing)
run: |
if ! command -v rustup >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --no-modify-path
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.cargo/bin:$PATH"
fi
rm -rf "$HOME/.rustup/downloads"/*.partial 2>/dev/null || true
rustup target add aarch64-apple-ios
rustup target add aarch64-apple-ios-sim
rustup target add x86_64-apple-ios
rustup show
- name: Install Go + gomobile (if missing)
run: |
if ! command -v go >/dev/null; then
if command -v brew >/dev/null; then
brew install go
else
echo "Go missing and brew not available"; exit 1
fi
fi
go version
GOPATH=$(go env GOPATH)
echo "$GOPATH/bin" >> "$GITHUB_PATH"
export PATH="$PATH:$GOPATH/bin"
if ! command -v gomobile >/dev/null; then
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
fi
which gomobile && gomobile version || true
- name: Show toolchain
run: |
set -x
flutter --version
xcodebuild -version
pod --version
uname -m
# ---- External prebuilt deps -----------------------------------------
# macOS doesn't ship wget; using curl everywhere.
- name: Fetch prebuilt torch_dart
run: |
set -x -e
pushd scripts
rm -rf torch_dart torch_dart.tar.gz
curl -fsSL -o torch_dart.tar.gz https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.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
curl -fsSL -o reown_flutter.tar.gz https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz
mkdir reown_flutter
tar -xzf reown_flutter.tar.gz -C reown_flutter
rm reown_flutter.tar.gz
popd
- name: Clone BitBox Flutter (iOS — skip Android bindings)
run: |
# See note in build-ios-sim.yml — iOS uses bitbox's native plugin,
# not the .aar that build_bindings.sh generates.
set -x -e
pushd scripts
if [[ ! -d bitbox_flutter ]]; then
git clone https://github.com/konstantinullrich/bitbox_flutter
fi
cd bitbox_flutter
git fetch -a
git reset --hard
git checkout 5a6e6dd388ef64003f86094af80d5453518b601d
git reset --hard
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"
curl -fsSL -O 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
- name: Build MoneroWallet + WowneroWallet XCFrameworks
run: |
set -x -e
pushd scripts/ios
bash ./gen_framework.sh
popd
ls -la ios/MoneroWallet.xcframework ios/WowneroWallet.xcframework
# ---- 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: Sanity-check Apple secrets are reaching the runner
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }}
APPLE_DIST_CERT_P12_PASSWORD: ${{ secrets.APPLE_DIST_CERT_P12_PASSWORD }}
APPLE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
run: |
# Print LENGTHS only — never the secret values. Length 0 means the
# secret isn't actually reaching the runner (most often: secret is
# set on a different Gitea repo than this workflow is running in).
set +e
echo "APPLE_TEAM_ID length: ${#APPLE_TEAM_ID}"
echo "APPLE_KEY_ID length: ${#APPLE_KEY_ID}"
echo "APPLE_ISSUER_ID length: ${#APPLE_ISSUER_ID}"
echo "APPLE_API_KEY_P8_BASE64 length: ${#APPLE_API_KEY_P8_BASE64}"
echo "APPLE_DIST_CERT_P12_BASE64 length: ${#APPLE_DIST_CERT_P12_BASE64}"
echo "APPLE_DIST_CERT_P12_PASSWORD length: ${#APPLE_DIST_CERT_P12_PASSWORD}"
echo "APPLE_PROVISIONING_PROFILE_BASE64 length: ${#APPLE_PROVISIONING_PROFILE_BASE64}"
echo "APPLE_KEYCHAIN_PASSWORD length: ${#APPLE_KEYCHAIN_PASSWORD}"
set -e
# Fail fast on any zero-length secret so we don't waste time on the
# subsequent steps.
for n in TEAM_ID KEY_ID ISSUER_ID API_KEY_P8_BASE64 DIST_CERT_P12_BASE64 DIST_CERT_P12_PASSWORD PROVISIONING_PROFILE_BASE64 KEYCHAIN_PASSWORD; do
var="APPLE_$n"
if [[ -z "${!var}" ]]; then
echo "FATAL: $var is empty — set it as a Gitea repo secret on the repo this workflow runs in (Builds/hash-wallet)."
exit 1
fi
done
- 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')
# Decode the user-uploaded .p12 to disk.
P12_MODERN="$RUNNER_TEMP/dist-modern.p12"
echo "$APPLE_DIST_CERT_P12_BASE64" | openssl base64 -d -A > "$P12_MODERN"
echo "=== uploaded .p12 file ==="
ls -la "$P12_MODERN"
xxd "$P12_MODERN" | head -1
file "$P12_MODERN" || true
# macOS Keychain Access in Sonoma+ exports .p12 with modern crypto
# (AES-256-GCM + PBKDF2 + SHA-256) that the `security import`
# binary on some macOS releases rejects with
# "SecKeychainItemImport: Unknown format in import." Re-export
# the PKCS12 through openssl using legacy PBE-SHA1-3DES so the
# macOS security command can definitely parse it.
P12="$RUNNER_TEMP/dist-legacy.p12"
echo "=== converting to legacy PKCS12 format ==="
openssl pkcs12 -in "$P12_MODERN" -nodes \
-password pass:"$APPLE_DIST_CERT_P12_PASSWORD" \
| openssl pkcs12 -export \
-macalg sha1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \
-password pass:"$APPLE_DIST_CERT_P12_PASSWORD" \
-out "$P12"
ls -la "$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_MODERN" "$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" | openssl base64 -d -A > "$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" | openssl base64 -d -A > "$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: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
- name: pod install
run: |
cd ios
pod install --repo-update
# ---- Override signing in Release.xcconfig for the archive step ------
# The committed project.pbxproj Release config uses Automatic signing
# + "Apple Development" identity (set up for local dev with Xcode
# signed into an Apple ID). CI can't do Automatic signing — it has
# only the cert + provisioning profile we installed in the temp
# keychain. Append manual-signing overrides to Release.xcconfig;
# xcconfig values take precedence over pbxproj defaults.
- name: Override signing settings for archive
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -x
cat >> ios/Flutter/Release.xcconfig <<XCFG
// CI-only signing overrides (appended at build time, not committed).
CODE_SIGN_IDENTITY = Apple Distribution
CODE_SIGN_STYLE = Manual
DEVELOPMENT_TEAM = ${APPLE_TEAM_ID}
PROVISIONING_PROFILE_SPECIFIER = ${PROFILE_NAME}
XCFG
tail -8 ios/Flutter/Release.xcconfig
# ---- 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