mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 07:11:29 +02:00
Run artifacts upload fine, but this Gitea reports zero artifacts for every run through its REST API, and the web download route answers "Not found." under token, bearer and basic auth alike. A signed build was therefore unreachable from anywhere except the browser session that produced it. Release assets do work: create, upload, list and download all round-trip with an ordinary token, so tag builds now attach their output to the tag's release as well as to the run. Retrieval must use browser_download_url; the asset-id API path returns the asset's JSON metadata even when asked for octet-stream. The step is fail-safe on purpose. With no token, no resolvable release, or a non-201 upload it explains itself and exits 0 rather than turning a green build red. The macOS upload also gains if-no-files-found, without which an empty publish passes silently.
294 lines
15 KiB
YAML
294 lines
15 KiB
YAML
name: macos build (gitea self-hosted)
|
|
|
|
# Runs ONLY on the Gitea mac runner (GitHub ignores .gitea/workflows).
|
|
# Dispatch-only: the mac runner has capacity 1 and also serves the Hash Bags
|
|
# iOS lane, so macOS wowlet builds are an explicit button, not a push hook.
|
|
# All build state lands under the runner's work dir, which lives on
|
|
# /Volumes/DataSSD (never backed up: the fleet backup pulls an allowlist of
|
|
# gitea data only).
|
|
#
|
|
# Signing/notarization uses the SAME seven secrets as the smirk-monorepo
|
|
# desktop lane, set by ~/keys/suchsoftware/set-gitea-desktop-secrets.sh:
|
|
# APPLE_CERTIFICATE (b64 Developer ID Application .p12),
|
|
# APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID,
|
|
# APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_P8_BASE64 (notarytool).
|
|
# If APPLE_CERTIFICATE is unset the build ships unsigned instead of failing.
|
|
|
|
on: [workflow_dispatch]
|
|
|
|
jobs:
|
|
build-macos:
|
|
name: "macOS (brew, native)"
|
|
runs-on: macos-latest
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: install dependencies (brew)
|
|
run: |
|
|
export PATH="/opt/homebrew/bin:$PATH"
|
|
set +o pipefail # yes(1) takes SIGPIPE when brew has nothing to ask
|
|
export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_UPGRADE=1
|
|
yes | brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp
|
|
|
|
- name: fetch wownerod to embed
|
|
run: |
|
|
set -e
|
|
ARCH=$(uname -m)
|
|
case "$ARCH" in
|
|
arm64) WOWTAR=wownero-aarch64-apple-darwin11-v0.11.4.0.tar.bz2 ;;
|
|
*) WOWTAR=wownero-x86_64-apple-darwin11-v0.11.4.0.tar.bz2 ;;
|
|
esac
|
|
mkdir -p /tmp/wownerod-embed /tmp/wow-extract
|
|
curl -L -o /tmp/wownero.tar.bz2 \
|
|
"https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/$WOWTAR"
|
|
# /tmp is a symlink on macOS — find(1) won't descend it as a bare argument
|
|
tar xjf /tmp/wownero.tar.bz2 -C /tmp/wow-extract
|
|
find /tmp/wow-extract -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \;
|
|
chmod +x /tmp/wownerod-embed/wownerod
|
|
ls -la /tmp/wownerod-embed
|
|
|
|
- name: fetch tor to embed
|
|
run: |
|
|
set -e
|
|
ARCH=$(uname -m)
|
|
case "$ARCH" in
|
|
arm64) TORTAR=tor-expert-bundle-macos-aarch64-15.0.tar.gz ;;
|
|
*) TORTAR=tor-expert-bundle-macos-x86_64-15.0.tar.gz ;;
|
|
esac
|
|
mkdir -p /tmp/teb /tmp/tor-embed
|
|
curl -L -o /tmp/teb.tgz "https://archive.torproject.org/tor-package-archive/torbrowser/15.0/$TORTAR"
|
|
tar xzf /tmp/teb.tgz -C /tmp/teb
|
|
cp /tmp/teb/tor/tor /tmp/teb/tor/libevent-*.dylib /tmp/tor-embed/
|
|
# tor ships UNSIGNED; arm64 macOS SIGKILLs unsigned binaries. Ad-hoc sign
|
|
# BEFORE embedding — the signature travels inside the file, so the copy
|
|
# the app unpacks at runtime on user machines is runnable too.
|
|
codesign -s - -f /tmp/tor-embed/tor /tmp/tor-embed/libevent-*.dylib
|
|
chmod +x /tmp/tor-embed/tor
|
|
# expert-bundle RELEASE number (15.0) is NOT the tor version — derive it
|
|
TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
|
|
echo "TOR_VERSION=$TORVER" >> "$GITHUB_ENV"
|
|
echo "embedded tor version: $TORVER"
|
|
|
|
- name: build
|
|
run: |
|
|
export PATH="/opt/homebrew/bin:$PATH"
|
|
# clang does not search /opt/homebrew/include by default; the feather
|
|
# target pulls monero headers that include boost, and modern Boost's
|
|
# CMake config leaves Boost_INCLUDE_DIRS empty. CPATH/LIBRARY_PATH make
|
|
# every linked brew header/lib findable without per-dep flag surgery.
|
|
export CPATH="$(brew --prefix)/include"
|
|
export LIBRARY_PATH="$(brew --prefix)/lib"
|
|
mkdir -p build && cd build
|
|
cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DBoost_USE_STATIC_LIBS=ON -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" ..
|
|
cmake --build . -j "$(sysctl -n hw.ncpu)"
|
|
|
|
- name: package app bundle
|
|
run: |
|
|
set -e
|
|
cd build
|
|
APP=$(find . -maxdepth 3 -name "*.app" -type d | head -1)
|
|
[ -n "$APP" ] || { echo "no .app bundle produced"; ls -la bin/; exit 1; }
|
|
mkdir -p dist
|
|
cp -R "$APP" dist/Wowlet.app
|
|
# cmake never places the icon in the bundle (upstream only copies it
|
|
# for the dormant `installed/` layout)
|
|
mkdir -p dist/Wowlet.app/Contents/Resources
|
|
cp ../src/assets/images/appicons/appicon.icns dist/Wowlet.app/Contents/Resources/
|
|
# bundle Qt frameworks/plugins so the app runs off-machine.
|
|
# -qmldir is not optional: our QML lives in .qrc resources, which
|
|
# macdeployqt cannot scan, so without it the bundle ships no QML
|
|
# modules at all and every page importing anything beyond QtQuick
|
|
# fails to load. That is how 5.1.2 shipped a grey mining tab on mac
|
|
# while Linux was fine: Contents/Resources held no qml directory, and
|
|
# the app logged 'module "QtQuick.Layouts" is not installed'.
|
|
/opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app -qmldir=../src/assets/qml
|
|
|
|
# qmlimportscanner reads only the `import` lines in our QML and does
|
|
# not follow a module's own qmldir depends chain, so transitive
|
|
# modules (QtQuick -> QtQml.WorkerScript/Models) are silently skipped.
|
|
# The AppImage build hit this twice and blanket-copies the trees; do
|
|
# the same here, before the dylib passes below so their deps resolve.
|
|
# Ask Qt where its QML modules live rather than assuming a brew
|
|
# layout; the AppImage build asks qmake the same question.
|
|
QTPREFIX="$(brew --prefix qt)"
|
|
QMLSRC="$("$QTPREFIX/bin/qmake6" -query QT_INSTALL_QML 2>/dev/null || echo "$QTPREFIX/share/qt/qml")"
|
|
[ -d "$QMLSRC" ] || { echo "ERROR: Qt QML modules not found at $QMLSRC" >&2; exit 1; }
|
|
mkdir -p dist/Wowlet.app/Contents/Resources/qml
|
|
for m in QtQuick QtQml; do
|
|
cp -Rn "$QMLSRC/$m" dist/Wowlet.app/Contents/Resources/qml/ 2>/dev/null || true
|
|
done
|
|
|
|
# Guard: the module the mining tab needs has to be in the bundle. A
|
|
# missing QML module is invisible until someone opens the tab, so it
|
|
# fails the build instead of shipping a page that paints grey.
|
|
for m in QtQuick QtQuick/Layouts; do
|
|
[ -d "dist/Wowlet.app/Contents/Resources/qml/$m" ] || {
|
|
echo "ERROR: $m missing from the bundle; QML pages will not load" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
# macdeployqt bundles Qt frameworks but not the plain-dylib brew deps
|
|
# of Qt's image plugins (libwebp/libsharpyuv/libbrotli...), which are
|
|
# referenced as @rpath/lib*.dylib. Without them a clean (no-Homebrew)
|
|
# Mac can't load those image formats. Qt frameworks use a different
|
|
# ref form (@rpath/Qt*.framework/...), so keying strictly on
|
|
# `@rpath/*.dylib` bundles the stragglers WITHOUT touching Qt.
|
|
# Several passes converge (a bundled lib pulls in further deps).
|
|
APP=dist/Wowlet.app
|
|
FW="$APP/Contents/Frameworks"
|
|
mkdir -p "$FW"
|
|
for pass in 1 2 3 4; do
|
|
while IFS= read -r macho; do
|
|
file "$macho" | grep -q Mach-O || continue
|
|
chmod u+w "$macho"
|
|
otool -L "$macho" | awk 'NR>1{print $1}' | while IFS= read -r dep; do
|
|
case "$dep" in
|
|
@rpath/*.dylib)
|
|
name=$(basename "$dep")
|
|
if [ -f "$FW/$name" ]; then
|
|
install_name_tool -change "$dep" "@executable_path/../Frameworks/$name" "$macho" 2>/dev/null || true
|
|
continue
|
|
fi
|
|
real=$(find /opt/homebrew/lib /opt/homebrew/opt/*/lib -maxdepth 1 -name "$name" 2>/dev/null | head -1)
|
|
if [ -n "$real" ]; then
|
|
cp -n "$real" "$FW/$name"
|
|
chmod u+w "$FW/$name"
|
|
install_name_tool -id "@executable_path/../Frameworks/$name" "$FW/$name" 2>/dev/null || true
|
|
install_name_tool -change "$dep" "@executable_path/../Frameworks/$name" "$macho" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
done < <(find "$APP" -type f)
|
|
done
|
|
|
|
# Fail if any @rpath/*.dylib (non-Qt) remains unresolved in the bundle
|
|
leftover=$(find "$APP" -type f | while IFS= read -r m; do
|
|
file "$m" | grep -q Mach-O || continue
|
|
otool -L "$m" | awk 'NR>1{print $1}' | grep -E '^@rpath/[^/]+\.dylib$' || true
|
|
done | sort -u)
|
|
if [ -n "$leftover" ]; then
|
|
echo "UNBUNDLED @rpath dylibs remain:"; echo "$leftover"; exit 1
|
|
fi
|
|
echo "all plain-dylib deps bundled"
|
|
ls -la "$FW" | grep -iE "webp|sharpyuv|brotli" || true
|
|
ls -la dist/
|
|
|
|
- name: sign and notarize (skipped when secrets absent)
|
|
run: |
|
|
set -e
|
|
cd build
|
|
if [ -z "$APPLE_CERTIFICATE" ]; then
|
|
echo "APPLE_CERTIFICATE not set — shipping UNSIGNED build"
|
|
exit 0
|
|
fi
|
|
KC=/tmp/wowlet-ci.keychain-db
|
|
KCPW=$(/usr/bin/uuidgen)
|
|
echo "$KCPW" > /tmp/wowlet-ci-kcpw # for the cleanup step only
|
|
security create-keychain -p "$KCPW" "$KC"
|
|
security set-keychain-settings -lut 3600 "$KC"
|
|
security unlock-keychain -p "$KCPW" "$KC"
|
|
printf '%s' "$APPLE_CERTIFICATE" | base64 -d > /tmp/dev_id.p12
|
|
security import /tmp/dev_id.p12 -k "$KC" -P "$APPLE_CERTIFICATE_PASSWORD" \
|
|
-T /usr/bin/codesign
|
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$KCPW" "$KC" >/dev/null
|
|
security list-keychains -d user -s "$KC" login.keychain-db
|
|
|
|
# Sign inside-out: every Mach-O NOT inside a .framework (embedded
|
|
# wownerod/tor, the bundled webp/sharpyuv/brotli dylibs, and the
|
|
# image-format plugins whose signatures install_name_tool just
|
|
# invalidated — keyed on `file`, not exec bit, since plugins/dylibs
|
|
# aren't +x), then each Qt framework as a unit, then the bundle.
|
|
SIGN() { codesign --force --options runtime --timestamp --keychain "$KC" \
|
|
--sign "$APPLE_SIGNING_IDENTITY" "$@"; }
|
|
find dist/Wowlet.app -type f ! -path "*/*.framework/*" \
|
|
| while read -r f; do file "$f" | grep -q Mach-O && SIGN "$f" || true; done
|
|
find dist/Wowlet.app/Contents/Frameworks -maxdepth 1 -name "*.framework" 2>/dev/null \
|
|
| while read -r fw; do SIGN "$fw"; done
|
|
SIGN dist/Wowlet.app
|
|
codesign --verify --deep --strict dist/Wowlet.app
|
|
|
|
printf '%s' "$APPLE_API_KEY_P8_BASE64" | base64 -d > /tmp/notary.p8
|
|
ditto -c -k --keepParent dist/Wowlet.app /tmp/wowlet-notarize.zip
|
|
xcrun notarytool submit /tmp/wowlet-notarize.zip \
|
|
--key /tmp/notary.p8 --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" \
|
|
--wait --timeout 30m
|
|
xcrun stapler staple dist/Wowlet.app
|
|
echo "signed + notarized + stapled"
|
|
|
|
- name: zip artifact
|
|
run: |
|
|
set -e
|
|
cd build/dist
|
|
ditto -c -k --keepParent Wowlet.app ../wowlet-macos-$(uname -m).zip
|
|
ls -la ../wowlet-macos-*.zip
|
|
|
|
- name: cleanup signing material
|
|
if: always()
|
|
run: |
|
|
if [ -f /tmp/wowlet-ci-kcpw ]; then
|
|
security delete-keychain /tmp/wowlet-ci.keychain-db || true
|
|
fi
|
|
rm -f /tmp/dev_id.p12 /tmp/notary.p8 /tmp/wowlet-ci-kcpw /tmp/wowlet-notarize.zip
|
|
|
|
- name: upload artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: wowlet-macos
|
|
path: build/wowlet-macos-*.zip
|
|
retention-days: 14
|
|
# Without this an empty upload passes silently, which is how a signed
|
|
# build can appear to publish while shipping nothing.
|
|
if-no-files-found: error
|
|
|
|
- name: publish the signed zip to the tag's release
|
|
# Same reason as the Linux job: run artifacts upload but this Gitea's
|
|
# REST listing returns zero for every run, so the notarized zip cannot
|
|
# be fetched back through the API. Release assets can be. Fail-safe:
|
|
# with no usable token this says why and exits 0.
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
env:
|
|
RELEASE_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -eu
|
|
FILE=$(ls build/wowlet-macos-*.zip 2>/dev/null | head -1)
|
|
if [ -z "$FILE" ]; then
|
|
echo "no macOS zip to publish; the packaging step should have failed first" >&2
|
|
exit 0
|
|
fi
|
|
TAG="${GITHUB_REF#refs/tags/}"
|
|
API="${GITHUB_SERVER_URL:-https://git.such.software}/api/v1/repos/$GITHUB_REPOSITORY"
|
|
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
|
echo "no token available to this runner, skipping the release upload" >&2
|
|
exit 0
|
|
fi
|
|
H="Authorization: token $RELEASE_TOKEN"
|
|
# Single-line python on purpose: a continuation line at column 0 would
|
|
# end this block scalar and break the workflow's YAML. `|| true` keeps
|
|
# a malformed response from aborting the step under `set -e`.
|
|
rid=$(curl -sS -H "$H" "$API/releases/tags/$TAG" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
if [ -z "$rid" ]; then
|
|
rid=$(curl -sS -H "$H" -H "Content-Type: application/json" -X POST \
|
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"wowlet $TAG\"}" "$API/releases" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
fi
|
|
if [ -z "$rid" ]; then
|
|
echo "could not resolve or create a release for $TAG, skipping the upload" >&2
|
|
exit 0
|
|
fi
|
|
code=$(curl -sS -H "$H" -o /dev/null -w '%{http_code}' -X POST \
|
|
-F "attachment=@$FILE" "$API/releases/$rid/assets?name=$(basename "$FILE")")
|
|
echo "release asset upload returned HTTP $code"
|
|
if [ "$code" != "201" ]; then
|
|
echo "the release upload did not return 201; the run artifact still holds the build" >&2
|
|
fi
|