mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 07:11:29 +02:00
ubuntu-latest and windows-latest both resolve to the same host in host mode, and both jobs began with `rm -rf /tmp/wownerod-embed`. Dispatched together for 5.1.4, the Windows fetch deleted the Linux job's binary in the eight seconds between its own `ls` showing the file present and cmake configuring, so the Linux build died on "WOWNEROD_DIR was specified but the wownerod binary could not be found" while the identical workflow had passed minutes earlier when it ran alone. Both jobs now extract under /tmp/wowlet-$GITHUB_RUN_ID, so concurrent runs stop sharing state. macOS is left alone: it runs on its own host, it never rm -rf's the directory, and it is the one platform currently building green.
273 lines
13 KiB
YAML
273 lines
13 KiB
YAML
name: windows build + sign (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.
|
|
#
|
|
# Two jobs on two different machines, and that split is the whole point:
|
|
#
|
|
# build -> `deb`, which advertises windows-latest:host because it carries the
|
|
# MinGW cross-compiler. Produces an UNSIGNED wowlet.exe.
|
|
# sign -> `desktop-iim2fs2` (windows:host), the only machine with the EV
|
|
# code-signing token plugged into it.
|
|
#
|
|
# The signing host does not sign in this job. The runner there is
|
|
# NT SERVICE\act_runner, which cannot use the token: SafeNet holds the token
|
|
# login per user, and the login is established interactively by an operator.
|
|
# So the job queues the artifact for the signing broker that Fleet installs
|
|
# (roles/windows_code_signing) and waits for it. CI never sees the key and never
|
|
# sees the token password, so it can never spend one of the token's three
|
|
# password attempts.
|
|
#
|
|
# Dispatch-only. Both runners are capacity 1 and shared with other lanes.
|
|
|
|
on: [workflow_dispatch]
|
|
|
|
env:
|
|
HOST: x86_64-w64-mingw32
|
|
|
|
jobs:
|
|
build:
|
|
name: "Windows x86_64 (mingw cross, depends)"
|
|
runs-on: windows-latest # gitea label windows-latest:host -> deb, host mode
|
|
steps:
|
|
- name: verify the cross toolchain is present
|
|
# Host mode on a shared fleet runner: this job installs nothing and
|
|
# needs no sudo. Fleet owns the toolchain; if something is missing the
|
|
# job says exactly what rather than half-building.
|
|
run: |
|
|
missing=0
|
|
for t in curl bzip2 unzip zip git cmake make ccache python3 perl pkg-config \
|
|
x86_64-w64-mingw32-gcc-posix x86_64-w64-mingw32-g++-posix; do
|
|
command -v "$t" >/dev/null || { echo "MISSING: $t"; missing=1; }
|
|
done
|
|
# mingw32.mk needs the posix threading variant (std::thread/mutex/cv).
|
|
x86_64-w64-mingw32-g++ -v 2>&1 | grep -q 'Thread model: posix' || {
|
|
echo "MISSING: x86_64-w64-mingw32-g++ is not the posix threading variant"; missing=1; }
|
|
[ "$missing" = 0 ] || { echo "Fix the build host in such-fleet, not here."; exit 1; }
|
|
echo "toolchain ok"; nproc; free -g | head -2
|
|
|
|
- name: configure git
|
|
run: git config --global --add safe.directory '*'
|
|
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
# depends is the slow part (static Qt6/boost/openssl/tor from source).
|
|
# Cache the source tarballs and the compiled packages so only a cold run
|
|
# pays the ~1h cost.
|
|
- name: restore depends sources cache
|
|
id: cache-sources
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: contrib/depends/sources
|
|
key: depends-sources-${{ hashFiles('contrib/depends/packages/*') }}
|
|
restore-keys: |
|
|
depends-sources-
|
|
|
|
- name: restore depends built cache (mingw)
|
|
id: cache-built
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: contrib/depends/built
|
|
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
|
|
restore-keys: |
|
|
depends-built-mingw32-
|
|
|
|
- name: build depends (static Qt6 + tor + boost + openssl for mingw)
|
|
# -j4, not the -j2 the GitHub lane needs: that limit existed because a
|
|
# parallel static Qt6 compile OOMs a 16G hosted runner. deb has 30G.
|
|
run: make -C contrib/depends HOST="$HOST" -j4
|
|
|
|
# Save with if:always() so an expensive toolchain banks even when the
|
|
# later wowlet.exe build fails; the automatic post-save is skipped on
|
|
# job failure, which otherwise re-pays hours every time.
|
|
- name: save depends sources cache
|
|
if: always() && steps.cache-sources.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: contrib/depends/sources
|
|
key: depends-sources-${{ hashFiles('contrib/depends/packages/*') }}
|
|
|
|
- name: save depends built cache (mingw)
|
|
if: always() && steps.cache-built.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: contrib/depends/built
|
|
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
|
|
|
|
- name: fetch wownerod.exe to embed
|
|
run: |
|
|
# This job and the Linux one land on the same host in host mode, and a
|
|
# fixed /tmp path let each wipe the other's binary mid-build. Scope the
|
|
# extract to the run.
|
|
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
|
|
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
|
|
curl -fL -o "$EMBED/wownero-win.zip" \
|
|
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-w64-mingw32-v0.11.4.0.zip
|
|
# -j flattens the versioned subdir so wownerod.exe is a DIRECT child
|
|
# (WownerodQrcGenerator globs ${WOWNEROD_DIR}/* non-recursively).
|
|
unzip -o -j "$EMBED/wownero-win.zip" 'wownero-x86_64-w64-mingw32-v0.11.4.0/wownerod.exe' -d "$EMBED/wownerod-embed"
|
|
ls -la "$EMBED/wownerod-embed"
|
|
|
|
- name: build wowlet.exe
|
|
run: |
|
|
rm -rf build && mkdir build && cd build
|
|
# ARCH=x86-64: portable baseline. wownero's CMake defaults ARCH=native,
|
|
# which under a cross compiler bakes the BUILD HOST's instructions into
|
|
# the Windows binary -> SIGILL on older CPUs.
|
|
cmake --toolchain "$GITHUB_WORKSPACE/contrib/depends/$HOST/share/toolchain.cmake" \
|
|
-DARCH=x86-64 \
|
|
-DWITH_SCANNER=OFF \
|
|
-DCHECK_UPDATES=OFF \
|
|
-DTOR_INSTALLED=On \
|
|
-DFEATHER_TARGET_TRIPLET="$HOST" \
|
|
-DWOWNEROD_DIR="/tmp/wowlet-${GITHUB_RUN_ID:-local}/wownerod-embed" \
|
|
..
|
|
cmake --build . -j6
|
|
ls -la bin/
|
|
|
|
- name: stage the unsigned exe for signing
|
|
# The signing job needs the bare exe, not the zip: the broker signs one
|
|
# PE file at a time. The zip is assembled after signing so the published
|
|
# archive contains the signed binary.
|
|
run: |
|
|
mkdir -p stage/tor
|
|
cp build/bin/wowlet.exe stage/wowlet.exe
|
|
cp "contrib/depends/$HOST/Tor/tor.exe" stage/tor/tor.exe
|
|
(cp LICENSE stage/LICENSE.txt 2>/dev/null || cp COPYING stage/LICENSE.txt 2>/dev/null) || true
|
|
ls -la stage stage/tor
|
|
|
|
- name: upload unsigned staging tree
|
|
# v3, not v4: Gitea Actions implements the older artifact protocol and
|
|
# rejects @actions/artifact v2 clients with
|
|
# "GHESNotSupportedError ... not currently supported on GHES".
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: wowlet-x86_64-windows-unsigned
|
|
path: stage/
|
|
if-no-files-found: error
|
|
|
|
sign:
|
|
name: "Authenticode sign (hardware token broker)"
|
|
needs: build
|
|
runs-on: windows # gitea label windows:host -> desktop-iim2fs2
|
|
steps:
|
|
- name: download unsigned staging tree
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: wowlet-x86_64-windows-unsigned
|
|
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
|
|
shell: powershell
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$queued = "wowlet-${env:GITHUB_RUN_ID}.exe"
|
|
$inPath = "C:\signing\in\$queued"
|
|
$outPath = "C:\signing\out\$queued"
|
|
|
|
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
|
|
Get-Content 'C:\signing\SIGNING_LOCKED'
|
|
throw "The signing broker's circuit breaker is tripped. An operator must clear it."
|
|
}
|
|
|
|
Copy-Item 'stage\wowlet.exe' $inPath -Force
|
|
Set-Content -LiteralPath "$inPath.ready" -Value $env:GITHUB_RUN_ID
|
|
Write-Host "queued $queued"
|
|
|
|
$deadline = (Get-Date).AddMinutes(10)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (Test-Path "$outPath.done") { break }
|
|
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
|
|
Get-Content 'C:\signing\SIGNING_LOCKED'
|
|
throw "The broker tripped its circuit breaker while signing this artifact."
|
|
}
|
|
Start-Sleep -Seconds 5
|
|
}
|
|
if (-not (Test-Path "$outPath.done")) { throw "Timed out waiting for the signing broker." }
|
|
|
|
Move-Item $outPath 'stage\wowlet.exe' -Force
|
|
Remove-Item "$outPath.done" -Force -ErrorAction SilentlyContinue
|
|
|
|
- name: verify the published binary is signed and timestamped
|
|
# The broker verifies before releasing; this verifies again on the
|
|
# artifact that is actually about to ship. A signature or timestamp
|
|
# failure blocks distribution.
|
|
shell: powershell
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$sig = Get-AuthenticodeSignature 'stage\wowlet.exe'
|
|
$sig | Format-List Status, StatusMessage,
|
|
@{n='Signer';e={$_.SignerCertificate.Subject}},
|
|
@{n='TimeStamper';e={$_.TimeStamperCertificate.Subject}}
|
|
if ($sig.Status -ne 'Valid') { throw "Authenticode status is $($sig.Status)" }
|
|
if (-not $sig.TimeStamperCertificate) { throw "Signature carries no RFC 3161 timestamp" }
|
|
if ($sig.SignerCertificate.Subject -notmatch 'Such Software LLC') {
|
|
throw "Unexpected signer: $($sig.SignerCertificate.Subject)"
|
|
}
|
|
Write-Host "SHA256 $((Get-FileHash 'stage\wowlet.exe' -Algorithm SHA256).Hash)"
|
|
|
|
- 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
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
|
|
# Not Compress-Archive: Windows PowerShell writes the OS separator
|
|
# into entry names, producing "tor\tor.exe". The ZIP format mandates
|
|
# forward slashes, and extractors that take the backslash literally
|
|
# leave a file called "tor\tor.exe" in the root instead of a tor
|
|
# directory -- at which point the bundled Tor is not where wowlet
|
|
# looks for it, and this wallet gives its daemon an exclusive
|
|
# --tx-proxy, so no Tor means no transaction relay at all.
|
|
# v5.1.0 shipped tor/tor.exe from `zip -X`; keep that shape.
|
|
function New-Archive([string] $Source, [string] $Destination) {
|
|
if (Test-Path $Destination) { Remove-Item $Destination -Force }
|
|
$root = (Resolve-Path $Source).Path.TrimEnd('\')
|
|
$zip = [System.IO.Compression.ZipFile]::Open($Destination, 'Create')
|
|
try {
|
|
foreach ($file in Get-ChildItem $root -Recurse -File | Sort-Object FullName) {
|
|
$entry = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
|
[void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
|
$zip, $file.FullName, $entry, 'Optimal')
|
|
}
|
|
} finally { $zip.Dispose() }
|
|
}
|
|
|
|
New-Archive 'unsigned' 'wowlet-x86_64-windows-unsigned.zip'
|
|
New-Archive 'stage' 'wowlet-x86_64-windows.zip'
|
|
|
|
# Prove the shape rather than trusting it: a backslash in any entry
|
|
# name fails the build.
|
|
foreach ($archive in @('wowlet-x86_64-windows.zip', 'wowlet-x86_64-windows-unsigned.zip')) {
|
|
$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $archive))
|
|
try {
|
|
$names = $zip.Entries | ForEach-Object { $_.FullName }
|
|
if ($names -match '\\') { throw "$archive contains a backslash entry name: $($names -join ', ')" }
|
|
if ($names -notcontains 'tor/tor.exe') { throw "$archive is missing tor/tor.exe (got: $($names -join ', '))" }
|
|
} finally { $zip.Dispose() }
|
|
"{0} {1} {2}" -f $archive, (Get-Item $archive).Length,
|
|
(Get-FileHash $archive -Algorithm SHA256).Hash
|
|
}
|
|
|
|
- name: upload Windows zips
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: wowlet-x86_64-windows
|
|
path: |
|
|
wowlet-x86_64-windows.zip
|
|
wowlet-x86_64-windows-unsigned.zip
|
|
if-no-files-found: error
|