mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
Three desktop targets, three different states, one destination. macOS did not build at all. There was no workflow, only Cake's from-source scripts and an Xcode project. It turns out most of the work was already done: monero_c ships aarch64-apple-darwin and x86_64-apple-darwin, and Runner.xcodeproj already references both dylibs with CodeSignOnCopy. So this fetches the same prebuilt bundle the iOS and Android legs use, lipos the two slices into universal libraries, and builds. One download serves Apple Silicon and Intel, because a single-arch build looks correct on the machine that made it and fails on half of everyone else's. It signs with a Developer ID Application certificate, which is NOT the Apple Distribution certificate the TestFlight workflow uses. They are not interchangeable: an App Store cert on a directly downloaded app is refused by Gatekeeper. Signing is inside-out by hand rather than --deep, which Apple deprecated and which skips nested code it does not recognise. Then notarytool with the App Store Connect key this repository already has, then stapler, so Gatekeeper clears the app without a network round trip. Every credential is optional and the preflight says, before spending forty minutes, exactly which stages the run can reach and what the user will see if it cannot reach them. Absent secrets downgrade the build loudly; they do not fail it and they do not pass silently. Windows and Linux already built. Windows already Authenticode-signs through the Fleet broker. What both lacked was a way to get the artifact to anybody: upload-artifact is the only publication and Gitea Actions artifacts here upload and then list as zero, so the signed Windows zip has never been reachable. Both now publish to the GitHub release, on the smirk pattern: tag-triggered, the release must already exist so a half-populated one never goes public, and a same-named asset from an earlier attempt is replaced rather than duplicated. Windows does its API work in PowerShell because git-bash there has neither python nor node guaranteed and ConvertFrom-Json is native. Tags now build automatically on all three. A tag is the thing a release is cut from, and the artifact that gets signed should come from it rather than from whatever was dispatched by hand. scripts/release/sign-release.sh detach-signs the collected artifacts and signs SHA256SUMS, which is the signature that matters when a download reaches someone from a mirror without its .asc. It verifies even immediately after signing: a signature written but never checked invites trust it has not earned. Not done here, and both need a human: GH_RELEASE_TOKEN does not exist in this repository's Gitea secrets, and the release key's user ID still says Smirk.
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
|
|
grep '^const trocador' lib/.secrets.g.dart # verify the substitutions
|
|
|
|
# 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"
|