# Hash Bags macOS builds, two lanes from one configured tree. # # developer-id Direct download: a zip of the .app signed with Developer ID # Application, notarized and stapled, published to the release. # app-store Mac App Store: a .pkg whose .app is signed with Apple # Distribution and whose installer is signed with 3rd Party Mac # Developer Installer, uploaded to App Store Connect. # # Each lane signs inside xcodebuild against its own provisioning profile # (scripts/macos/build_signed.sh). The approach and every failure it avoids are # in docs infra/macos-app-store-release.md, from Vegan IQ's first store build. # # Secrets. A selected lane with any of its own missing refuses in the first # step, naming each one, before forty minutes of prerequisites. # both lanes APPLE_KEYCHAIN_PASSWORD, APPLE_KEY_ID, APPLE_ISSUER_ID, # APPLE_API_KEY_P8_BASE64 (notarytool and altool) # developer-id MACOS_DEVELOPER_ID_CERT_P12_BASE64, MACOS_DEVELOPER_ID_CERT_P12_PASSWORD, # MACOS_DEVELOPER_ID_PROFILE_BASE64 (a "Developer ID" profile) # app-store APPLE_DIST_CERT_P12_BASE64, APPLE_DIST_CERT_P12_PASSWORD (shared # with iOS), MACOS_INSTALLER_CERT_P12_BASE64, # MACOS_INSTALLER_CERT_P12_PASSWORD, MACOS_APP_STORE_PROFILE_BASE64 # optional GH_RELEASE_TOKEN publishes the Developer ID zip to the release name: Hash Bags macOS build on: push: tags: - 'hash-bags-*' workflow_dispatch: inputs: lanes: description: Which macOS builds to produce type: choice options: [both, developer-id, app-store] default: both app_store_upload: description: >- Upload the App Store package to App Store Connect. A build number already uploaded is refused by Apple, so a rebuild of the same version sets this false. type: boolean default: true 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 # A tag push builds both lanes and uploads; a dispatch says what it wants. LANES: ${{ github.event.inputs.lanes || 'both' }} APP_STORE_UPLOAD: ${{ github.event.inputs.app_store_upload || 'true' }} KEYCHAIN: ${{ github.workspace }}/../hashbags-macos.keychain-db LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 steps: - uses: actions/checkout@v4 with: fetch-depth: 1 # Cheap guards first: a selected lane without its signing material refuses # here, in seconds, naming every missing secret. Presence only, never values. - name: Require each selected lane's signing material env: APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_BASE64 }} MACOS_DEVELOPER_ID_CERT_P12_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_PASSWORD }} MACOS_DEVELOPER_ID_PROFILE_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_PROFILE_BASE64 }} APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }} APPLE_DIST_CERT_P12_PASSWORD: ${{ secrets.APPLE_DIST_CERT_P12_PASSWORD }} MACOS_INSTALLER_CERT_P12_BASE64: ${{ secrets.MACOS_INSTALLER_CERT_P12_BASE64 }} MACOS_INSTALLER_CERT_P12_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERT_P12_PASSWORD }} MACOS_APP_STORE_PROFILE_BASE64: ${{ secrets.MACOS_APP_STORE_PROFILE_BASE64 }} run: | set -euo pipefail case "$LANES" in both|developer-id|app-store) ;; *) echo "FATAL: lanes must be both, developer-id or app-store, not '$LANES'"; exit 64 ;; esac need="APPLE_KEYCHAIN_PASSWORD APPLE_KEY_ID APPLE_ISSUER_ID APPLE_API_KEY_P8_BASE64" if [ "$LANES" != app-store ]; then need="$need MACOS_DEVELOPER_ID_CERT_P12_BASE64 MACOS_DEVELOPER_ID_CERT_P12_PASSWORD MACOS_DEVELOPER_ID_PROFILE_BASE64" fi if [ "$LANES" != developer-id ]; then need="$need APPLE_DIST_CERT_P12_BASE64 APPLE_DIST_CERT_P12_PASSWORD MACOS_INSTALLER_CERT_P12_BASE64 MACOS_INSTALLER_CERT_P12_PASSWORD MACOS_APP_STORE_PROFILE_BASE64" fi missing="" for v in $need; do eval "val=\${$v:-}" [ -n "$val" ] || missing="$missing $v" done if [ -n "$missing" ]; then echo "FATAL: lanes=$LANES needs these repository secrets, which are not set:" for v in $missing; do echo " $v"; done echo "The certificates and profiles they hold, and how to export them, are in" echo "docs infra/apple-desktop-signing.md. Run one lane with the lanes input" echo "while the other's material is still being gathered." exit 1 fi echo "lanes=$LANES: signing material present; App Store upload=$APP_STORE_UPLOAD" - name: Check Flutter 3.41.9 id: flutter_check run: | if command -v flutter >/dev/null && flutter --version 2>/dev/null | grep -q "3\.41\.9"; then echo "installed=true" >> "$GITHUB_OUTPUT" flutter --version else echo "installed=false" >> "$GITHUB_OUTPUT" fi - name: Install Flutter 3.41.9 (if missing) if: steps.flutter_check.outputs.installed != 'true' uses: subosito/flutter-action@v2 with: flutter-version: '3.41.9' 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 # ---- Prerequisites ------------------------------------------------------ # Ported step for step from build-linux.yml, which is the desktop build # known to pass, rather than written fresh. The first version of this # workflow was written fresh: it skipped the clone that creates # scripts/monero_c and died there, and behind that it had also skipped the # step that pins the crypto salts. curl instead of wget, and BSD sed's # -i.bak form, because this runner is a Mac. - name: Fetch prebuilt torch_dart run: | set -euo pipefail pushd scripts rm -rf torch_dart torch_dart.tar.gz curl -fsSL 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 -euo pipefail pushd scripts rm -rf reown_flutter reown_flutter.tar.gz curl -fsSL 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 # The iOS variant, not build_bitbox_flutter.sh: that script also builds # the Android .aar through gomobile, which an Apple target does not use. - name: Clone BitBox Flutter (Apple — skip Android bindings) run: | set -euo pipefail 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 ---------------------------------------------- # prepare_moneroc.sh clones scripts/monero_c at the pinned revision. It # is not a submodule, so checkout alone leaves the directory absent. - name: Fetch prebuilt monero_c bundle run: | set -euo pipefail ./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" if [ ! -d aarch64-apple-darwin ]; then 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 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. 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 # ---- Configure -------------------------------------------------------- # app_env.sh sets the name, bundle id and version that app_config.sh # writes into Info.plist; without it PlistBuddy writes empty strings. It # has to be sourced in the same shell, and app_config.sh has to start in # scripts/macos because it walks up with relative cds. - name: Run configure (cakewallet build profile) run: | set -euo pipefail pushd scripts/macos source ./app_env.sh cakewallet ./app_config.sh popd /usr/libexec/PlistBuddy -c "Print :CFBundleName" macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" macos/Runner/Info.plist - name: Generate per-module secrets.g.dart files (all empty defaults) run: | set -euo pipefail 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 -euo pipefail # 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). The same pinned values as # every other platform, so a wallet moves between them intact. 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 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: | set -euo pipefail sed -i.bak \ -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 rm -f lib/.secrets.g.dart.bak 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 - name: Build generated code (mobx + hive adapters) run: bash model_generator.sh - name: Generate localization run: dart run tool/generate_localization.dart # res/pictures is the source for every compiled brand asset. Skipping this # would fall back to raw SVGs, and it is the step whose inputs shipped # Cake's artwork in 1.0.3, so it runs here exactly as it does elsewhere. - name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec) run: ./compile_graphics.sh # ---- Configure the Xcode project, then build each lane ---------------- - name: Configure the macOS project run: flutter build macos --release --config-only # One throwaway keychain for both lanes, never the login keychain: this is # a shared Mac. Each tool is granted only the keys it uses: codesign the # application certificates, productbuild the installer one. - name: Prepare the signing keychain, profiles and API key env: APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_BASE64 }} MACOS_DEVELOPER_ID_CERT_P12_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_PASSWORD }} MACOS_DEVELOPER_ID_PROFILE_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_PROFILE_BASE64 }} APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }} APPLE_DIST_CERT_P12_PASSWORD: ${{ secrets.APPLE_DIST_CERT_P12_PASSWORD }} MACOS_INSTALLER_CERT_P12_BASE64: ${{ secrets.MACOS_INSTALLER_CERT_P12_BASE64 }} MACOS_INSTALLER_CERT_P12_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERT_P12_PASSWORD }} MACOS_APP_STORE_PROFILE_BASE64: ${{ secrets.MACOS_APP_STORE_PROFILE_BASE64 }} run: | set -euo pipefail umask 077 S="$RUNNER_TEMP/macos-signing" rm -rf "$S"; mkdir -p "$S/private_keys" # A job killed before cleanup leaves the keychain behind, and # create-keychain then refuses every later run until someone deletes it. security delete-keychain "$KEYCHAIN" 2>/dev/null || true security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN" security set-keychain-settings -lut 21600 "$KEYCHAIN" security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN" import() { # printf '%s' "$1" | base64 --decode > "$S/cert.p12" security import "$S/cert.p12" -k "$KEYCHAIN" -P "$2" -T "$3" rm -f "$S/cert.p12" } if [ "$LANES" != app-store ]; then import "$MACOS_DEVELOPER_ID_CERT_P12_BASE64" "$MACOS_DEVELOPER_ID_CERT_P12_PASSWORD" /usr/bin/codesign printf '%s' "$MACOS_DEVELOPER_ID_PROFILE_BASE64" | base64 --decode > "$S/developer-id.provisionprofile" fi if [ "$LANES" != developer-id ]; then import "$APPLE_DIST_CERT_P12_BASE64" "$APPLE_DIST_CERT_P12_PASSWORD" /usr/bin/codesign import "$MACOS_INSTALLER_CERT_P12_BASE64" "$MACOS_INSTALLER_CERT_P12_PASSWORD" /usr/bin/productbuild printf '%s' "$MACOS_APP_STORE_PROFILE_BASE64" | base64 --decode > "$S/app-store.provisionprofile" fi security set-key-partition-list -S apple-tool:,apple: -s -k "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null # Prepend rather than replace, so the runner keeps every keychain it had. EXISTING="$(security list-keychains -d user | sed -e 's/^[[:space:]]*"//' -e 's/"$//')" # shellcheck disable=SC2086 security list-keychains -d user -s "$KEYCHAIN" $EXISTING security find-identity -v "$KEYCHAIN" | sed -n 's/.*"\(.*\)".*/identity: \1/p' # Under the job's temp dir, not the runner home: altool finds it through # API_PRIVATE_KEYS_DIR, and nothing is left on the shared disk. printf '%s' "$APPLE_API_KEY_P8_BASE64" | base64 --decode > "$S/private_keys/AuthKey_${APPLE_KEY_ID}.p8" - name: Build the Developer ID app id: devid if: env.LANES != 'app-store' run: | set -euo pipefail APP="$(scripts/macos/build_signed.sh "$RUNNER_TEMP/macos-signing/developer-id.provisionprofile" \ "$KEYCHAIN" build/macos-developer-id ENABLE_HARDENED_RUNTIME=YES | tail -1)" echo "app=$APP" >> "$GITHUB_OUTPUT" - name: Notarize and staple the Developer ID app if: env.LANES != 'app-store' env: APP: ${{ steps.devid.outputs.app }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} run: | set -euo pipefail ZIP="$RUNNER_TEMP/notarize.zip" # ditto, not zip: it keeps the bundle's symlinks and extended attributes. ditto -c -k --keepParent "$APP" "$ZIP" xcrun notarytool submit "$ZIP" \ --key "$RUNNER_TEMP/macos-signing/private_keys/AuthKey_${APPLE_KEY_ID}.p8" \ --key-id "$APPLE_KEY_ID" --issuer "$APPLE_ISSUER_ID" \ --wait --timeout 45m rm -f "$ZIP" xcrun stapler staple "$APP" xcrun stapler validate "$APP" # What a user's Mac will decide on first launch, asked of this one. spctl --assess --type execute --verbose=2 "$APP" - name: Build the App Store package id: mas if: env.LANES != 'developer-id' run: | set -euo pipefail APP="$(scripts/macos/build_signed.sh "$RUNNER_TEMP/macos-signing/app-store.provisionprofile" \ "$KEYCHAIN" build/macos-app-store | tail -1)" # Matched by name: a hardcoded hash would break on the annual renewal. # -p codesigning is absent on purpose; an installer identity is not one. INSTALLER_ID="$(security find-identity -v "$KEYCHAIN" | sed -n 's/.*"\(3rd Party Mac Developer Installer:[^"]*\)".*/\1/p' | head -1)" [ -n "$INSTALLER_ID" ] || { echo "FATAL: no 3rd Party Mac Developer Installer identity in the keychain." echo "MACOS_INSTALLER_CERT_P12_BASE64 imported, so it most likely holds an" echo "application certificate; its CN should read Installer." exit 1; } mkdir -p dist PKG="dist/app-store.pkg" productbuild --component "$APP" /Applications \ --sign "$INSTALLER_ID" --keychain "$KEYCHAIN" "$PKG" pkgutil --check-signature "$PKG" echo "pkg=$PKG" >> "$GITHUB_OUTPUT" - name: Upload the App Store package to App Store Connect if: env.LANES != 'developer-id' && env.APP_STORE_UPLOAD == 'true' env: PKG: ${{ steps.mas.outputs.pkg }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} run: | set -euo pipefail API_PRIVATE_KEYS_DIR="$RUNNER_TEMP/macos-signing/private_keys" \ xcrun altool --upload-app --type macos --file "$PKG" \ --apiKey "$APPLE_KEY_ID" --apiIssuer "$APPLE_ISSUER_ID" - name: Name the outputs after the version they carry id: package env: DEVID_APP: ${{ steps.devid.outputs.app }} MAS_PKG: ${{ steps.mas.outputs.pkg }} run: | set -euo pipefail FULL=$(awk '/^version:/ {print $2; exit}' pubspec_description.yaml) VER="${FULL%%+*}"; BUILD="${FULL#*+}" if [ -z "$VER" ] || [ "$BUILD" = "$FULL" ]; then echo "pubspec_description.yaml version '$FULL' is not +; refusing to guess a name" >&2 exit 1 fi BASE="hash-bags-${VER}-build${BUILD}-macos" mkdir -p dist if [ -n "${DEVID_APP:-}" ]; then ditto -c -k --keepParent "$DEVID_APP" "dist/${BASE}.zip" echo "zip=dist/${BASE}.zip" >> "$GITHUB_OUTPUT" fi if [ -n "${MAS_PKG:-}" ]; then mv "$MAS_PKG" "dist/${BASE}-app-store.pkg" fi (cd dist && shasum -a 256 hash-bags-* | tee SHA256SUMS) echo "base=$BASE" >> "$GITHUB_OUTPUT" - name: Upload artifact uses: actions/upload-artifact@v3 with: name: ${{ steps.package.outputs.base }}-${{ github.sha }} path: dist/ 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 # Only the Developer ID zip is a download; the App Store package goes to Apple. - name: Publish to the GitHub release if: steps.reltag.outputs.tag != '' && steps.package.outputs.zip != '' env: GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} TAG: ${{ steps.reltag.outputs.tag }} FILE: ${{ steps.package.outputs.zip }} 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, profiles and API key if: always() run: | security delete-keychain "$KEYCHAIN" 2>/dev/null || true rm -rf "$RUNNER_TEMP/macos-signing"