mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-07 08:04:04 +01:00
torch_dart's prebuilt tarball stores the framework's Versions/Current, top-level binary and Resources as copies of Versions/A, and codesign refuses the result as an ambiguous bundle, failing the app's signature. relink_framework.sh puts the three symlinks back, after checking that every copy it replaces matches Versions/A byte for byte.
536 lines
26 KiB
YAML
536 lines
26 KiB
YAML
# 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' }}
|
|
# Gitea hands a dispatch boolean over as a real false, which `|| 'true'`
|
|
# would turn back into true; format() keeps it as the string 'false'.
|
|
APP_STORE_UPLOAD: ${{ github.event_name == 'workflow_dispatch' && format('{0}', 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
|
|
# The tarball is packed on Linux with the framework's symlinks stored
|
|
# as copies, which codesign refuses as an ambiguous bundle.
|
|
scripts/macos/relink_framework.sh \
|
|
scripts/torch_dart/macos/LibTorch.xcframework/macos-arm64_x86_64/LibTorch.framework
|
|
|
|
- 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() { # <base64> <password> <tool>
|
|
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 <name>+<build>; 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"
|