Gitea is on the same Mac mini as the runner, behind NPM in a separate
container. The proper fix is a hosts entry on the Mac that points
git.such.software at NPM's LAN IP so the runner reaches Gitea via
NPM's Let's Encrypt cert.
The env var stays in the workflow as a belt-and-suspenders fallback —
no harm if certs already validate cleanly, saves the workflow from
breaking again if the hosts entry ever gets blown away (system reset,
container migration, etc).
iOS:
- Build itself succeeds (Runner.app sits in build/ios/iphonesimulator/).
- actions/upload-artifact's Node.js HTTP client hits git.such.software
via LAN (NAT hairpin), bypassing the public NPM/Let's Encrypt cert
and landing on Gitea's internal self-signed cert. Node refuses with
DEPTH_ZERO_SELF_SIGNED_CERT.
- Setting NODE_TLS_REJECT_UNAUTHORIZED=0 at job env disables verification
for all Node-based actions in the job. Acceptable here because we're
talking to our own server on our own LAN — no external MITM surface.
- Long-term cleaner fix: install Gitea's CA cert into the runner's
system trust store, or have the runner reach Gitea via the public
hostname so NPM's Let's Encrypt cert is presented.
Android AAB:
- APK built + signed + uploaded fine (~14 min). AAB build then failed
at FinalizeBundleTask$BundleToolRunnable / :app:signReleaseBundle
with no visible error.
- Added --verbose to 'flutter build appbundle' so the next run prints
the actual gradle stacktrace. Once we see WHY signReleaseBundle is
failing, we can target the real fix.
Build was failing with:
component download failed for rust-std-aarch64-apple-ios-sim:
could not rename downloaded file from '.../9362b66...partial' to
'.../9362b66...': No such file or directory
One of the iOS pods (likely reown_walletkit's yttrium FFI or similar)
invokes rustup mid-build to fetch iOS targets. When the pod build runs
multiple targets in parallel, multiple rustup processes race on the
same .partial download file — one finishes/renames, the next looks
for the .partial that's no longer there.
Fix: add a workflow step that runs before pod install + flutter build:
- Install rustup if missing (idempotent — self-hosted runner persists).
- Wipe any stale *.partial files (debris from previous racing).
- rustup target add the three iOS triples serially.
That way the pod's mid-build rustup invocation finds everything already
installed and just no-ops.
Two fixes that together address the 'There is no XCFramework found at...'
build failure:
1) ios/Runner.xcodeproj/project.pbxproj: remove 10 lines referencing
ZanoWallet.xcframework + WarpApiFFI.xcframework (zcash). Those were
stale — both chains were removed in commit 4eb7db0b7 weeks ago, but
the Xcode project still listed them in BuildFile, CopyFiles, FileReference,
and Group sections. xcode error referenced them as missing.
2) Both iOS workflows now run scripts/ios/gen_framework.sh after staging
the monero_c bundle. The script reads
aarch64-apple-ios/lib<wallet>_wallet2_api_c.dylib and
aarch64-apple-ios-simulator/lib<wallet>_wallet2_api_c.dylib from the
bundle, wraps each as a .framework with an Info.plist, then bundles
device + simulator into ios/MoneroWallet.xcframework +
ios/WowneroWallet.xcframework via xcodebuild -create-xcframework.
These are exactly the paths project.pbxproj's CopyFiles phase
references.
The 'Inspect iOS targets' debug step was expanded to print bundle
subdirs + sample target contents, so if the monero_c bundle doesn't
actually ship those exact dirs we'll see it in the log.
Empty action-row buttons (Send/Receive/Swap/Scan) and empty bottom
navbar tabs are the same root cause on Linux + Android (+ likely iOS):
the workflows weren't running Cake's compile_graphics.sh, which is
what converts res/pictures/*.svg sources → assets/new-ui/*.svg.vec
precompiled vector assets that CakeImageWidget loads via
AssetBytesLoader.
History: commit 89e07fbba (Cake upstream, March 2026) moved to
precompiled SVGs, added compile_graphics.sh, and added a 'Compile
graphics' workflow step. Then 980c0278b added \*.svg.vec to .gitignore
(so the artifacts are no longer tracked). End result: CI MUST run
compile_graphics.sh to produce the .vec files, or every CakeImageWidget
silently renders nothing.
Fix:
- Add vector_graphics_compiler: ^1.1.16 to dev_dependencies (the script
invokes 'dart run vector_graphics_compiler ...' which needs the
package in the project's deps for 'dart run' to find it).
- Add a 'Compile SVG assets' step to all four workflows, between
generate_localization and the build/pod-install step.
This will populate assets/new-ui/ with ~82 .svg.vec files at build
time, so every CakeImageWidget reference (action row, navbar, top bar,
settings rows, etc.) resolves to its actual icon.
pod install fails with Encoding::CompatibilityError 'Unicode
Normalization not appropriate for ASCII-8BIT' when the shell locale
isn't UTF-8. CocoaPods' own warning tells you: 'CocoaPods requires
your terminal to be using UTF-8 encoding'.
Gitea Actions runner on macOS launches subprocesses with LANG unset,
which Ruby treats as ASCII-8BIT. Setting env-level LANG=en_US.UTF-8
and LC_ALL=en_US.UTF-8 fixes it for all steps in the job.
build_bindings.sh in bitbox_flutter unconditionally runs
'gomobile bind -target=android -androidapi 24' which produces an
api.aar — needed only for Android. The Mac runner has no Android SDK,
so gomobile fails with 'could not locate Android SDK'.
iOS doesn't need the .aar. BitBox's iOS support is a separate native
plugin under bitbox_flutter/ios/Classes/ + bitbox_flutter.podspec,
linked via flutter pub get + pod install. We just need the directory
present so the path-dep resolves.
Replace the wrapper invocation in iOS workflows with the clone steps
only; skip build_bindings.sh entirely. Android workflow keeps using
the wrapper as-is.
scripts/build_bitbox_flutter.sh invokes build_bindings.sh which calls
gomobile to generate iOS bindings for the Go BitBox client. Without
Go + gomobile on the Mac runner, that step fails with 'gomobile:
command not found'.
Adds an idempotent step: install Go via brew if missing, then 'go
install golang.org/x/mobile/cmd/{gomobile,gobind}@latest' if missing.
Self-hosted runner persists $GOPATH/bin between runs, so steady-state
is zero-cost after the first install.
Same conditional-install pattern as Flutter + CocoaPods.
Android:
- monero_c prebuilt bundle doesn't ship i686-linux-android (32-bit x86
Android — essentially extinct, sim/emulators use x86_64). Was failing
loud on the missing target. Now: missing target = skip + log; only
the truly-modern ABIs (arm64-v8a, x86_64) are REQUIRED. armeabi-v7a
(32-bit ARM) is shipped if available but not required. The resulting
APK is universal across whichever ABIs got staged.
iOS:
- 'sudo gem install cocoapods' was hitting a password prompt because
the Gitea runner user has no passwordless sudo configured (rightly).
Switched to brew-first install (Homebrew lives in its own prefix,
no sudo needed on Mac). Falls back to 'gem install --user-install'
(no sudo) if brew is somehow unavailable. The whole step is still
no-op if 'pod' is already on PATH.
Avoids any confusion / potential trademark friction with HashWallet
(Spanish hardware-wallet company). Hash Bags is a more distinctive
name, harder to collide with, and we can still rank for 'hash wallet'
search via App Store / Play Store subtitle ('Hash Bags - A Wallet
for your Coins').
What changed (all user-visible strings):
- Launcher labels: android/app.properties, scripts/android/app_env.sh,
scripts/ios/app_env.sh, linux window title (CMakeLists +
my_application.cc)
- In-app text: lib/generated/i18n.dart + ARB sources under res/values/
- Brand docs: README.md, CONTRIBUTING.md, PRIVACY.md,
assets/text/Terms_of_Use.txt, assets/text/Release_Notes.txt
- hash.boats website: index.html, og-image.svg + regen og-image.png,
site.webmanifest, styles.css comment, splash/confirm/pulse/mark/
lockup svg titles, report/index.html, server/server.js
- CI workflow display names ("Hash Bags Linux build", etc.)
What did NOT change (deliberate):
- Bundle ID / Package: com.suchsoftware.hashwallet (immutable app
identity on App Store + Play Store — changing == orphaning the app)
- Binary name: hash_wallet (Linux executable)
- Dart import paths: package:hash_wallet/...
- Git repo: hash-wallet (Such-Software/hash-wallet)
- URL scheme: hashwallet:// (internal deep links)
- support@such.software (no domain change yet)
- App Store / Play Store listing names (whatever was registered there
stays; we updated launcher label only)
- hash.boats domain (still the wallet site)
Also kept the '#ash Wallet' wordplay in the nav but updated to
'#ash Bags' so the # mark still serves as the H. Nav reads:
[# mark] #ash Bags
Website hero tagline now matches the App Store sub-title concept:
'A wallet for your coins. Open-source, non-custodial, multi-chain.
Privacy-leaning by default.'
108 files modified, ~674 string replacements.
iOS workflows were broken on the Mac mini runner because Flutter
wasn't installed. Three fixes:
1) Conditional Flutter setup. Check 'flutter --version' first; only
pull subosito/flutter-action@v2 if missing or wrong version. Self-
hosted Mac runner persists SDKs between runs, so steady-state is
zero-cost. First run on the runner triggers a full SDK download
via subosito (with cache: true so subsequent fresh installs are
still fast).
2) wget → curl across all iOS workflow steps. macOS doesn't ship
wget by default; curl is always present and respects -fsSL +
redirect properly.
3) Concurrency groups on all four workflows. A burst of commits no
longer queues N copies of the same build — newer pushes cancel
in-flight runs on the same branch. Groups:
linux-<ref>, android-<ref>, ios-sim-<ref>, ios-testflight-<ref>
This addresses the '6 runs from one merge' issue: each commit in
the merge triggered all 3 push-listening workflows. Concurrency
cancels the earlier 3 the moment the newer commit's runs start.
4) CocoaPods install is also conditional (was already 'if ! command -v
pod' under the hood; just tightened the step name).
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.