ci(gitea): move the Linux AppImage off GitHub, and ship both Windows archives

Linux: the x86_64 AppImage now builds on the Gitea runner. Unlike the Windows
lane, which builds a static toolchain of its own through contrib/depends, it
links against the host's Qt6 -- so those dependencies are owned by such-fleet
(roles/wowlet_build_host) and the job asserts them rather than apt-installing.
A workflow that installs needs sudo on a shared runner and makes the build
host's contents a property of whichever job ran last.

Windows: the lane now publishes two archives instead of one. Authenticode
mutates the exe, so a signed binary can never byte-match a build someone
reproduces from source. Shipping only the signed one quietly costs the project
its reproducibility story; shipping only the unsigned one costs every normal
user a SmartScreen warning. Both, with both hashes in the PGP-signed
hashes.txt, costs neither -- and keeps one canonical download page rather than
splitting the project across a fork.

The AppImage lane carries the packaging knowledge that a plain linuxdeploy run
does not: qmlimportscanner does not follow a module's own qmldir dependency
chain, so the QtQuick and QtQml trees are copied wholesale and each copied
plugin's Qt libraries resolved by hand, and a guard fails the build if
mining.qml ever imports QtQuick.Controls again rather than shipping a white
mining tab.
This commit is contained in:
jwinterm
2026-08-31 18:23:46 -04:00
parent f5e40ab5b2
commit 11e8e3241c
2 changed files with 169 additions and 5 deletions
+149
View File
@@ -0,0 +1,149 @@
name: linux appimage (gitea self-hosted)
# Runs ONLY on Gitea (GitHub ignores .gitea/workflows), in the private Builds
# repository. Gitea is master; GitHub is a mirror and a release surface.
#
# Unlike the Windows lane, which builds a static toolchain of its own through
# contrib/depends, the x86_64 AppImage links against the host's Qt6. Those
# dependencies are owned by such-fleet (roles/wowlet_build_host) rather than
# installed here: a workflow that apt-installs needs sudo on a shared runner and
# makes the build host's contents a property of whichever job ran last.
#
# Dispatch-only. The runner is capacity 1 and shared with the fleet gates.
on: [workflow_dispatch]
jobs:
appimage:
name: "Linux x86_64 (host Qt6 AppImage)"
runs-on: ubuntu-latest # gitea label ubuntu-latest:host -> deb, host mode
steps:
- name: verify the build host has its dependencies
run: |
missing=0
for t in cmake make curl bzip2 git patchelf pkg-config qmake6; do
command -v "$t" >/dev/null || { echo "MISSING TOOL: $t"; missing=1; }
done
for h in /usr/include/boost/version.hpp; do
[ -f "$h" ] || { echo "MISSING HEADER: $h"; missing=1; }
done
pkg-config --exists libsodium || { echo "MISSING: libsodium"; missing=1; }
[ "$missing" = 0 ] || {
echo "Run playbooks/wowlet-build-host.yml in such-fleet; do not apt-install from CI."
exit 1; }
echo "qt $(qmake6 -query QT_VERSION)"; nproc; free -g | head -2
- name: configure git
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
submodules: recursive
- name: fetch wownerod to embed
run: |
rm -rf /tmp/wownerod-embed && mkdir -p /tmp/wownerod-embed
curl -fL -o /tmp/wownero.tar.bz2 \
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-linux-gnu-v0.11.4.0.tar.bz2
rm -rf /tmp/wow-x && mkdir -p /tmp/wow-x
tar xjf /tmp/wownero.tar.bz2 -C /tmp/wow-x
find /tmp/wow-x -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: |
rm -rf /tmp/tor-embed && mkdir -p /tmp/tor-embed
curl -fL -o /tmp/teb.tgz \
https://archive.torproject.org/tor-package-archive/torbrowser/15.0/tor-expert-bundle-linux-x86_64-15.0.tar.gz
rm -rf /tmp/teb && mkdir -p /tmp/teb && tar xzf /tmp/teb.tgz -C /tmp/teb
cp /tmp/teb/tor/tor /tmp/teb/tor/libssl.so.3 /tmp/teb/tor/libevent-2.1.so.7 \
/tmp/teb/tor/libcrypto.so.3 /tmp/tor-embed/
# The expert bundle's tor carries no rpath; point it and its libs at
# $ORIGIN so the embedded copy resolves them from the extract dir on
# any distro.
for f in tor libssl.so.3 libevent-2.1.so.7 libcrypto.so.3; do
patchelf --set-rpath '$ORIGIN' /tmp/tor-embed/$f
done
chmod +x /tmp/tor-embed/tor
- name: build
run: |
# TOR_VERSION must EXACTLY match `tor --version` of the embedded
# binary, or TorManager::unpackBins() sees embedded > unpacked and
# rewrites Tor on every launch. The bundle's release number (15.0) is
# not its tor version, so derive it.
TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
echo "embedded tor version: $TORVER"
# ARCH=x86-64, never native: the build host's CPU is newer than many
# users' and native bakes in unsupported instructions -> SIGILL.
cmake -S . -B build \
-DWITH_SCANNER=OFF \
-DARCH=x86-64 \
-DWOWNEROD_DIR=/tmp/wownerod-embed \
-DTOR_DIR=/tmp/tor-embed \
-DTOR_VERSION="$TORVER"
cmake --build build -j6
ls -la build/bin/
- name: package AppImage
run: |
rm -rf AppDir
mkdir -p AppDir/usr/bin AppDir/usr/share/applications \
AppDir/usr/share/icons/hicolor/256x256/apps
cp build/bin/feather AppDir/usr/bin/wowlet
chmod +x AppDir/usr/bin/wowlet
cp src/assets/wowlet.desktop AppDir/usr/share/applications/wowlet.desktop
cp src/assets/images/appicons/256x256.png \
AppDir/usr/share/icons/hicolor/256x256/apps/wowlet.png
curl -sL -o linuxdeploy https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
curl -sL -o linuxdeploy-plugin-qt https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
curl -sL -o appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x linuxdeploy linuxdeploy-plugin-qt appimagetool
export APPIMAGE_EXTRACT_AND_RUN=1
export QMAKE="$(command -v qmake6)"
export EXTRA_QT_PLUGINS="svg;multimedia;tls"
export QML_SOURCES_PATHS="$PWD/src/assets/qml"
export PATH="$PWD:$PATH"
./linuxdeploy --appdir AppDir --plugin qt \
-d AppDir/usr/share/applications/wowlet.desktop \
-i AppDir/usr/share/icons/hicolor/256x256/apps/wowlet.png
# qmlimportscanner reads only the `import` lines in our QML; it does
# not follow a module's own qmldir depends/default-import chain, so
# transitive modules (QtQuick -> QtQml.WorkerScript/Models) are
# silently skipped. Blanket-copy the trees, then resolve each copied
# plugin's Qt libs ourselves -- linuxdeploy only does that for modules
# it deployed, not ones we copied by hand.
QMLDIR=$(qmake6 -query QT_INSTALL_QML)
mkdir -p AppDir/usr/qml AppDir/usr/lib
for m in QtQuick QtQml; do cp -rn "$QMLDIR/$m" AppDir/usr/qml/ 2>/dev/null || true; done
find AppDir/usr/qml -name '*.so' -print0 | while IFS= read -r -d '' so; do
ldd "$so" 2>/dev/null | awk '/=> \//{print $3}' \
| grep -E '/(libQt6|libicu|libpcre2|libdouble-conversion|libzstd|libb2|libpng|libharfbuzz|libmd4c|libglib|libgraphite)' \
| while read -r lib; do cp -L -n "$lib" AppDir/usr/lib/ 2>/dev/null || true; done
done
echo "=== qt.conf ==="; cat AppDir/usr/bin/qt.conf
# Guard: mining.qml must not pull in QtQuick.Controls. If a future
# edit re-adds a Controls type this fails loudly instead of shipping
# a white mining tab.
if grep -q 'import QtQuick.Controls' src/assets/qml/mining.qml; then
echo "ERROR: mining.qml imports QtQuick.Controls again." >&2
exit 1
fi
ARCH=x86_64 ./appimagetool AppDir wowlet-x86_64.AppImage
ls -la wowlet-x86_64.AppImage
- name: upload wowlet AppImage
# v3: Gitea implements the older artifact protocol and rejects
# @actions/artifact v2 clients (upload-artifact@v4+) as unsupported.
uses: actions/upload-artifact@v3
with:
name: wowlet-x86_64-linux-AppImage
path: wowlet-x86_64.AppImage
if-no-files-found: error
+20 -5
View File
@@ -155,6 +155,12 @@ jobs:
name: wowlet-x86_64-windows-unsigned name: wowlet-x86_64-windows-unsigned
path: stage path: stage
- name: keep the unsigned tree for the reproducible archive
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
Copy-Item 'stage' 'unsigned' -Recurse -Force
- name: queue wowlet.exe for the signing broker and wait - name: queue wowlet.exe for the signing broker and wait
shell: powershell shell: powershell
run: | run: |
@@ -204,16 +210,25 @@ jobs:
} }
Write-Host "SHA256 $((Get-FileHash 'stage\wowlet.exe' -Algorithm SHA256).Hash)" Write-Host "SHA256 $((Get-FileHash 'stage\wowlet.exe' -Algorithm SHA256).Hash)"
- name: package the signed zip - name: package both zips
# Two archives ship, deliberately. Authenticode mutates the exe, so a
# signed binary can never byte-match what someone reproduces from
# source. The unsigned archive stays reproducible and is what verifiers
# check; the signed one is what a normal user should download. Both
# hashes go in the PGP-signed hashes.txt.
shell: powershell shell: powershell
run: | run: |
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Compress-Archive -Path 'stage\*' -DestinationPath 'wowlet-x86_64-windows.zip' -Force Compress-Archive -Path 'unsigned\*' -DestinationPath 'wowlet-x86_64-windows-unsigned.zip' -Force
Get-ChildItem 'wowlet-x86_64-windows.zip' | Format-List Name, Length Compress-Archive -Path 'stage\*' -DestinationPath 'wowlet-x86_64-windows.zip' -Force
Get-ChildItem 'wowlet-x86_64-windows*.zip' |
ForEach-Object { "{0} {1} {2}" -f $_.Name, $_.Length, (Get-FileHash $_.FullName -Algorithm SHA256).Hash }
- name: upload signed Windows zip - name: upload Windows zips
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: wowlet-x86_64-windows name: wowlet-x86_64-windows
path: wowlet-x86_64-windows.zip path: |
wowlet-x86_64-windows.zip
wowlet-x86_64-windows-unsigned.zip
if-no-files-found: error if-no-files-found: error