igneum/.github/workflows/windows.yml
igneum-labs 60eb06ec24 CI hardening: one gate script for the hook and CI, the research exclusion list, the Windows paths check, the red watcher, the box runner switch, the failure classification
168 non-green runs since the first workflow run were classified (docs/analysis/ci-failures-2026-10-06.md): 102 were tree checks that finish in under 25 s on the pushing machine, 40 were GitHub-side refusals nobody saw.

tools/ci/pre-push.sh is the one list of fast checks; ci.yml's site job calls it with --ci and the pre-push hook with --hook (full gate for master and release-*, structural checks for other refs; never writes into the worktree). tools/ci/export-exclude.txt lists research documents outside the public export list, pruned by identity-check.sh and by the mirror's sync.sh (self-test: an excluded path may quote the patterns, an exported one may not); polish.md and this record are its first entries, which makes master green. tools/ci/windows-paths-check.sh (colon, trailing dot or space, reserved names, over 240 characters) runs as the pre-commit hook on staged paths and in the gate. tools/ci/red-watch.mjs plus the red job on the box's runner record one line per failed master or release-* run to /srv/ci-red/red.jsonl; igneum-ci-red.timer posts each once to the updates channel. pow and sims read IGNEUM_CI_RUNNER for the box. no-foreign-tree-writes.sh no longer exits silently on its warning pipeline under pipefail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 20:58:54 +00:00

313 lines
18 KiB
YAML

