name: Hash Bags Windows build # Phase 1 of Windows CI: produce an unsigned Windows release bundle (.zip of # the Flutter Release output + MSVC runtime DLLs). Drop the zip on any Win # 10/11 box, extract, run hash_wallet.exe. # # Phase 2 (separate iteration): wire up Inno Setup installer once the # scripts/windows/build_exe_installer.iss file is rebranded from Cake Wallet # → Hash Bags. on: # Manual, or a release tag. workflow_dispatch runs as the triggering user, so # secrets are always available (PR triggers from forks would strip them — not # relevant here since the runner is self-hosted and listening on a # self-hosted Gitea). A tag builds automatically because a tag is the thing a # release is cut from, and the signed artifact should come from it rather # than from whatever happened to be 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. required: false default: '' # Cancel in-flight runs when a newer commit lands on the same branch. concurrency: group: windows-${{ github.ref }} cancel-in-progress: true defaults: run: # Explicit Git Bash path (not `shell: bash`) because Windows resolves # bare `bash` to C:\Windows\System32\bash.exe (WSL), which refuses to # run when the act_runner service is hosted under LocalSystem: # "Running WSL as local system is not supported." # Git Bash works fine under LocalSystem and matches how GitHub's # hosted windows-latest runner invokes bash. shell: '"C:\Program Files\Git\bin\bash.exe" --noprofile --norc -eo pipefail {0}' jobs: build: # Matches the label our act_runner.exe advertised: "windows:host" → executor=host. runs-on: windows env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} # Belt-and-suspenders fallback that bypasses Node TLS verification for # actions/* against the self-hosted runner's internal git host. Same # fallback as build-ios-sim.yml. NODE_TLS_REJECT_UNAUTHORIZED: '0' steps: - uses: actions/checkout@v4 with: fetch-depth: 1 # ---- Git config for LocalSystem --------------------------------------- # The runner service runs as LocalSystem, which has no global git # identity. scripts/monero_c/apply_patches.sh (and a few other # submodule operations) do `git commit`-equivalents that need # user.email + user.name set. Also pre-empt git's "safe.directory" # check, which fires when the checkout dir owner != runner identity. - name: Configure git identity + safe.directory run: | git config --global --add safe.directory '*' git config --global user.email "ci@suchsoftware.com" git config --global user.name "Hash Bags CI" git config --global --list # ---- Rust toolchain --------------------------------------------------- # The dnssec_proof plugin builds its native lib through cargokit # (windows/CMakeLists.txt -> apply_cargokit), so `flutter build windows` # genuinely needs cargo/rustc — the prebuilt monero_c bundle doesn't # cover it. # # setup-windows-runner.ps1 installs Rust to C:\Rust and puts # C:\Rust\cargo\bin on the *Machine* PATH, but the act_runner service # only picks up Machine PATH changes when it restarts, and a rebuilt or # re-imaged box loses it entirely. Either way the job died at exit 127 # ("rustc: command not found"). Locate it ourselves, and install it if # it really isn't there, so CI doesn't depend on the box's env being # right. No-op when rustc is already on PATH. - name: Ensure Rust toolchain is on PATH run: | set -e if command -v rustc >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1; then echo "Rust already on PATH: $(command -v rustc)" exit 0 fi # Known install locations, in preference order. First is where # setup-windows-runner.ps1 puts it; the rest cover a rustup-init # run under a normal user account. CANDIDATES=( "/c/Rust/cargo/bin" "$USERPROFILE/.cargo/bin" "/c/Users/Administrator/.cargo/bin" "/c/Windows/System32/config/systemprofile/.cargo/bin" ) for d in "${CANDIDATES[@]}"; do if [[ -x "$d/rustc.exe" ]]; then echo "Found Rust at $d — adding to PATH for later steps" export PATH="$d:$PATH" # $GITHUB_PATH wants a Windows-style path; MSYS converts the # resulting semicolon PATH back to POSIX form on shell start. cygpath -w "$d" >> "$GITHUB_PATH" # rustup's shims resolve toolchains via RUSTUP_HOME; without it # they look in %USERPROFILE%\.rustup, which is empty for the # LocalSystem service account. if [[ -d "$(dirname "$(dirname "$d")")/rustup" ]]; then RUSTUP_DIR="$(dirname "$(dirname "$d")")/rustup" # Export for THIS step as well as writing to $GITHUB_ENV. # $GITHUB_ENV only reaches later steps, and the verification on # the next line is itself a rustup shim: without the export it # falls back to %USERPROFILE%\.rustup, which on this runner is # a file rather than a directory, so it fails with os error 183 # having ignored the toolchain we just found. RUSTUP_HOME="$(cygpath -w "$RUSTUP_DIR")" CARGO_HOME="$(cygpath -w "$(dirname "$d")")" export RUSTUP_HOME CARGO_HOME echo "RUSTUP_HOME=$RUSTUP_HOME" >> "$GITHUB_ENV" echo "CARGO_HOME=$CARGO_HOME" >> "$GITHUB_ENV" fi rustc --version exit 0 fi done echo "Rust not found in any known location — installing to C:\\Rust" CARGO_HOME="C:\\Rust\\cargo" RUSTUP_HOME="C:\\Rust\\rustup" export CARGO_HOME RUSTUP_HOME mkdir -p /c/Rust/cargo /c/Rust/rustup # act_runner sets RUNNER_TEMP, but don't hard-depend on it. DL_DIR="${RUNNER_TEMP:-/c/Rust}" curl -fsSL -o "$DL_DIR/rustup-init.exe" https://win.rustup.rs/x86_64 "$DL_DIR/rustup-init.exe" -y \ --default-toolchain stable \ --profile minimal \ --target x86_64-pc-windows-msvc \ --no-modify-path echo "C:\\Rust\\cargo\\bin" >> "$GITHUB_PATH" echo "CARGO_HOME=$CARGO_HOME" >> "$GITHUB_ENV" echo "RUSTUP_HOME=$RUSTUP_HOME" >> "$GITHUB_ENV" /c/Rust/cargo/bin/rustc --version # ---- Windows Developer Mode (symlink support) ------------------------ # `flutter build windows` materialises each plugin under # windows/flutter/ephemeral/.plugin_symlinks/ as a real NTFS symlink, and # creating one needs SeCreateSymbolicLinkPrivilege. Flutter doesn't test # the privilege directly — it reads the AppModelUnlock registry key and # bails with: # "Building with plugins requires symlink support. # Please enable Developer Mode in your system settings." # Setting the key is exactly what the Settings toggle does; HKLM is # writable by the LocalSystem service account, so no interactive login # is involved. Idempotent — a no-op once the value is 1. - name: Enable Developer Mode (symlink support for plugin builds) run: | set -e powershell -NoProfile -Command ' $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } $cur = (Get-ItemProperty -Path $key -Name AllowDevelopmentWithoutDevLicense -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense if ($cur -eq 1) { Write-Host "Developer Mode already enabled" } else { Set-ItemProperty -Path $key -Name AllowDevelopmentWithoutDevLicense -Value 1 -Type DWord Write-Host "Developer Mode enabled (was: $cur)" } ' # ---- Flutter, pinned ------------------------------------------------- # Every other platform builds on 3.41.9: Linux and Android inside the # cake_wallet debian13-flutter3.41.9 image, iOS by installing it # explicitly. Windows was the one that took whatever the runner happened # to have, and on 2026-09-19 that was 3.38.5 / Dart 3.10.4. Dart 3.10 # removed the _macros package, so `flutter pub get` could not solve # hive_generator against build_resolvers and the build died at exit 69 # long before it compiled anything. A build machine's drifting toolchain # must not decide which dependencies resolve. - name: Require Flutter 3.41.9 run: | set -e ver="$(flutter --version 2>/dev/null | head -1 || true)" echo "runner has: ${ver:-}" case "$ver" in *3.41.9*) exit 0 ;; esac cat >&2 <<'MSG' Refusing to build: this runner's Flutter is not the pinned 3.41.9. Every other platform is pinned, Linux and Android by the cake_wallet debian13-flutter3.41.9 image and iOS by installing it, so a drifted Windows SDK breaks this platform alone and does it during dependency resolution rather than at compile time. Fix it on the runner rather than here. Installing an SDK per build would download a gigabyte to paper over a machine that provisioning is supposed to own, and subosito/flutter-action cannot do it anyway: its setup script needs jq, which this runner does not have. powershell -File scripts\windows\setup-windows-runner.ps1 That now checks the version rather than only that flutter.bat exists, which is why it had been reporting a drifted SDK as done. MSG exit 1 # ---- Toolchain sanity ------------------------------------------------ # Our self-hosted runner has Flutter / Rust / VS Build Tools / Inno # Setup pre-installed (see scripts/windows/setup-windows-runner.ps1). # This step just prints versions so failures are easy to diagnose. - name: Show installed toolchain versions run: | set -x # PATH may need refreshing after install — refresh from the registry. # Git Bash inherits Windows PATH at shell start, so just printing # `which` is enough. which flutter dart cargo rustc git 2>/dev/null || true flutter --version dart --version rustc --version cargo --version # ---- External path-dep plugins ---------------------------------------- # Several plugins are path-deps in pubspec.yaml. The path directories # must exist BEFORE `flutter pub get` runs against the regenerated # pubspec, or pub fails with "could not find package X at ". # # The Android + iOS + Linux workflows use upstream-published .tar.gz # tarballs. We don't, because those tarballs contain example/* # symlinks pointing to absolute /home/runner/.pub-cache/ paths # (artifacts from the upstream build host) — Linux tar tolerates # dangling symlinks, Windows tar bails the whole extraction. Clone # the source repos at the matching tagged versions instead. Same # pattern as bitbox_flutter further down. - name: Clone torch_dart at v1.0.17 run: | set -x -e pushd scripts rm -rf torch_dart git clone --depth 1 --branch v1.0.17 https://github.com/MrCyjaneK/torch_dart popd - name: Clone reown_flutter at v0.0.4 run: | set -x -e pushd scripts rm -rf reown_flutter git clone --depth 1 --branch v0.0.4 https://github.com/cake-tech/reown_flutter popd - name: Clone BitBox Flutter run: | # Pubspec has bitbox_flutter as a path dep — the directory must # exist for pub get even if BitBox Windows support isn't # functional. Same approach as the iOS workflow: clone the repo # at a pinned commit, don't run any Android-binding build step. 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) ------------------ # The same release-bundle.zip that Android + iOS use. Contains # pre-cross-compiled native libs for many target triples. We'll need # the x86_64-w64-mingw32 (Windows MinGW) or x86_64-pc-windows-msvc # entries — inspection step below logs what's actually shipped. - name: Fetch prebuilt monero_c 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" curl -fsSL -O https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC2/release-bundle.zip unzip -q release-bundle.zip # RC2 nests everything under an inner v0.18.4.6-*/ dir; strip it. shopt -s nullglob for inner in v0.18.4.6-*/; do mv "$inner"* . && rmdir "$inner"; done shopt -u nullglob rm release-bundle.zip echo "=== bundle contents (top level) ===" ls popd - name: Inspect Windows targets 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 "=== full mingw32 dir contents ===" ls -la "$BUNDLE_DIR/x86_64-w64-mingw32" 2>/dev/null || true # ---- Stage monero_c DLLs into the layout windows/CMakeLists.txt expects # Upstream Cake's Windows build cross-compiles monero_c on Linux via # scripts/windows/build_all.sh, which lays files out as: # scripts/monero_c/release//_.dll # The CMakeLists.txt at windows/CMakeLists.txt:84-94 hardcodes those # paths in install(FILES ...) rules — it copies + RENAMEs them to the # final names (monero_libwallet2_api_c.dll etc.) in the Release dir. # # We're using the prebuilt bundle from MrCyjaneK's release, which has # a different layout: # scripts/monero_c/release///lib_wallet2_api_c.dll # So we restage the bundle's files into the upstream-build-all-style # layout before flutter build windows runs. - name: Stage monero_c bundle into upstream cross-compile layout run: | set -x -e MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) SRC="scripts/monero_c/release/$MONERO_C_TAG/x86_64-w64-mingw32" MONERO_DST="scripts/monero_c/release/monero" WOWNERO_DST="scripts/monero_c/release/wownero" mkdir -p "$MONERO_DST" "$WOWNERO_DST" # The wallet C-API DLLs, one per coin cp -v "$SRC/libmonero_wallet2_api_c.dll" "$MONERO_DST/x86_64-w64-mingw32_libwallet2_api_c.dll" cp -v "$SRC/libwownero_wallet2_api_c.dll" "$WOWNERO_DST/x86_64-w64-mingw32_libwallet2_api_c.dll" # MinGW runtime DLLs (libssp-0, libwinpthread-1) — the wallet libs # are dynamically linked against these. MrCyjaneK's prebuilt # release-bundle.zip does NOT ship them (only the wallet *.dll + # *.dll.a import libs), so we source them from Git Bash's bundled # MinGW-w64 distribution, which ships the same x86_64-w64-mingw32 # ABI used to build the wallet libs. # ORDERING MATTERS: monero_c's prebuilt wallet DLLs are built against # recent MinGW-w64 (post-2022 winpthreads ABI with pthread_cond_timedwait64). # Flutter's bundled mingit ships a 2017-vintage libwinpthread that's # missing newer symbols and crashes wallet creation at runtime. Prefer # Git for Windows' mingw64/bin which is updated frequently. RUNTIME_PATHS=( "/c/Program Files/Git/mingw64/bin" # Git for Windows — modern MinGW runtime (preferred) "$SRC" # monero_c bundle (in case it grows DLLs) "/c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/bin" # choco install -y mingw "/c/flutter/bin/mingit/mingw64/bin" # Flutter's bundled mingit — old (2017), use only as last resort "/mingw64/bin" # MSYS2 fallback ) for runtime in libssp-0.dll libwinpthread-1.dll; do src="" for d in "${RUNTIME_PATHS[@]}"; do if [[ -f "$d/$runtime" ]]; then src="$d/$runtime" break fi done if [[ -z "$src" ]]; then echo "FATAL: $runtime not found in any of:" printf ' %s\n' "${RUNTIME_PATHS[@]}" for d in "${RUNTIME_PATHS[@]}"; do echo "--- contents of $d (if exists) ---" ls "$d" 2>/dev/null | head -20 || echo "(missing)" done exit 1 fi cp -v "$src" "$MONERO_DST/x86_64-w64-mingw32_${runtime}" done echo "=== final staged layout ===" ls -la "$MONERO_DST/" "$WOWNERO_DST/" # ---- Configure: pubspec.yaml + per-coin enablement ------------------- # Mirror of hashwallet.bat — drives tool/configure.dart with the same # set of coin flags that upstream Cake's Windows build uses. - name: Configure pubspec + Wallet types run: | set -x -e cp -f pubspec_description.yaml pubspec.yaml flutter pub get dart run tool/generate_pubspec.dart flutter pub get dart run tool/configure.dart \ --monero --bitcoin --ethereum --polygon --nano --bitcoinCash \ --wownero --dogecoin --base --arbitrum --bsc # ---- Secrets --------------------------------------------------------- - name: Generate per-module secrets.g.dart files (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 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: | # Length-only debug visibility — never prints actual secrets. echo "TROCADOR_API_KEY length: ${#TROCADOR_API_KEY}" echo "TROCADOR_MONERO_API_KEY length: ${#TROCADOR_MONERO_API_KEY}" echo "TROCADOR_EXCHANGE_MARKUP length: ${#TROCADOR_EXCHANGE_MARKUP}" if [[ -z "$TROCADOR_API_KEY" ]]; then echo "WARN: TROCADOR_API_KEY not reaching runner — build will proceed" echo " but Trocador exchange features won't work in this binary." fi 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 # ---- Codegen --------------------------------------------------------- - 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 # ---- Build the Windows .exe ------------------------------------------ - name: Build Windows release run: | set -x -e flutter config --enable-windows-desktop flutter build windows --dart-define-from-file=env.json --release --verbose # ---- Reconcile monero/wownero DLL naming for Dart FFI ---------------- # windows/CMakeLists.txt's install(FILES ... RENAME ...) rules produce # _libwallet2_api_c.dll, but package:monero's Dart FFI loader # (mrcyjanek's package) opens lib_wallet2_api_c.dll — different # filename layout, runtime error "Failed to load dynamic library". # Create the lib-prefixed copies so both name conventions resolve. - name: Mirror monero/wownero DLLs under lib-prefixed names run: | set -x -e DST="build/windows/x64/runner/Release" for coin in monero wownero; do src="$DST/${coin}_libwallet2_api_c.dll" dst="$DST/lib${coin}_wallet2_api_c.dll" if [[ -f "$src" ]]; then cp -v "$src" "$dst" else echo "WARN: $src not in Release dir — CMake install may have skipped it" ls "$DST" | head -20 fi done # ---- Bundle MSVC runtime DLLs with the .exe -------------------------- # Standalone Windows builds need msvcp140.dll + vcruntime140.dll + # vcruntime140_1.dll next to the .exe (or installed via vc_redist on # the user's machine). Bundling avoids the "VCRUNTIME140.dll not found" # error on machines without VC++ redistributable installed. - name: Copy MSVC runtime DLLs next to the .exe run: | set -x -e # Find the redist dir for the currently-installed MSVC version (the # version number rolls forward with VS updates, so do NOT hardcode). REDIST_BASE="/c/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Redist/MSVC" REDIST_DIR=$(find "$REDIST_BASE" -type d -name 'Microsoft.VC*.CRT' -path '*/x64/*' 2>/dev/null | sort | tail -1) if [[ -z "$REDIST_DIR" ]]; then echo "FATAL: could not find VC redist dir under $REDIST_BASE" ls "$REDIST_BASE" 2>/dev/null || echo "(redist base dir missing entirely)" exit 1 fi echo "Using redist dir: $REDIST_DIR" cp -v "$REDIST_DIR/msvcp140.dll" "$REDIST_DIR/vcruntime140.dll" "$REDIST_DIR/vcruntime140_1.dll" \ build/windows/x64/runner/Release/ # ---- Authenticode sign the launcher ---------------------------------- # This workflow already runs on the machine with the EV code-signing # token in it, but the runner (NT SERVICE\act_runner) cannot use the # token: SafeNet holds the login per user and an operator establishes it # interactively. So the exe is queued for the Fleet-installed signing # broker (such-fleet roles/windows_code_signing), which signs it in the # operator's session. CI never holds the key or the token password, so it # cannot spend one of the token's three password attempts. # # Only HashWallet.exe is signed. The bundled MSVC runtime DLLs are # already Microsoft-signed and must not be re-signed, and the plugin DLLs # are loaded by an already-trusted process -- it is the launcher the user # double-clicks that SmartScreen judges. - name: Sign HashWallet.exe with the hardware token shell: powershell run: | $ErrorActionPreference = 'Stop' $release = "build\windows\x64\runner\Release" $exe = Join-Path $release 'HashWallet.exe' if (-not (Test-Path $exe)) { throw "no HashWallet.exe at $exe" } if (Test-Path 'C:\signing\SIGNING_LOCKED') { Get-Content 'C:\signing\SIGNING_LOCKED' throw "The signing broker's circuit breaker is tripped. An operator must clear it." } $queued = "hashbags-$env:GITHUB_RUN_ID.exe" $inPath = "C:\signing\in\$queued" $outPath = "C:\signing\out\$queued" Copy-Item $exe $inPath -Force Set-Content -LiteralPath "$inPath.ready" -Value $env:GITHUB_RUN_ID Write-Host "queued $queued" $deadline = (Get-Date).AddMinutes(10) while ((Get-Date) -lt $deadline) { if (Test-Path "$outPath.done") { break } if (Test-Path 'C:\signing\SIGNING_LOCKED') { Get-Content 'C:\signing\SIGNING_LOCKED' throw "The broker tripped its circuit breaker while signing this artifact." } Start-Sleep -Seconds 5 } if (-not (Test-Path "$outPath.done")) { throw "Timed out waiting for the signing broker." } Move-Item $outPath $exe -Force Remove-Item "$outPath.done" -Force -ErrorAction SilentlyContinue # Verify on the binary that actually ships. A bad signature or a # missing timestamp blocks the release. $sig = Get-AuthenticodeSignature $exe $sig | Format-List Status, StatusMessage, @{n='Signer';e={$_.SignerCertificate.Subject}}, @{n='TimeStamper';e={$_.TimeStamperCertificate.Subject}} if ($sig.Status -ne 'Valid') { throw "Authenticode status is $($sig.Status)" } if (-not $sig.TimeStamperCertificate) { throw "Signature carries no RFC 3161 timestamp" } if ($sig.SignerCertificate.Subject -notmatch 'Such Software LLC') { throw "Unexpected signer: $($sig.SignerCertificate.Subject)" } Write-Host "SHA256 $((Get-FileHash $exe -Algorithm SHA256).Hash)" # ---- Package + upload ------------------------------------------------ - name: List built artifacts (for debug visibility) run: | ls -la build/windows/x64/runner/Release/ # ---- Package + upload as zip ----------------------------------------- # The Flutter Windows build produces a directory tree (~500 files — # HashWallet.exe + plugin DLLs + data/ + locales/ + bundled monero_c # + MinGW runtime DLLs). Ship as a zip; users extract and run. # # Tried wrapping with 7-Zip SFX for a single-file "portable" .exe # earlier but path_provider_windows computes the wallet storage dir # from the .exe location, and SFX extracts to a different %TEMP% # subdir on every launch — wallet keys couldn't be found across # runs. Multi-file zip is what Firefox Portable / VS Code Portable # / every other Flutter Windows desktop app does too. - name: Create Hash Bags Windows release zip run: | set -x -e cd build/windows/x64/runner/Release # PowerShell's Compress-Archive is in PATH; no extra deps needed. powershell -NoProfile -Command "Compress-Archive -Path .\* -DestinationPath ..\..\..\..\..\HashBags-windows-${{ github.sha }}.zip -Force" cd "$GITHUB_WORKSPACE" ls -la HashBags-windows-*.zip echo "Size: $(du -h HashBags-windows-${{ github.sha }}.zip | cut -f1)" - name: Upload Hash Bags Windows zip uses: actions/upload-artifact@v3 with: name: hash-bags-windows-${{ github.sha }} path: HashBags-windows-${{ github.sha }}.zip retention-days: 14 # ---- Publish to the GitHub release ------------------------------------ # PowerShell rather than the bash used elsewhere in this file: this step # parses JSON, and git-bash here has neither python nor node guaranteed, # while ConvertFrom-Json is native. Skips quietly without a token. - 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 signed zip to the GitHub release if: steps.reltag.outputs.tag != '' shell: powershell env: GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} TAG: ${{ steps.reltag.outputs.tag }} run: | $ErrorActionPreference = 'Stop' if (-not $env:GH_TOKEN) { Write-Host 'GH_RELEASE_TOKEN not set; skipping the GitHub upload' exit 0 } $repo = 'Such-Software/hash-wallet' $api = "https://api.github.com/repos/$repo" $hdr = @{ Authorization = "Bearer $env:GH_TOKEN"; 'User-Agent' = 'hash-bags-ci' } $src = Get-ChildItem -Path . -Filter 'HashBags-windows-*.zip' | Select-Object -First 1 if (-not $src) { throw 'no Windows zip to upload' } # Named for the tag, not the commit: a release asset called # HashBags-windows-<40 hex>.zip tells a downloader nothing. $name = "HashBags-windows-$env:TAG.zip" try { $rel = Invoke-RestMethod -Headers $hdr -Uri "$api/releases/tags/$env:TAG" } catch { Write-Host "no GitHub release for $env:TAG; skipping" exit 0 } # 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. foreach ($a in @($rel.assets | Where-Object { $_.name -eq $name })) { Invoke-RestMethod -Headers $hdr -Method Delete -Uri "$api/releases/assets/$($a.id)" | Out-Null } $up = "https://uploads.github.com/repos/$repo/releases/$($rel.id)/assets?name=$name" Invoke-RestMethod -Headers $hdr -Method Post -ContentType 'application/octet-stream' ` -InFile $src.FullName -Uri $up | Out-Null Write-Host "uploaded $name to the GitHub release $env:TAG"