name: Hash Bags iOS Simulator build # Phase 1 of iOS CI: validate the Mac runner end-to-end by producing an # unsigned simulator .app. Drop the resulting Runner.app into the iOS # Simulator on any Apple Silicon Mac to verify the app actually compiles # and launches. # # Phase 2 (separate workflow): full TestFlight pipeline with signing. on: # Manual-only for now. Trigger via Actions → "Hash Bags iOS Simulator # build" → Run workflow when you want a build. workflow_dispatch: # Cancel in-flight runs when a newer commit lands on the same branch — so a # burst of commits doesn't pile up N copies of the same long build. concurrency: group: ios-sim-${{ 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 refuses to operate when LANG is ASCII-8BIT (default for the # Gitea runner shell). Force UTF-8 globally so `pod install` succeeds. LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 # Belt-and-suspenders fallback for when the self-hosted runner's internal # git host presents a self-signed cert. Preferred fix is to trust the # internal CA on the runner, after which this can be removed. (Runner # host and network specifics live in the internal CI docs, not this repo.) NODE_TLS_REJECT_UNAUTHORIZED: '0' steps: - uses: actions/checkout@v4 with: fetch-depth: 1 # Self-hosted Mac runner persists Flutter between runs. Only install if # it's not already on PATH or version doesn't match. - 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 # Clean any partial downloads from a previously crashed install — those # are the source of the 'could not rename .partial → final: No such # file or directory' race that breaks parallel pod builds. rm -rf "$HOME/.rustup/downloads"/*.partial 2>/dev/null || true # Pre-install iOS targets serially so the pod build doesn't try to # do it under concurrency. 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 # Ensure $(go env GOPATH)/bin is on PATH for subsequent steps. GOPATH=$(go env GOPATH) echo "$GOPATH/bin" >> "$GITHUB_PATH" export PATH="$PATH:$GOPATH/bin" # gomobile + gobind are needed by scripts/build_bitbox_flutter.sh 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 which curl unzip # ---- External prebuilt deps (same as android/linux) ------------------ # macOS doesn't ship wget by default; 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: | # Pubspec has bitbox_flutter as a path: dep at scripts/bitbox_flutter, # so the directory must exist for pub get. The bundled build_bindings.sh # runs `gomobile bind -target=android` which needs the Android SDK we # don't have on the Mac runner — and the resulting .aar is Android-only. # iOS uses bitbox_flutter's native ios/Classes plugin, no .aar required. 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 # ---- Native crypto cores (monero_c prebuilt bundle) ------------------ - 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 echo "=== bundle contents (one level) ===" ls popd - name: Inspect iOS targets available in monero_c bundle run: | set -x MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG" echo "=== bundle top-level ===" ls "$BUNDLE_DIR" || true echo "=== iOS-y subdirs ===" find "$BUNDLE_DIR" -maxdepth 1 -type d \( -name '*ios*' -o -name '*apple*' \) 2>/dev/null echo "=== sample contents of one apple-ios target ===" for d in "$BUNDLE_DIR"/aarch64-apple-ios "$BUNDLE_DIR"/aarch64-apple-ios-simulator; do [[ -d "$d" ]] && { echo "--- $d ---"; ls "$d"; } done - 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, GeneratedPluginRegistrant --- - 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 (all empty defaults) run: dart run tool/generate_new_secrets.dart - name: Pin crypto secrets (never ship random salts) env: CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }} CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }} CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }} CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }} CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }} CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }} CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }} CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }} run: | set -e # generate_new_secrets.dart mints RANDOM salts each run. Shipping # those breaks decryption of everything the previous build stored # (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned # values and refuse to build if any is missing. for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do eval "val=\${$v:-}" [ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; } done sed -i.bak \ -e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \ -e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \ -e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \ -e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \ -e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \ -e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \ -e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \ -e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \ lib/.secrets.g.dart rm -f lib/.secrets.g.dart.bak for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; } done echo "crypto secrets pinned (values not printed)" - 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: | # macOS sed needs '' after -i; Linux sed doesn't accept that. This # workflow runs on macos only. 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 awk -F"'" '/^const (trocador[A-Za-z]*) = / { split($1, a, " "); v = (length($2) ? "set (" length($2) " chars)" : "EMPTY"); print a[2] ": " v }' lib/.secrets.g.dart # lengths only; values never reach the log # ---- Flutter init + codegen ----------------------------------------- - 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 (mobx + hive adapters) 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 # CocoaPods: the iOS Podfile installs Flutter plugin pods. Required # before flutter build can link them. - name: pod install run: | cd ios pod install --repo-update # ---- Build iOS Simulator app (no codesign) --------------------------- - name: Build iOS Simulator app run: | set -x -e flutter build ios --simulator --no-codesign \ --dart-define-from-file=env.json ls -la build/ios/iphonesimulator/ - name: Zip Runner.app run: | cd build/ios/iphonesimulator zip -r hash_wallet_ios_sim_${{ github.sha }}.zip Runner.app # Confirm git.such.software resolves to a local target (the Mac itself). # The runner inherits the Mac's /etc/hosts directly (host-mode act_runner), # so the user should have: # 127.0.0.1 git.such.software # already in /etc/hosts. We only assert that here, don't modify (sudo # without NOPASSWD prompts for a password the runner can't enter). # # macOS's `getent` doesn't exist and `nslookup` bypasses /etc/hosts. # We grep /etc/hosts directly — that's what Node's getaddrinfo will # actually consult. - name: Verify Gitea hostname resolves locally run: | if grep -qE '^[[:space:]]*(127\.0\.0\.1|host\.docker\.internal|192\.168\.)[[:space:]]+git\.such\.software' /etc/hosts; then echo "✓ /etc/hosts points git.such.software at a local target:" grep git.such.software /etc/hosts else echo "✗ /etc/hosts is MISSING a local override for git.such.software." echo " Node will resolve to the public IP and fail on NAT hairpin." echo " Add (once, on the Mac mini):" echo " sudo sh -c 'echo \"127.0.0.1 git.such.software\" >> /etc/hosts'" exit 1 fi # Diagnostic: print exactly where upload-artifact will try to send data. # act_runner injects these env vars; their values tell us whether the # upload targets git.such.software (uses our /etc/hosts override) or # a LAN IP directly (override irrelevant). Also probe the URL with # curl -k so we see the cert + reachability before the real upload. - name: Diagnose Actions API endpoint run: | set +e echo "=== Actions runtime env ===" echo "ACTIONS_RUNTIME_URL=${ACTIONS_RUNTIME_URL:-}" echo "ACTIONS_RESULTS_URL=${ACTIONS_RESULTS_URL:-}" echo "ACTIONS_CACHE_URL=${ACTIONS_CACHE_URL:-}" echo "GITHUB_SERVER_URL=${GITHUB_SERVER_URL:-}" echo "GITHUB_API_URL=${GITHUB_API_URL:-}" echo "NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-}" echo # Probe each non-empty URL to see what Node will actually hit. for var in ACTIONS_RUNTIME_URL ACTIONS_RESULTS_URL GITHUB_SERVER_URL; do url="${!var}" [ -z "$url" ] && continue host=$(echo "$url" | sed -E 's|^https?://([^/:]+).*|\1|') echo "--- $var → $url ---" echo "DNS for $host:" dscacheutil -q host -a name "$host" 2>/dev/null || nslookup "$host" 2>/dev/null || true echo "HEAD probe (curl -kIv, 10s timeout):" curl -kIv --max-time 10 "$url" 2>&1 | head -40 || true echo done - name: Upload .app artifact uses: actions/upload-artifact@v3 with: name: hash-wallet-ios-sim-${{ github.sha }} path: build/ios/iphonesimulator/hash_wallet_ios_sim_*.zip retention-days: 14