# Windows one-click app, built on GitHub's Windows runners so no PC is needed (4 October 2026).
#
# parse: every .ps1 under the Windows folders through the Windows PowerShell 5.1 parser (powershell.exe, the PowerShell
# on the PCs; 5.1 rejects "$name: text" and that class broke two launchers on 4 October), PSScriptAnalyzer as
# warnings, and a parenthesis check of every .bat/.cmd (the bare ")" class of 3 October). Required: build
# needs it.
# build: the engine (app/igneum-app, cargo on the MSVC target, so one fewer input), the window host exactly as
# app\windows\BUILD-APP.bat does it (MSVC, WebView2 SDK from NuGet, static loader, host.rc with the coin icon),
# the payload with packaging/windows/make-payload.sh in Git Bash, the installer with build-installer.ps1
# (Inno Setup, rcedit), a smoke run of both exes (--version, --help), the launcher's DRY_RUN, then the installer,
# the payload zip and the host as artifacts (90 days).
#
# Inputs that are not in git (igneumd.exe, igneum-miner.exe from the node fork; the prebuilt GPU workers with NVIDIA's
# NVRTC DLLs) come from payload-inputs.zip on the downloads host, published by packaging/windows/push-inputs.sh on the
# Mac; the DL_TOKEN repository secret is the path token (gh secret set DL_TOKEN < ~/.config/igneum/dl-token).
# The zip is trusted only through payload-inputs.json and its detached Ed25519 signature, made on the Mac with the
# OTA key: the step "payload inputs" verifies the signature with the public key compiled into the app
# (igneum-ota-sign verify-inputs embedded, built by the engine step), checks the zip's sha256 and every unpacked
# file against the manifest, and checks the manifest's node commit against packaging/windows/node-source.pin in
# this checkout, all before anything is built from them (review round 4, R4.5.2, ledger G13). The verified
# manifest, its signature and the runner's record go up as the igneum-windows-inputs artifact, which
# packaging/windows/fetch-ci-artifacts.sh re-verifies on the Mac before it will sign an update manifest.
# The Mac side of the loop is packaging/windows/fetch-ci-artifacts.sh (gh run download into the downloads folder).
#
# The packaged configuration (rotation phase 2, 5 October 2026; docs/plans/rotation-phase-2.md): the runner writes the
# repository secrets to the same files the Mac keeps under ~/.config/igneum, and make-payload.sh picks them exactly as
# on the Mac (packaging/mac/packaged-config.sh: a .next file wins when present).
# LOG_INTAKE_KEY required: the intake key the payload ships (gh secret set LOG_INTAKE_KEY < ~/.config/igneum/log-intake-key)
# LOG_INTAKE_KEY_NEXT optional, during a rotation: the next key; when set it is the one the payload ships
# DL_TOKEN required: the folder the inputs come from, and the manifest folder when no DL_TOKEN_NEXT
# DL_TOKEN_NEXT optional, during a rotation: the manifest folder the payload checks
# After a rotation the owner sets LOG_INTAKE_KEY and DL_TOKEN to the new values and deletes the two _NEXT secrets.
name: windows-ci
on:
push:
branches: [master]
paths:
- 'app/**'
- 'packaging/windows/**'
- 'packaging/mac/packaged-config.sh'
- 'proto-cuda/windows-app/**'
- 'proto-cuda/windows-miner/**'
- 'proto-cuda/windows-node/**'
- 'proto-cuda/nvrtc/**'
- 'proto-cuda/build.bat'
- 'proto-opencl/**'
- 'proving/windows-wsl2/**'
- 'relay/clients/**'
- 'relay/playbooks/**'
- 'brand/icons/**'
- 'tools/ci/windows/**'
- '.github/workflows/windows.yml'
workflow_dispatch:
concurrency:
group: windows-${{ github.ref }}
cancel-in-progress: true
jobs:
parse:
name: PowerShell 5.1 parse, PSScriptAnalyzer, batch parentheses
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Windows PowerShell 5.1 parse of every .ps1 (with the negative self-test)
shell: powershell
run: |
$PSVersionTable.PSVersion.ToString()
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-ps51.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: parentheses in every .bat and .cmd (with the negative self-test)
shell: powershell
run: |
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-bat.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: PSScriptAnalyzer (warnings only, never fails the job)
shell: powershell
continue-on-error: true
run: |
try {
if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -AllowClobber
}
Import-Module PSScriptAnalyzer
$folders = @('proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node', 'proving/windows-wsl2', 'relay/clients', 'relay/playbooks', 'packaging/windows', 'tools/ci/windows')
$total = 0
foreach ($f in $folders) {
$results = Invoke-ScriptAnalyzer -Path $f -Recurse -Severity Warning, Error -ExcludeRule PSAvoidUsingWriteHost, PSUseShouldProcessForStateChangingFunctions, PSUseSingularNouns, PSAvoidUsingPositionalParameters
foreach ($r in $results) {
$total += 1
$file = ($r.ScriptPath -replace '\\', '/')
Write-Host ("::warning file={0},line={1}::{2}: {3}" -f $file, $r.Line, $r.RuleName, $r.Message)
}
}
Write-Host "PSScriptAnalyzer: $total warnings (informational)"
} catch {
Write-Host "::warning::PSScriptAnalyzer could not run: $_"
}
build:
name: engine, window host, payload, installer, smoke run
needs: parse
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: versions
shell: bash
run: |
set -euo pipefail
v="$(sed -n 's/^version = "\(.*\)"/\1/p' app/igneum-app/Cargo.toml | head -1)"
echo "APP_VERSION=$v" >> "$GITHUB_ENV"
echo "app version $v"
rustc --version; cargo --version
git --version; bash --version | head -1; perl --version | sed -n 2p; 7z 2>/dev/null | head -2 | tail -1 || true
- name: engine (app/igneum-app, cargo build --release on the MSVC target)
shell: bash
working-directory: app/igneum-app
run: |
set -euo pipefail
cargo build --release --locked
ls -la target/release/igneum-app.exe
- name: packaged configuration (the secrets as the files packaged-config.sh reads; values never echoed)
shell: bash
env:
DL_TOKEN: ${{ secrets.DL_TOKEN }}
DL_TOKEN_NEXT: ${{ secrets.DL_TOKEN_NEXT }}
LOG_INTAKE_KEY: ${{ secrets.LOG_INTAKE_KEY }}
LOG_INTAKE_KEY_NEXT: ${{ secrets.LOG_INTAKE_KEY_NEXT }}
run: |
set -euo pipefail
mkdir -p "$HOME/.config/igneum"
if [ -z "${DL_TOKEN:-}" ]; then
echo "::error::the DL_TOKEN repository secret is not set. On the Mac: tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum"
exit 1
fi
if [ -z "${LOG_INTAKE_KEY:-}" ] && [ -z "${LOG_INTAKE_KEY_NEXT:-}" ]; then
echo "::error::neither LOG_INTAKE_KEY nor LOG_INTAKE_KEY_NEXT is set; the payload would ship without an intake key. On the Mac: tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum"
exit 1
fi
printf '%s' "$DL_TOKEN" > "$HOME/.config/igneum/dl-token"
[ -n "${DL_TOKEN_NEXT:-}" ] && printf '%s' "$DL_TOKEN_NEXT" > "$HOME/.config/igneum/dl-token.next"
[ -n "${LOG_INTAKE_KEY:-}" ] && printf '%s' "$LOG_INTAKE_KEY" > "$HOME/.config/igneum/log-intake-key"
[ -n "${LOG_INTAKE_KEY_NEXT:-}" ] && printf '%s' "$LOG_INTAKE_KEY_NEXT" > "$HOME/.config/igneum/log-intake-key.next"
chmod 600 "$HOME"/.config/igneum/*
echo "files: $(ls "$HOME/.config/igneum" | tr '\n' ' ')"
bash packaging/mac/packaged-config.sh --test
- name: payload inputs (payload-inputs.zip from the downloads host, signature, hashes and node commit verified)
shell: bash
env:
DL_TOKEN: ${{ secrets.DL_TOKEN }}
run: |
set -euo pipefail
base="https://dl.igneum.network/dl/$DL_TOKEN"
signer="app/igneum-app/target/release/igneum-ota-sign.exe"
[ -x "$signer" ] || { echo "::error::$signer was not built by the engine step"; exit 1; }
pin="packaging/windows/node-source.pin"
[ -s "$pin" ] || { echo "::error::$pin is missing: push-inputs.sh writes it, commit it with the inputs push"; exit 1; }
mkdir -p build/inputs # ~/.config/igneum/dl-token was written by the packaged configuration step
curl -fsSL --retry 3 -o build/payload-inputs.json "$base/payload-inputs.json"
curl -fsSL --retry 3 -o build/payload-inputs.json.sig "$base/payload-inputs.json.sig"
curl -fsSL --retry 3 -o build/payload-inputs.zip "$base/payload-inputs.zip"
echo "inputs manifest:"; cat build/payload-inputs.json
# 1. the signature (the key compiled into the app), the zip's sha256 and size, the pinned node commit: all before unpacking
"$signer" verify-inputs embedded build/payload-inputs.json build/payload-inputs.json.sig --zip build/payload-inputs.zip --node-commit "$pin"
7z x -y -bso0 -bsp0 -obuild/inputs-unpacked build/payload-inputs.zip
mv build/inputs-unpacked/payload-inputs/* build/inputs/
# 2. every unpacked file by sha256 and size, and nothing in the folder the manifest does not name
"$signer" verify-inputs embedded build/payload-inputs.json build/payload-inputs.json.sig --dir build/inputs
echo "inputs:"; ls -la build/inputs
for f in igneumd.exe igneum-miner.exe; do [ -f "build/inputs/$f" ] || { echo "::error::payload-inputs.zip has no $f"; exit 1; }; done
# 3. the runner's record for fetch-ci-artifacts.sh, which re-verifies the signature and the pin on the Mac
fp="$("$signer" embedded | sed -n 2p)"
node_commit="$(jq -r .node_source_commit build/payload-inputs.json)"
zip_sha="$(jq -r .zip.sha256 build/payload-inputs.json)"
mkdir -p build/inputs-artifact
cp build/payload-inputs.json build/payload-inputs.json.sig build/inputs-artifact/
printf '{ "run_id": "%s", "run_attempt": "%s", "head_sha": "%s", "key_fingerprint": "%s", "node_commit": "%s", "zip_sha256": "%s", "verified_at": "%s" }\n' \
"$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$GITHUB_SHA" "$fp" "$node_commit" "$zip_sha" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > build/inputs-artifact/inputs-verified.json
cat build/inputs-artifact/inputs-verified.json
- name: window host (app\windows\BUILD-APP.bat, exactly as on the PC)
shell: cmd
working-directory: app\windows
run: call BUILD-APP.bat < nul
- name: payload (packaging/windows/make-payload.sh, as on the Mac, in Git Bash)
shell: bash
run: |
set -euo pipefail
export IGNEUM_WIN_RELEASE="$PWD/build/inputs"
export IGNEUM_WORKERS_DIR="$PWD/build/inputs"
export IGNEUM_APP_EXE="$PWD/app/igneum-app/target/release/igneum-app.exe"
packaging/windows/make-payload.sh "$PWD/packaging/windows/dist/igneum-windows-app.zip"
test -f "packaging/windows/igneum-windows-app/Igneum Miner.exe" || { echo "::error::the window host did not land in the payload"; exit 1; }
- name: installer (packaging/windows/build-installer.ps1 in Windows PowerShell 5.1, Inno Setup, rcedit)
shell: powershell
working-directory: packaging\windows
run: |
$iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) { choco install innosetup -y --no-progress | Out-Null }
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\build-installer.ps1 -NoWinget -Version $env:APP_VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Get-ChildItem dist | Format-Table Name, Length
- name: smoke run (igneum-app.exe --version, Igneum Miner.exe --version and --help)
shell: powershell
run: |
$payload = Resolve-Path 'packaging\windows\igneum-windows-app'
function Run-Capture([string]$exe, [string]$flag) {
$out = Join-Path $env:RUNNER_TEMP ('smoke-' + [IO.Path]::GetRandomFileName() + '.txt')
$p = Start-Process -FilePath $exe -ArgumentList $flag -Wait -NoNewWindow -PassThru -RedirectStandardOutput $out
$text = if (Test-Path $out) { (Get-Content $out -Raw) } else { '' }
Write-Host ("{0} {1} -> exit {2}: {3}" -f (Split-Path -Leaf $exe), $flag, $p.ExitCode, $text.Trim())
if ($p.ExitCode -ne 0) { throw "$exe $flag exited $($p.ExitCode)" }
return $text
}
$v = $env:APP_VERSION
$a = Run-Capture (Join-Path $payload 'igneum-app.exe') '--version'
if ($a -notmatch "igneum-app $([regex]::Escape($v))") { throw "igneum-app --version did not print 'igneum-app $v'" }
$b = Run-Capture (Join-Path $payload 'Igneum Miner.exe') '--version'
if ($b -notmatch "Igneum Miner $([regex]::Escape($v))") { throw "Igneum Miner.exe --version did not print 'Igneum Miner $v' (version.h and Cargo.toml differ?)" }
$c = Run-Capture (Join-Path $payload 'Igneum Miner.exe') '--help'
if ($c -notmatch 'Usage') { throw 'Igneum Miner.exe --help did not print the usage line' }
$info = (Get-Item (Join-Path $payload 'Igneum Miner.exe')).VersionInfo
Write-Host ("host version block: {0} {1} {2}" -f $info.ProductName, $info.ProductVersion, $info.FileDescription)
if ($info.ProductName -ne 'Igneum Miner') { throw 'the host exe carries no Igneum Miner version block (host.rc)' }
- name: launcher dry run (proto-cuda/windows-app, DRY_RUN=1, Windows PowerShell 5.1 via the .ps1 and the .bat)
shell: powershell
run: |
$stage = Join-Path $env:RUNNER_TEMP 'launcher'
New-Item -ItemType Directory -Force -Path $stage | Out-Null
Copy-Item -Path 'proto-cuda\windows-app\*' -Destination $stage -Recurse -Force
foreach ($f in @('igneumd.exe', 'igneum-miner.exe', 'igneum-worker-cuda.exe', 'igneum-worker-opencl.exe')) {
if (Test-Path "build\inputs\$f") { Copy-Item "build\inputs\$f" $stage }
}
Get-ChildItem 'build\inputs' -Filter 'nvrtc*.dll' | Copy-Item -Destination $stage
$env:DRY_RUN = '1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $stage 'start-igneum.ps1')
if ($LASTEXITCODE -ne 0) { throw "start-igneum.ps1 DRY_RUN=1 exited $LASTEXITCODE" }
$bat = cmd /c "cd /d `"$stage`" && START-IGNEUM.bat < nul 2>&1" | Out-String
Write-Host $bat
if ($bat -notmatch 'dry run done') { throw 'START-IGNEUM.bat with DRY_RUN=1 did not reach the end of the plan' }
- name: sizes
shell: bash
run: |
set -euo pipefail
{
echo "## Windows build $APP_VERSION"
echo
echo "| file | bytes |"
echo "|---|---:|"
for f in packaging/windows/dist/Igneum-Miner-Setup-*.exe packaging/windows/dist/igneum-windows-app.zip "app/windows/dist/Igneum Miner.exe" app/igneum-app/target/release/igneum-app.exe; do
printf '| %s | %s |\n' "$(basename "$f")" "$(stat -c %s "$f")"
done
echo
echo "inputs (signature, hashes and node commit verified): $(tr -d '\n' < build/payload-inputs.json | head -c 400)"
echo
echo "verified: $(cat build/inputs-artifact/inputs-verified.json)"
} | tee -a "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4
with:
name: igneum-windows-installer
path: packaging/windows/dist/Igneum-Miner-Setup-*.exe
retention-days: 90
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: igneum-windows-payload
path: packaging/windows/dist/igneum-windows-app.zip
retention-days: 90
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: igneum-windows-host
path: app/windows/dist/Igneum Miner.exe
retention-days: 90
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: igneum-windows-inputs
path: build/inputs-artifact/
retention-days: 90
if-no-files-found: error
red:
# The red watcher for the Windows pipeline (see ci.yml `red`): one line per failed master or release-* run to the
# hidden updates channel and /srv/ci-red/red.jsonl on the box, recorded by the box's own runner.
name: red watcher (master and release-* only; one line per failed run to the updates channel and the box file)
needs: [parse, build]
if: ${{ failure() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-')) }}
runs-on: [self-hosted, linux, x64, igneum-build-1]
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci
- name: record this run (one line, the failed jobs and their first failed step, from the run's own API)
env:
GITHUB_TOKEN: ${{ github.token }}
RED_WATCH_TITLE: ${{ github.event.head_commit.message }}
run: node tools/ci/red-watch.mjs record --file /srv/ci-red/red.jsonl