diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 30e20a11e..e15354051 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -5,10 +5,26 @@ name: Hash Bags Linux build # - on PRs targeting dev/main (gates merges) # - manual via workflow_dispatch ("Run workflow" button in the UI) on: - # Manual-only for now — auto-run on every push was too noisy. Trigger + # 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:` back here if/when we want pre-merge validation. + # 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 }} @@ -295,3 +311,63 @@ jobs: 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" diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 000000000..948c389e0 --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,353 @@ +# Hash Bags macOS build: signed with Developer ID, notarized, stapled. +# +# This is DIRECT distribution, not the Mac App Store, so it needs a +# "Developer ID Application" certificate. That is a different certificate from +# the "Apple Distribution" one build-ios-testflight.yml uses, and the two are +# not interchangeable: a build signed with the App Store cert is rejected by +# Gatekeeper when a user downloads it from a web page. Notarization, on the +# other hand, reuses the App Store Connect API key that repository already has. +# +# Secrets, all optional. Absent ones do not fail the build; they downgrade it, +# loudly, and the preflight below prints exactly which stage you will lose. +# APPLE_CERTIFICATE base64 of the Developer ID Application .p12 +# APPLE_CERTIFICATE_PASSWORD password set when the .p12 was exported +# APPLE_SIGNING_IDENTITY "Developer ID Application: Such Software LLC (D8PL9F7X33)" +# APPLE_TEAM_ID D8PL9F7X33 +# APPLE_KEYCHAIN_PASSWORD any value; scopes a throwaway keychain +# APPLE_API_KEY_P8_BASE64 base64 of AuthKey_.p8 (notarization) +# APPLE_API_KEY key id (notarization) +# APPLE_API_ISSUER issuer UUID (notarization) +# GH_RELEASE_TOKEN publishes the zip to the GitHub release +# +# The same names smirk-monorepo uses, deliberately: both repositories live under +# the Builds owner, so an organisation-level secret serves both and a reader +# comparing the two workflows is not left wondering whether the difference means +# something. + +name: Hash Bags macOS build + +on: + push: + tags: + - 'hash-bags-*' + workflow_dispatch: + inputs: + release_tag: + description: >- + Existing release tag to publish 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: macos-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + build: + name: Build, sign, notarize + runs-on: macos-latest + timeout-minutes: 90 + env: + APP_MACOS_TYPE: cakewallet + LANG: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + # Cheap guards first: this reports what the run can actually produce + # before spending forty minutes finding out. Lengths only, never values. + - name: Preflight — which signing stages are available + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + set -euo pipefail + have() { [ -n "${1:-}" ] && echo "present (${#1} chars)" || echo "ABSENT"; } + echo "APPLE_CERTIFICATE : $(have "${APPLE_CERTIFICATE:-}")" + echo "APPLE_CERTIFICATE_PASSWORD: $(have "${APPLE_CERTIFICATE_PASSWORD:-}")" + echo "APPLE_SIGNING_IDENTITY : $(have "${APPLE_SIGNING_IDENTITY:-}")" + echo "APPLE_API_KEY_P8_BASE64 : $(have "${APPLE_API_KEY_P8_BASE64:-}")" + echo "APPLE_API_KEY : $(have "${APPLE_API_KEY:-}")" + echo "APPLE_API_ISSUER : $(have "${APPLE_API_ISSUER:-}")" + echo "GH_RELEASE_TOKEN : $(have "${GH_TOKEN:-}")" + echo + if [ -n "${APPLE_CERTIFICATE:-}" ] && [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then + echo "VERDICT: will code-sign with Developer ID" + else + echo "VERDICT: NO Developer ID cert — the .app will be ad-hoc signed and" + echo " Gatekeeper will refuse it on any machine but this one." + fi + if [ -n "${APPLE_API_KEY_P8_BASE64:-}" ] && [ -n "${APPLE_API_KEY:-}" ] && [ -n "${APPLE_API_ISSUER:-}" ]; then + echo "VERDICT: will notarize and staple" + else + echo "VERDICT: NO notarization credentials — users get the unidentified" + echo " developer warning even with a valid Developer ID signature." + fi + + - 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: | + set -euo pipefail + 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" + echo "GEM_HOME=$HOME/.gem" >> "$GITHUB_ENV" + echo "$HOME/.gem/bin" >> "$GITHUB_PATH" + gem install --user-install cocoapods + fi + + # ---- Native crypto cores ---------------------------------------------- + # Same prebuilt bundle the iOS and Android legs use, so all three ship the + # same wallet core rather than three builds of it. + - name: Fetch prebuilt monero_c bundle + run: | + set -euo pipefail + 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" + if [ ! -d aarch64-apple-darwin ]; then + 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 + fi + ls -la aarch64-apple-darwin x86_64-apple-darwin + popd + + # Runner.xcodeproj already references these two by bare filename with + # CodeSignOnCopy, so they belong beside the project at macos/. + - name: Stage universal dylibs into macos/ + run: | + set -euo pipefail + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + BUNDLE="scripts/monero_c/release/$MONERO_C_TAG" + for lib in libmonero_wallet2_api_c libwownero_wallet2_api_c; do + arm="$BUNDLE/aarch64-apple-darwin/$lib.dylib" + x86="$BUNDLE/x86_64-apple-darwin/$lib.dylib" + [ -f "$arm" ] || { echo "missing $arm"; exit 1; } + # Universal, so one download serves Apple Silicon and Intel. A + # single-arch build looks fine on the machine that made it and + # fails on half the users' machines, which is the worst way to + # find out. + if [ -f "$x86" ]; then + lipo -create "$arm" "$x86" -output "macos/$lib.dylib" + else + echo "no x86_64 slice for $lib; shipping arm64 only" + cp "$arm" "macos/$lib.dylib" + fi + lipo -info "macos/$lib.dylib" + done + + - name: Configure the app (bundle id, version, icons) + run: | + set -euo pipefail + # Copies pubspec_description.yaml over pubspec.yaml, so the version + # comes from the one file every platform reads. + pushd scripts/macos + bash ./app_config.sh + popd + grep -m1 '^version:' pubspec.yaml + + - name: flutter pub get + run: flutter pub get + + - name: Build macOS release + run: | + set -euo pipefail + flutter build macos --release + ls -la build/macos/Build/Products/Release/ + + # ---- Developer ID signing --------------------------------------------- + - name: Import the Developer ID certificate + id: cert + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${APPLE_CERTIFICATE:-}" ]; then + echo "no Developer ID certificate; skipping import" + echo "imported=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + KEYCHAIN_PASSWORD="${APPLE_KEYCHAIN_PASSWORD:-hashbags-ci}" + KEYCHAIN_PATH="$RUNNER_TEMP/hashbags-macos.keychain-db" + # A dedicated throwaway keychain, never the login keychain: this runner + # is a shared Mac and a build should not be able to read, or leave + # anything in, the operator's own keys. + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g') + P12="$RUNNER_TEMP/developer_id.p12" + echo "$APPLE_CERTIFICATE" | openssl base64 -d -A > "$P12" + security import "$P12" -k "$KEYCHAIN_PATH" -P "${APPLE_CERTIFICATE_PASSWORD:-}" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + rm -f "$P12" + security find-identity -v -p codesigning "$KEYCHAIN_PATH" + echo "keychain=$KEYCHAIN_PATH" >> "$GITHUB_OUTPUT" + echo "imported=true" >> "$GITHUB_OUTPUT" + + - name: Codesign the app + if: steps.cert.outputs.imported == 'true' + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + run: | + set -euo pipefail + APP=$(ls -d build/macos/Build/Products/Release/*.app | head -1) + echo "signing $APP" + # --deep is deliberately NOT used: Apple deprecated it and it skips + # nested code it does not recognise. Sign inside-out by hand so every + # dylib is covered and a failure names the file it could not sign. + find "$APP/Contents" -type f \( -name '*.dylib' -o -name '*.so' \) -print0 | + while IFS= read -r -d '' lib; do + codesign --force --timestamp --options runtime \ + --sign "$APPLE_SIGNING_IDENTITY" "$lib" + done + find "$APP/Contents/Frameworks" -maxdepth 1 -name '*.framework' -print0 2>/dev/null | + while IFS= read -r -d '' fw; do + codesign --force --timestamp --options runtime \ + --sign "$APPLE_SIGNING_IDENTITY" "$fw" + done + # Hardened runtime is what notarization requires; entitlements come + # from the templated Release.entitlements written by app_config.sh. + codesign --force --timestamp --options runtime \ + --entitlements macos/Runner/Release.entitlements \ + --sign "$APPLE_SIGNING_IDENTITY" "$APP" + codesign --verify --deep --strict --verbose=2 "$APP" + + # ---- Notarization ------------------------------------------------------ + - name: Notarize and staple + id: notarize + if: steps.cert.outputs.imported == 'true' + env: + APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: | + set -euo pipefail + if [ -z "${APPLE_API_KEY_P8_BASE64:-}" ] || [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_ISSUER:-}" ]; then + echo "no notarization credentials; the build stays signed but un-notarized" + echo "notarized=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + APP=$(ls -d build/macos/Build/Products/Release/*.app | head -1) + KEY="$RUNNER_TEMP/AuthKey.p8" + echo "$APPLE_API_KEY_P8_BASE64" | openssl base64 -d -A > "$KEY" + ZIP="$RUNNER_TEMP/notarize.zip" + # ditto, not zip: it preserves the bundle's symlinks and extended + # attributes, and a plain zip of a .app arrives at Apple as something + # it will not accept. + ditto -c -k --keepParent "$APP" "$ZIP" + xcrun notarytool submit "$ZIP" \ + --key "$KEY" --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" \ + --wait --timeout 45m + rm -f "$KEY" "$ZIP" + # Stapling writes the ticket into the bundle so Gatekeeper clears it + # without a network round trip, which is what a user on a bad + # connection or behind a firewall actually needs. + xcrun stapler staple "$APP" + xcrun stapler validate "$APP" + echo "notarized=true" >> "$GITHUB_OUTPUT" + + - name: Package the app + id: package + run: | + set -euo pipefail + APP=$(ls -d build/macos/Build/Products/Release/*.app | head -1) + VERSION=$(awk -F': ' '/^version:/ {print $2; exit}' pubspec_description.yaml | cut -d+ -f1) + OUT="HashBags-macos-${VERSION}.zip" + ditto -c -k --keepParent "$APP" "$OUT" + shasum -a 256 "$OUT" | tee "$OUT.sha256" + ls -la "$OUT" + echo "file=$OUT" >> "$GITHUB_OUTPUT" + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-macos-${{ github.sha }} + path: ${{ steps.package.outputs.file }} + retention-days: 14 + + - 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 to the GitHub release + if: steps.reltag.outputs.tag != '' + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + TAG: ${{ steps.reltag.outputs.tag }} + FILE: ${{ steps.package.outputs.file }} + 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" + 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 + ID=$(python3 -c 'import json;print(json.load(open("rel-gh.json"))["id"])') + for oid in $(python3 -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" + + - name: Clean up the keychain + if: always() && steps.cert.outputs.imported == 'true' + run: security delete-keychain "${{ steps.cert.outputs.keychain }}" || true diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 9b511daee..6ffdfd0d2 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -9,11 +9,24 @@ name: Hash Bags Windows build # → Hash Bags. on: - # Manual-only. Trigger via Actions → "Hash Bags Windows build" → Run workflow. - # 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). + # 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: @@ -584,3 +597,55 @@ jobs: 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" diff --git a/android/app/src/main/jniLibs/.gitignore b/android/app/src/main/jniLibs/.gitignore index e4a50eb2f..7572c5a3a 100644 --- a/android/app/src/main/jniLibs/.gitignore +++ b/android/app/src/main/jniLibs/.gitignore @@ -1,4 +1,14 @@ lib*_wallet2_api_c.so -# embedded Wownero node daemon (provisioned at build time, like the wallet libs) +# Embedded Wownero node daemon. NOT provisioned by any build step: no script +# here and no CI job stages it, so a CI-built APK does not contain it. It is +# built by hand and kept out of the tree entirely, at +# ~/Build/hash-wallet/embedded-node//libwownerod.so. +# +# Binaries do not belong in src even when ignored. Everything else in this +# directory is staging output that build_monero_all.sh or the Android workflow +# regenerates; this one had no generator, which is why it sat here for weeks. +# When the UI toggle and foreground service land, it needs a provisioning step +# beside the monero_c one, and shipping it before then would put 20 MB of +# unreachable executable in front of a store reviewer for no reason. libwownerod.so diff --git a/scripts/release/sign-release.sh b/scripts/release/sign-release.sh new file mode 100755 index 000000000..5e3dd438d --- /dev/null +++ b/scripts/release/sign-release.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# sign-release.sh: detach-sign every shipped desktop artifact, and verify. +# +# Same shape as smirk's, deliberately: one person verifies downloads from both +# products and should not have to learn two procedures. +# +# windows : HashBags-windows-.zip +# macos : HashBags-macos-.zip +# linux : Hash_Bags-x86_64.AppImage +# +# Each artifact gets a detached ASCII signature beside it (.asc), and +# SHA256SUMS is written and signed too. Signing the sums file is the one that +# matters most: it is what lets someone verify a download that reached them +# from a mirror or a store page, where the .asc never travelled with the file. +# +# Usage: +# scripts/release/sign-release.sh 1.0.3 --dir ~/release-1.0.3 +# scripts/release/sign-release.sh 1.0.3 --dir ~/release-1.0.3 --verify +# HASHBAGS_SIGNING_KEY= scripts/release/sign-release.sh 1.0.3 --dir ... +# +# HASHBAGS_SIGNING_KEY should name the RELEASE SIGNING SUBKEY, not the primary, +# and should carry a trailing exclamation mark so gpg uses exactly that subkey +# rather than choosing one itself. The primary never needs to be present on a +# build machine, and is not needed here. +set -euo pipefail + +fail() { echo "sign-release: $1" >&2; exit 1; } + +VERSION="${1:-}" +if [ -z "$VERSION" ] || [ "$VERSION" = "-h" ] || [ "$VERSION" = "--help" ]; then + sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//' + exit 2 +fi +shift + +DIR="" +VERIFY_ONLY=0 +while [ $# -gt 0 ]; do + case "$1" in + --dir) DIR="${2:-}"; shift 2 ;; + --verify) VERIFY_ONLY=1; shift ;; + *) fail "unknown argument: $1" ;; + esac +done +[ -n "$DIR" ] || fail "no --dir. Where are the downloaded artifacts?" +[ -d "$DIR" ] || fail "no such directory: $DIR" + +# The company release key. One key covers Hash Bags and Smirk, which is a +# deliberate choice: the fingerprint a user is asked to trust belongs to Such +# Software rather than to a product, so it does not have to be re-learned per +# app. The cost is that rotating it affects everything, which is why the +# primary stays offline and only this subkey signs. +KEY="${HASHBAGS_SIGNING_KEY:-96F6836D5110C2CB!}" + +command -v gpg >/dev/null || fail "gpg is not installed" + +cd "$DIR" +shopt -s nullglob +FILES=( HashBags-windows-*.zip HashBags-macos-*.zip Hash_Bags-*.AppImage *.dmg *.deb ) +[ ${#FILES[@]} -gt 0 ] || fail "no release artifacts found in $DIR" + +echo "artifacts in $DIR:" +printf ' %s\n' "${FILES[@]}" +echo + +if [ "$VERIFY_ONLY" -eq 0 ]; then + # SHA256SUMS first, so the signature covers the final set rather than a set + # that grew afterwards. + : > SHA256SUMS + for f in "${FILES[@]}"; do + if command -v sha256sum >/dev/null; then sha256sum "$f" >> SHA256SUMS + else shasum -a 256 "$f" >> SHA256SUMS; fi + done + cat SHA256SUMS + echo + + for f in "${FILES[@]}" SHA256SUMS; do + rm -f "$f.asc" + gpg --local-user "$KEY" --armor --detach-sign --output "$f.asc" "$f" + echo "signed $f" + done + echo +fi + +# Verify unconditionally, including right after signing. A signature that was +# written but cannot be checked is worth less than none, because it invites +# trust it has not earned. +rc=0 +for f in "${FILES[@]}" SHA256SUMS; do + [ -f "$f.asc" ] || { echo "MISSING SIGNATURE: $f.asc"; rc=1; continue; } + if gpg --verify "$f.asc" "$f" 2>/dev/null; then + echo "verified $f" + else + echo "VERIFY FAILED: $f"; rc=1 + fi +done +[ "$rc" -eq 0 ] || fail "one or more artifacts did not verify" +echo +echo "all artifacts signed and verified for $VERSION"