mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
The first macOS run died after 88 seconds on `cd scripts/monero_c: No such file
or directory`. That was the cheap failure. The workflow had been written fresh
rather than ported, and behind that missing clone it had also skipped the step
that pins the crypto salts. Had it got further it would have shipped a build
that mints random salts, which is the 2026-08-29 corrupted-seeds incident on a
new platform: wallets created on it would not decrypt after the next upgrade.
So the prerequisites are now ported step for step from build-linux.yml, the
desktop build known to pass: torch_dart, reown_flutter, bitbox (the Apple
variant, without the gomobile Android bindings), prepare_moneroc.sh, configure,
secrets generation, salt pinning with the same refusal, Trocador injection,
codegen, localization, and compile_graphics.sh. curl for wget and BSD sed's
-i.bak form, because the runner is a Mac.
Two more things the fresh version got wrong, found while porting:
configure was run without sourcing app_env.sh, and app_config.sh writes
CFBundleName, the bundle id and the version into Info.plist from variables only
app_env.sh sets. The app would have shipped with an empty name. The step now
prints the three values it wrote.
And app_env.sh hardcoded 1.0.0 (1) on macOS, 6.1.0 (79) on Linux and 1.9.0 (29)
in the fish variant, the last two being Cake Wallet's own version numbers. On
macOS PlistBuddy writes that into the bundle, so every macOS build would have
called itself 1.0.0. Linux only escaped because its substitution looks for a
version: 0.0.0 placeholder that pubspec_description.yaml does not contain. All
three now derive from pubspec_description.yaml the way Android already does, and
the two sh variants resolve to 1.0.4 (14). The fish one is unverified: fish is
not installed here and CI does not use it.
Separately, four workflows verified their secret injection by grepping
lib/.secrets.g.dart into the log, which prints the values. Android's printed the
Trocador, WalletConnect, MoonPay and exchange-helper keys. Gitea likely masks
registered secrets, but masking is a best-effort layer and values never belong
in output. Each now reports every key's name and length and nothing else.
Windows is not in this commit. Its run refused correctly, on the guard added in
c11c79b0c: the runner has Flutter 3.38.5, not the pinned 3.32.0. That is fixed on
the runner, not in the workflow.
374 lines
18 KiB
YAML
374 lines
18 KiB
YAML
name: Hash Bags Linux build
|
|
|
|
# Triggers:
|
|
# - on push to dev/main (validates the foundation each time we land work)
|
|
# - on PRs targeting dev/main (gates merges)
|
|
# - manual via workflow_dispatch ("Run workflow" button in the UI)
|
|
on:
|
|
# Manual-only for pushes — auto-run on every push was too noisy. Trigger
|
|
# via Actions → "Hash Bags Linux build" → Run workflow when you want a
|
|
# build. Add `push: branches:` back here if/when we want pre-merge validation.
|
|
#
|
|
# Tags DO build automatically: a tag is a deliberate act and it is the thing
|
|
# a release is cut from, so the artifact that gets signed and published should
|
|
# come from the tag rather than from whatever was dispatched by hand.
|
|
push:
|
|
tags:
|
|
- 'hash-bags-*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_tag:
|
|
description: >-
|
|
Existing release tag to publish this build to (e.g. hash-bags-1.0.3).
|
|
Leave empty to build without publishing. The release must already
|
|
exist: this never creates one, because a release created here would
|
|
go public half-populated while the other platforms are still building.
|
|
required: false
|
|
default: ''
|
|
|
|
concurrency:
|
|
group: linux-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
jobs:
|
|
build:
|
|
# Both your Gitea linux-beast runner and GitHub-hosted ubuntu runners
|
|
# carry the `ubuntu-latest` label, so this works on either platform.
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
# Cake Labs' prebuilt build image with Flutter 3.32.0, Go 1.24.1, NDK r28,
|
|
# Rust nightly, and all the C++ build deps (boost, openssl, libsodium,
|
|
# libzmq, unbound, icu) preinstalled. Saves us from doing the simplybs
|
|
# source-bootstrap dance that ate 9GB on jw-laptop.
|
|
image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.32.0-ndkr28-go1.24.1-ruststablenightly
|
|
env:
|
|
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
|
# cake_wallet build profile name, retained from upstream — internal
|
|
# identifier only, doesn't appear in the binary or UI.
|
|
APP_LINUX_TYPE: cakewallet
|
|
|
|
steps:
|
|
- name: Fix Actions HOME pointing at /github/home (breaks ~/.cache writes)
|
|
run: echo "HOME=/root" >> $GITHUB_ENV
|
|
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Configure git inside the container
|
|
run: |
|
|
git config --global --add safe.directory '*'
|
|
git config --global user.email "ci@suchsoftware.com"
|
|
git config --global user.name "Hash Bags CI"
|
|
|
|
# ---- Secrets ---------------------------------------------------------
|
|
# IMPORTANT: don't pre-write any of the tool/.*secrets-config.json
|
|
# files. The generator at tool/generate_secrets_config.dart has an
|
|
# early-return if tool/.secrets-config.json already exists (lines
|
|
# 57-63), which then ALSO skips creating the per-module configs
|
|
# (evm, nano, bitcoin) — every cw_* module then fails to compile
|
|
# with "Undefined name secrets.xxx" for dozens of keys.
|
|
#
|
|
# Instead: run the generator first so it creates all configs from the
|
|
# full SecretKey list (with empty defaults), then sed-inject Trocador
|
|
# values into the freshly-generated lib/.secrets.g.dart.
|
|
|
|
# ---- External clones (use prebuilt tarballs, NOT prepare_*.sh) -------
|
|
# The prepare_torch.sh / prepare_reown.sh scripts do dev-time clones
|
|
# that take forever; CI uses the pre-tagged release tarballs instead.
|
|
- 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 (small, builds fast)
|
|
run: |
|
|
set -x -e
|
|
pushd scripts
|
|
./build_bitbox_flutter.sh
|
|
popd
|
|
|
|
# ---- Native crypto cores (monero_c prebuilt bundle) ------------------
|
|
# Skips the full simplybs toolchain bootstrap. The bundle has libmonero,
|
|
# libwownero, AND libzano .so files; CMake only installs the ones we
|
|
# ship (monero + wownero) — libzano is dropped at install time even
|
|
# though monero_c builds it.
|
|
- name: Fetch prebuilt monero_c .so bundle
|
|
run: |
|
|
set -x -e
|
|
./scripts/prepare_moneroc.sh
|
|
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
|
|
echo "monero_c TAG: $MONERO_C_TAG"
|
|
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
|
|
ls x86_64-linux-gnu/
|
|
popd
|
|
|
|
# ---- Configure: pubspec.yaml, AndroidManifest.xml, env vars ----------
|
|
- name: Run configure (cakewallet build profile)
|
|
run: |
|
|
pushd scripts/linux
|
|
source ./app_env.sh cakewallet
|
|
./app_config.sh
|
|
popd
|
|
|
|
# ---- Codegen: mobx, hive adapters, FFI bindings ----------------------
|
|
# model_generator.sh is already trimmed to our 8 enabled chains.
|
|
# Use `async` flag for parallel build_runner across modules.
|
|
- name: Generate per-module secrets.g.dart files (all empty defaults)
|
|
run: |
|
|
# tool/generate_new_secrets.dart imports app deps (encrypt/convert/intl),
|
|
# so the app's packages must be resolved first. pubspec_gen.sh runs
|
|
# `flutter pub get` WITHOUT `set -e`, so any hiccup fails silently and
|
|
# surfaces here as "Couldn't resolve the package". Resolve explicitly
|
|
# (and loudly) before running the tool.
|
|
set -e
|
|
flutter pub get
|
|
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 into lib/.secrets.g.dart
|
|
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: |
|
|
# Replace empty default '' with real values for the three Trocador keys.
|
|
# Other ~80 secrets stay as empty strings — code that uses them just
|
|
# fails-soft at runtime (no transaction history without Etherscan key,
|
|
# no Cake Pay without API key, etc.). Add more sed lines here as we
|
|
# acquire other affiliate keys.
|
|
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
|
|
|
|
# Initialize Flutter SDK once before parallel-ish work happens. flutter
|
|
# precache for Flutter 3.32.0 hits a 404 on flutter_gpu.zip (asset not
|
|
# actually published at the path Flutter constructs); the `|| true`
|
|
# absorbs that — desktop Linux build doesn't need flutter_gpu.
|
|
- name: Initialize Flutter SDK
|
|
run: |
|
|
flutter --version
|
|
flutter precache --linux --no-android --no-ios --no-macos --no-windows --no-fuchsia --no-web || true
|
|
|
|
# Sequential — async flag races on Flutter's startup lock and corrupts
|
|
# the SDK state, causing the "doesn't support null safety" cascade.
|
|
- 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
|
|
|
|
# ---- Compile + package ----------------------------------------------
|
|
- name: Build Linux app
|
|
run: flutter build linux --dart-define-from-file=env.json --release
|
|
|
|
# ---- Package as AppImage --------------------------------------------
|
|
# Self-contained .AppImage is the user-facing Linux deliverable: single
|
|
# file, double-click to run, no install, works on any glibc Linux from
|
|
# the last few years. appimagetool is run with --appimage-extract-and-run
|
|
# so it works inside the container without FUSE.
|
|
- name: Stage AppDir
|
|
run: |
|
|
set -e -x
|
|
REL=build/linux/x64/release
|
|
APPDIR=$REL/Hash_Bags.AppDir
|
|
rm -rf "$APPDIR"
|
|
mkdir -p "$APPDIR/usr/bin" \
|
|
"$APPDIR/usr/share/applications" \
|
|
"$APPDIR/usr/share/icons/hicolor/256x256/apps"
|
|
|
|
# The whole Flutter Linux bundle (binary + data/ + lib/) lives
|
|
# under usr/bin/. The Flutter binary has RPATH=$ORIGIN/lib, so
|
|
# plugin/native libs in usr/bin/lib/ resolve naturally.
|
|
cp -r "$REL/bundle"/* "$APPDIR/usr/bin/"
|
|
|
|
# Icon: scale the iOS marketing icon to 256 if convert is around,
|
|
# else copy as-is (appimagetool only requires that AppDir root and
|
|
# the hicolor path contain a same-named PNG).
|
|
ICON_SRC=assets/images/ios_icons/hashwallet_ios_icons/Icon-App-1024x1024@1x.png
|
|
ICON_DEST="$APPDIR/usr/share/icons/hicolor/256x256/apps/hash_bags.png"
|
|
if command -v convert >/dev/null 2>&1; then
|
|
convert "$ICON_SRC" -resize 256x256 "$ICON_DEST"
|
|
else
|
|
cp "$ICON_SRC" "$ICON_DEST"
|
|
fi
|
|
cp "$ICON_DEST" "$APPDIR/hash_bags.png"
|
|
|
|
# .desktop file (at both canonical locations; appimagetool checks both)
|
|
cat > "$APPDIR/hash_bags.desktop" <<'DESKTOP'
|
|
[Desktop Entry]
|
|
Name=Hash Bags
|
|
Comment=Non-custodial multi-chain crypto wallet
|
|
Exec=hash_wallet
|
|
Icon=hash_bags
|
|
Type=Application
|
|
Categories=Office;Finance;
|
|
Terminal=false
|
|
DESKTOP
|
|
# Strip the leading whitespace from the heredoc
|
|
sed -i 's/^ //' "$APPDIR/hash_bags.desktop"
|
|
cp "$APPDIR/hash_bags.desktop" "$APPDIR/usr/share/applications/"
|
|
|
|
# AppRun: the entry point AppImage executes. Adds the bundled lib/
|
|
# dir to LD_LIBRARY_PATH and exec's the Flutter binary.
|
|
cat > "$APPDIR/AppRun" <<'APPRUN'
|
|
#!/bin/bash
|
|
HERE="$(dirname "$(readlink -f "${0}")")"
|
|
export LD_LIBRARY_PATH="${HERE}/usr/bin/lib:${LD_LIBRARY_PATH}"
|
|
exec "${HERE}/usr/bin/hash_wallet" "$@"
|
|
APPRUN
|
|
sed -i 's/^ //' "$APPDIR/AppRun"
|
|
chmod +x "$APPDIR/AppRun"
|
|
|
|
ls -la "$APPDIR"
|
|
|
|
- name: Build AppImage
|
|
run: |
|
|
set -e -x
|
|
REL=build/linux/x64/release
|
|
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /tmp/appimagetool
|
|
chmod +x /tmp/appimagetool
|
|
# --appimage-extract-and-run avoids the FUSE requirement (no fusermount
|
|
# in the build container). ARCH= tells appimagetool which arch tag to
|
|
# embed in the filename.
|
|
ARCH=x86_64 /tmp/appimagetool --appimage-extract-and-run \
|
|
"$REL/Hash_Bags.AppDir" \
|
|
"$REL/Hash_Bags-x86_64.AppImage"
|
|
ls -la "$REL"/*.AppImage
|
|
|
|
# actions/upload-artifact@v4 uses an HTTP API that Gitea Actions does
|
|
# not implement (only the v1/v3 wire format). Pinned to @v3 for Gitea
|
|
# compatibility — also works on GitHub Actions.
|
|
- name: Upload AppImage artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: hash-wallet-linux-appimage-${{ github.sha }}
|
|
path: build/linux/x64/release/Hash_Bags-x86_64.AppImage
|
|
retention-days: 14
|
|
|
|
# ---- Publish to the release pages -------------------------------------
|
|
# Artifact retrieval through Gitea Actions is unreliable here: uploads
|
|
# succeed and then list as zero, so a build nobody can download is a build
|
|
# that did not happen. Release assets are the path that actually returns
|
|
# bytes, and GitHub is where users are told to look.
|
|
#
|
|
# Both steps skip quietly without a token, so a fork or a local runner
|
|
# does not fail a build over a credential it was never given.
|
|
- name: Resolve the release tag
|
|
id: reltag
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "${GITHUB_REF:-}" != "${GITHUB_REF#refs/tags/}" ]; then
|
|
TAG="${GITHUB_REF#refs/tags/}"
|
|
else
|
|
TAG="${{ github.event.inputs.release_tag }}"
|
|
fi
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
if [ -n "$TAG" ]; then echo "publishing to $TAG"; else echo "no tag; build only"; fi
|
|
|
|
- name: Publish the AppImage to the GitHub release
|
|
if: steps.reltag.outputs.tag != ''
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
|
|
TAG: ${{ steps.reltag.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${GH_TOKEN:-}" ]; then
|
|
echo "GH_RELEASE_TOKEN not set; skipping the GitHub upload"
|
|
exit 0
|
|
fi
|
|
REPO="Such-Software/hash-wallet"
|
|
API="https://api.github.com/repos/$REPO"
|
|
AUTH="Authorization: Bearer $GH_TOKEN"
|
|
FILE="build/linux/x64/release/Hash_Bags-x86_64.AppImage"
|
|
[ -f "$FILE" ] || { echo "no AppImage to upload"; exit 1; }
|
|
NAME="$(basename "$FILE")"
|
|
code=$(curl -sS -o rel-gh.json -w '%{http_code}' -H "$AUTH" "$API/releases/tags/$TAG" || true)
|
|
if [ "$code" != "200" ]; then
|
|
echo "no GitHub release for $TAG (HTTP $code); skipping"
|
|
exit 0
|
|
fi
|
|
# Fail rather than skip if there is no JSON parser. An upload that
|
|
# quietly did not happen leaves a release page that looks finished
|
|
# and a user with nothing to download.
|
|
PY=""
|
|
for c in python3 python; do command -v "$c" >/dev/null && { PY="$c"; break; }; done
|
|
[ -n "$PY" ] || { echo "no python available to parse the release JSON"; exit 1; }
|
|
ID=$($PY -c 'import json;print(json.load(open("rel-gh.json"))["id"])')
|
|
# Replace a same-named asset from an earlier attempt, so a re-run does
|
|
# not leave two that differ only by which build produced them.
|
|
for oid in $($PY -c 'import json,sys;d=json.load(open("rel-gh.json"));print(" ".join(str(a["id"]) for a in d.get("assets",[]) if a["name"]==sys.argv[1]))' "$NAME"); do
|
|
curl -sS -o /dev/null -X DELETE -H "$AUTH" "$API/releases/assets/$oid" || true
|
|
done
|
|
curl -sS --fail-with-body -o /dev/null -X POST -H "$AUTH" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary "@$FILE" \
|
|
"https://uploads.github.com/repos/$REPO/releases/$ID/assets?name=$NAME"
|
|
echo "uploaded $NAME to the GitHub release $TAG"
|