Windows CI: the one-click app built on GitHub runners, no PC needed

windows.yml: parse job (every .ps1 through the Windows PowerShell 5.1 parser with a negative fixture, PSScriptAnalyzer
as warnings, parenthesis check of every .bat/.cmd with a negative fixture), build job (engine on the MSVC target,
window host through BUILD-APP.bat as it is, payload through make-payload.sh in Git Bash, installer through
build-installer.ps1, smoke run of both exes, launcher DRY_RUN, three artifacts for 90 days).
push-inputs.sh publishes payload-inputs.zip (node, miner, workers, NVRTC DLLs) to the downloads host from the Mac;
fetch-ci-artifacts.sh pulls the green run's installer and payload back into the downloads folder.
Host: --version and --help, version.h shared with host.rc. Launcher: DRY_RUN=1 prints the plan and starts nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-04 10:32:20 +00:00
parent 5097c0468a
commit 7717c4fdac
13 changed files with 679 additions and 14 deletions

233
.github/workflows/windows.yml vendored Normal file
View file

@ -0,0 +1,233 @@
# 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 Mac side of the loop is packaging/windows/fetch-ci-artifacts.sh (gh run download into the downloads folder).
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/**'
- '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', '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: payload inputs (payload-inputs.zip from the downloads host, sha256 checked)
shell: bash
env:
DL_TOKEN: ${{ secrets.DL_TOKEN }}
run: |
set -euo pipefail
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
base="https://dl.igneum.network/dl/$DL_TOKEN"
mkdir -p build/inputs "$HOME/.config/igneum"
printf '%s' "$DL_TOKEN" > "$HOME/.config/igneum/dl-token" # make-payload.sh reads it for the update manifest URL
curl -fsSL --retry 3 -o build/payload-inputs.json "$base/payload-inputs.json"
curl -fsSL --retry 3 -o build/payload-inputs.sha256 "$base/payload-inputs.sha256"
curl -fsSL --retry 3 -o build/payload-inputs.zip "$base/payload-inputs.zip"
echo "$(tr -d '[:space:]' < build/payload-inputs.sha256) build/payload-inputs.zip" | sha256sum -c -
7z x -y -bso0 -bsp0 -obuild/inputs-unpacked build/payload-inputs.zip
mv build/inputs-unpacked/payload-inputs/* build/inputs/
echo "inputs manifest:"; cat build/payload-inputs.json
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
- 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: $(tr -d '\n' < build/payload-inputs.json | head -c 400)"
} | 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

View file

@ -8,8 +8,12 @@
// Untested on a real PC at the time of writing (written on a Mac): BUILD-APP.bat is the first run.
#define WIN32_LEAN_AND_MEAN
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <shellapi.h>
#include <wrl.h>
@ -18,6 +22,7 @@
#include <thread>
#include <mutex>
#include "WebView2.h"
#include "version.h"
using namespace Microsoft::WRL;
@ -341,7 +346,40 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
return DefWindowProcW(hwnd, msg, wp, lp);
}
// --version and --help print one line and exit, for the CI smoke run and support scripts. A windows-subsystem exe has
// no console of its own: the text goes to the inherited stdout when there is one (a pipe or a file), else to the
// parent's console.
static bool handleCliFlags() {
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (!argv) return false;
std::wstring text;
for (int i = 1; i < argc; i++) {
std::wstring a = argv[i];
if (a == L"--version" || a == L"-V") text = L"Igneum Miner " IGNEUM_HOST_VERSION_STR L" (window host)\r\n";
else if (a == L"--help" || a == L"-h" || a == L"/?")
text = L"Igneum Miner " IGNEUM_HOST_VERSION_STR L" (window host)\r\n"
L"Usage: \"Igneum Miner.exe\" [--version | --help]\r\n"
L"Without flags it starts igneum-app.exe --wrapper next to it and shows the dashboard in a WebView2 window.\r\n";
}
LocalFree(argv);
if (text.empty()) return false;
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
if (out == nullptr || out == INVALID_HANDLE_VALUE) {
if (AttachConsole(ATTACH_PARENT_PROCESS)) out = GetStdHandle(STD_OUTPUT_HANDLE);
}
if (out && out != INVALID_HANDLE_VALUE) {
int n = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0, nullptr, nullptr);
std::string utf8(n, 0);
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), &utf8[0], n, nullptr, nullptr);
DWORD written = 0;
WriteFile(out, utf8.data(), (DWORD)utf8.size(), &written, nullptr);
}
return true;
}
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) {
if (handleCliFlags()) return 0;
HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumMinerWindow");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
HWND other = FindWindowW(L"IgneumMinerWindow", nullptr);

View file

@ -1,12 +1,14 @@
// Resources for Igneum Miner.exe (the window host): the coin icon and the version block.
// Compiled by BUILD-APP.bat with rc.exe; the icon path is relative to brand\icons (passed with /i).
// Compiled by BUILD-APP.bat with rc.exe; the icon path is relative to brand\icons (passed with /i). The version comes
// from version.h, shared with host.cpp.
#include <winver.h>
#include "version.h"
1 ICON "igneum.ico"
1 VERSIONINFO
FILEVERSION 0,3,0,0
PRODUCTVERSION 0,3,0,0
FILEVERSION IGNEUM_HOST_VERSION_RC
PRODUCTVERSION IGNEUM_HOST_VERSION_RC
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
@ -19,12 +21,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Igneum"
VALUE "FileDescription", "Igneum Miner"
VALUE "FileVersion", "0.3.0"
VALUE "FileVersion", IGNEUM_HOST_VERSION_STR
VALUE "InternalName", "Igneum Miner"
VALUE "LegalCopyright", "Igneum. Nothing is bought or sold."
VALUE "OriginalFilename", "Igneum Miner.exe"
VALUE "ProductName", "Igneum Miner"
VALUE "ProductVersion", "0.3.0"
VALUE "ProductVersion", IGNEUM_HOST_VERSION_STR
END
END
BLOCK "VarFileInfo"

8
app/windows/version.h Normal file
View file

@ -0,0 +1,8 @@
// The version of "Igneum Miner.exe" (the window host). One place for host.rc (the version block Explorer shows) and
// host.cpp (--version). Keep it equal to app/igneum-app/Cargo.toml and the AppVersion default in
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
#ifndef IGNEUM_HOST_VERSION_H
#define IGNEUM_HOST_VERSION_H
#define IGNEUM_HOST_VERSION_STR "0.3.0"
#define IGNEUM_HOST_VERSION_RC 0,3,0,0
#endif

View file

@ -1,9 +1,70 @@
# Igneum Miner for Windows (packaging/windows)
[![windows-ci](https://github.com/igneum-network/igneum/actions/workflows/windows.yml/badge.svg?branch=master)](https://github.com/igneum-network/igneum/actions/workflows/windows.yml)
Inno Setup installer around the app payload. Version 0.3.0 (4 October 2026): the app (engine + window host) instead of
the console launchers of 0.1.0.
## On the Mac
## Built on GitHub, not on a PC (4 October 2026)
`.github/workflows/windows.yml` builds the whole Windows app on a hosted Windows runner on every push to master that
touches `app/`, `packaging/windows/`, `proto-cuda/windows-*`, `proto-cuda/nvrtc/`, `proto-opencl/`,
`proving/windows-wsl2/` or `relay/clients/` (and on `gh workflow run windows.yml`). Nobody runs `BUILD-APP.bat` or
`BUILD-INSTALLER.bat` on a PC any more; both files stay for a hand build and the workflow runs them as they are.
Two jobs:
1. `parse` (required): every `.ps1` under the Windows folders through the Windows PowerShell 5.1 parser
(`tools/ci/windows/check-ps51.ps1`, run with `powershell.exe`, the PowerShell on the PCs; it refuses to pass unless
the fixture `tools/ci/windows/fixtures/bad-drive-ref.ps1.txt`, a `"$x: y"` drive-qualified reference, FAILS, so a
green run proves the check bites), PSScriptAnalyzer as warnings, and a parenthesis check of every `.bat` and `.cmd`
(`check-bat.ps1`: caret escapes, quotes and `for /f ('...')` strings ignored, depth never below zero, zero at the
end; its fixture `bad-bare-paren.bat.txt` must be flagged). Both scripts run on a PC too:
`powershell -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-ps51.ps1`.
2. `build`: the engine with `cargo build --release` on the MSVC target (so the Mac cross-build is no longer an input),
the window host with `app\windows\BUILD-APP.bat` as it is (MSVC from the runner's Visual Studio, WebView2 SDK from
NuGet, `host.rc` with the coin icon; `version.h` carries the host version), the payload with `make-payload.sh` in
Git Bash, the installer with `build-installer.ps1` (Inno Setup from the runner image or chocolatey, rcedit), then a
smoke run (`igneum-app.exe --version`, `Igneum Miner.exe --version` and `--help`, the version block of the host),
the launcher `proto-cuda/windows-app/start-igneum.ps1` with `DRY_RUN=1` (prints the node command and the GPU plan,
starts nothing) through the `.ps1` and through `START-IGNEUM.bat`, and three artifacts kept 90 days:
`igneum-windows-installer` (`Igneum-Miner-Setup-<version>.exe`), `igneum-windows-payload`
(`igneum-windows-app.zip`), `igneum-windows-host` (`Igneum Miner.exe`).
### The inputs the runner cannot build
`igneumd.exe` and `igneum-miner.exe` come from the node fork in `vendor/` (not in git, 20 to 55 minutes to build) and
the prebuilt GPU workers need NVIDIA's NVRTC DLLs (90 MB, not in git). They travel as `payload-inputs.zip` on the
downloads host:
packaging/windows/push-inputs.sh # on the Mac, after each node or worker cross-build
collects `igneumd.exe`, `igneum-miner.exe`, the three mingw DLLs, `igneum-worker-cuda.exe`, `nvrtc*.dll`, the licence
texts, `igneum-worker-opencl.exe` and an `inputs.json` (sha256 and bytes of each, the commits, the date), zips them
into `dl/<token>/payload-inputs.zip` with `payload-inputs.sha256` and `payload-inputs.json` next to it in the
downloads folder (`~/.config/igneum/dlsite-dir` names it; `IGNEUM_DLSITE` overrides), and deploys the folder with the
Vercel CLI (`--no-deploy` to skip). The workflow downloads the three with the `DL_TOKEN` repository secret and checks
the sha256. The secret was set once from the Mac and never printed:
tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum
### The outputs, back to the downloads host
The runner cannot deploy the downloads project (it is deployed by CLI from a folder outside the repo with the Igneum
Vercel login), so the Mac pulls the artifacts:
packaging/windows/fetch-ci-artifacts.sh [--deploy] [run-id]
downloads the installer and the payload zip from the latest green run on master (`gh run download`, as igneum-labs),
copies them into `dl/<token>/` next to the Mac-built packages, writes `igneum-windows-ci.json` (run URL, time), and
prints the deploy command, or deploys with `--deploy`.
### What still needs a human
A code-signing certificate. Until Igneum has one, the installer and the exes are unsigned and SmartScreen shows
"Windows protected your PC" (More info, Run anyway). Everything else in this folder runs without a PC.
## On the Mac (hand build, kept for reference)
packaging/windows/make-payload.sh [out.zip]
@ -17,7 +78,7 @@ worker sources for the fallback build (`proto-cuda\`, `proto-opencl\`), `igneum-
token from `~/.config/igneum/dl-token`, log intake, live page), `stop-igneum.ps1`, and `app\windows\` (the window host
sources and `BUILD-APP.bat`, since WebView2 cannot be linked from the Mac).
## On the PC
## On the PC (hand build, kept for reference; CI does all of this)
1. Extract the zip next to `packaging\windows` (or anywhere: `build-installer.ps1 -Payload <folder>`; without a folder it
downloads `dl.igneum.network/igneum-windows-app.zip`).

View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Pulls the Windows installer and payload from the latest green run of .github/workflows/windows.yml on master and
# copies them into the downloads folder (dl/<token>/), where the other packages live. Run on the Mac:
#
# packaging/windows/fetch-ci-artifacts.sh [--deploy] [run-id]
#
# Without --deploy it prints the deploy command for the main session to run; with --deploy it deploys the folder with
# the Vercel CLI itself. A run id (gh run list) picks a specific run instead of the latest green one.
# Reads ~/.config/igneum/dl-token, ~/.config/igneum/dlsite-dir (IGNEUM_DLSITE overrides) and the gh login, which must
# be igneum-labs (gh auth switch --user igneum-labs).
set -euo pipefail
REPO="igneum-network/igneum"
DEPLOY=0
RUN_ID=""
for a in "$@"; do
case "$a" in --deploy) DEPLOY=1 ;; *) RUN_ID="$a" ;; esac
done
TOKEN_FILE="$HOME/.config/igneum/dl-token"
DLSITE="${IGNEUM_DLSITE:-}"
[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true
[ -f "$TOKEN_FILE" ] || { echo "no $TOKEN_FILE" >&2; exit 1; }
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
DEST="$DLSITE/dl/$TOKEN"
[ -n "$DLSITE" ] && [ -d "$DEST" ] || { echo "no downloads folder: set IGNEUM_DLSITE or ~/.config/igneum/dlsite-dir (must hold dl/<token>/)" >&2; exit 1; }
gh auth status 2>&1 | grep -q 'Active account: true' || { echo "gh is not logged in" >&2; exit 1; }
gh auth status 2>&1 | grep -B1 'Active account: true' | grep -q 'igneum-labs' || { echo "gh active account is not igneum-labs: run gh auth switch --user igneum-labs" >&2; exit 1; }
if [ -z "$RUN_ID" ]; then
RUN_ID="$(gh run list --repo "$REPO" --workflow windows.yml --branch master --status success --limit 1 --json databaseId --jq '.[0].databaseId')"
[ -n "$RUN_ID" ] && [ "$RUN_ID" != "null" ] || { echo "no green windows.yml run on master yet" >&2; exit 1; }
fi
gh run view "$RUN_ID" --repo "$REPO" --json headSha,displayTitle,updatedAt,url,conclusion --jq '"run \(.url)\n\(.displayTitle)\n\(.headSha[0:12]) \(.updatedAt) \(.conclusion)"'
TMP="$(mktemp -d)"
gh run download "$RUN_ID" --repo "$REPO" --name igneum-windows-installer --name igneum-windows-payload --dir "$TMP"
SETUP="$(find "$TMP" -name 'Igneum-Miner-Setup-*.exe' | head -1)"
PAYLOAD="$(find "$TMP" -name 'igneum-windows-app.zip' | head -1)"
[ -n "$SETUP" ] && [ -n "$PAYLOAD" ] || { echo "the run has no installer or payload artifact" >&2; ls -R "$TMP"; exit 1; }
cp "$SETUP" "$DEST/"
cp "$PAYLOAD" "$DEST/igneum-windows-app.zip"
cat > "$DEST/igneum-windows-ci.json" <<JSON
{ "run": "https://github.com/$REPO/actions/runs/$RUN_ID", "installer": "$(basename "$SETUP")", "payload": "igneum-windows-app.zip", "fetched_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" }
JSON
rm -rf "$TMP"
echo "copied into $DEST:"
ls -la "$DEST/$(basename "$SETUP")" "$DEST/igneum-windows-app.zip"
if [ "$DEPLOY" = 1 ]; then
(cd "$DLSITE" && npx vercel@latest --global-config "$HOME/.config/igneum/vercel" deploy --prod --yes 2>&1 | grep -v "$TOKEN" || true)
echo "live: https://dl.igneum.network/dl/<token>/$(basename "$SETUP")"
else
echo "deploy: cd $DLSITE && npx vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes"
fi

View file

@ -14,12 +14,18 @@
# proto-cuda\, proto-opencl\ the worker sources and build.bat for that fallback
# igneum-app.json the update manifest URL (token from ~/.config/igneum/dl-token, never in the repo), the log intake
# stop-igneum.ps1 what the installer runs before an upgrade and on uninstall
# app\windows\ host.cpp, host.rc, BUILD-APP.bat (the window host is built on the PC)
# app\windows\ host.cpp, host.rc, version.h, BUILD-APP.bat (the window host is built on the PC or by CI)
# Igneum Miner.exe the window host, when app/windows/dist/ holds one (BUILD-APP.bat ran before this script)
# On GitHub (.github/workflows/windows.yml) the same script runs in Git Bash with IGNEUM_WIN_RELEASE and
# IGNEUM_WORKERS_DIR pointing at the unpacked payload-inputs.zip (push-inputs.sh) and IGNEUM_APP_EXE at the engine
# built on the runner; the mingw DLLs are taken from the release folder there, zip falls back to 7z.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
VERSION="0.3.0"
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "$ROOT/app/igneum-app/Cargo.toml" | head -1)"
OUT="${1:-$HOME/Desktop/igneum-windows-app.zip}"
case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
mkdir -p "$(dirname "$OUT")"
REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release}"
ENGINE="${IGNEUM_APP_EXE:-$ROOT/app/igneum-app/target/x86_64-pc-windows-gnu/release/igneum-app.exe}"
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
@ -37,7 +43,8 @@ cp "$REL/igneumd.exe" "$STAGE/igneumd.exe"
cp "$REL/igneum-miner.exe" "$STAGE/igneum-miner.exe"
for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do
name="$(basename "$dll")"
if [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true
if [ -f "$REL/$name" ]; then cp "$REL/$name" "$STAGE/" # push-inputs.sh ships them next to the exes (CI)
elif [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true
else echo "note: $name not found in the mingw toolchain (the node exe is linked -static, so it may not need it)"; fi
done
@ -63,9 +70,10 @@ else echo "note: no prebuilt igneum-worker-cuda.exe / igneum-worker-opencl.exe f
# the worker sources for the fallback build
cp "$ROOT/proto-cuda/host.cu" "$ROOT/proto-cuda/build.bat" "$ROOT/proto-cuda/README.md" "$STAGE/proto-cuda/"
cp "$ROOT/proto-opencl/host.c" "$ROOT/proto-opencl/build.bat" "$ROOT/proto-opencl/README.md" "$STAGE/proto-opencl/"
# the window host sources, built on the PC
cp "$ROOT/app/windows/host.cpp" "$ROOT/app/windows/host.rc" "$ROOT/app/windows/BUILD-APP.bat" "$STAGE/app/windows/"
# the window host sources, built on the PC or by CI; the built host when BUILD-APP.bat already ran here
cp "$ROOT/app/windows/host.cpp" "$ROOT/app/windows/host.rc" "$ROOT/app/windows/version.h" "$ROOT/app/windows/BUILD-APP.bat" "$STAGE/app/windows/"
mkdir -p "$STAGE/app/windows/art" && cp "$ROOT/brand/icons/igneum.ico" "$STAGE/app/windows/art/"
if [ -f "$ROOT/app/windows/dist/Igneum Miner.exe" ]; then cp "$ROOT/app/windows/dist/Igneum Miner.exe" "$STAGE/"; echo "window host: Igneum Miner.exe from app/windows/dist"; fi
cp "$HERE/stop-igneum.ps1" "$STAGE/stop-igneum.ps1"
# the packaged configuration: the download token stays out of the repo
@ -96,7 +104,10 @@ for f in "$STAGE"/*.bat "$STAGE/README.txt" "$STAGE/stop-igneum.ps1" "$STAGE/app
[ -f "$f" ] && perl -pi -e 's/\r?\n/\r\n/' "$f"
done
rm -f "$OUT"
(cd "$HERE" && zip -qr "$OUT" "igneum-windows-app" -x '*.DS_Store')
if command -v zip >/dev/null 2>&1; then (cd "$HERE" && zip -qr "$OUT" "igneum-windows-app" -x '*.DS_Store')
elif command -v 7z >/dev/null 2>&1; then (cd "$HERE" && 7z a -tzip -bso0 -bsp0 "$OUT" "igneum-windows-app" '-xr!.DS_Store')
else echo "neither zip nor 7z is on PATH" >&2; exit 1; fi
echo "staged $STAGE"
ls -la "$OUT"
unzip -l "$OUT" | tail -n +4 | awk '{print $1, $4}' | sed '/^ *$/d' | head -40
if command -v unzip >/dev/null 2>&1; then unzip -l "$OUT" | tail -n +4 | awk '{print $1, $4}' | sed '/^ *$/d' | head -40
else (cd "$STAGE" && find . -type f | sort | head -40); fi

View file

@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Publishes payload-inputs.zip: the pieces of the Windows app that the GitHub build (.github/workflows/windows.yml)
# cannot make on a hosted runner, because they come from the node fork (vendor/, not in git) or from NVIDIA's
# redistributables (large, not in git). Run it on the Mac after every node or worker cross-build:
#
# packaging/windows/push-inputs.sh [--no-deploy]
#
# What goes in (flat): igneumd.exe, igneum-miner.exe (vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/
# release, IGNEUM_WIN_RELEASE overrides), the three mingw runtime DLLs, igneum-worker-cuda.exe with nvrtc64_*_0.dll,
# nvrtc-builtins64_*.dll and the licence texts (proto-cuda/nvrtc, fetch-redist.sh + build-windows.sh),
# igneum-worker-opencl.exe (proto-opencl), and inputs.json (sha256 and size of each file, the commits, the date).
# The zip, its .sha256 and the .json land in the downloads folder (dl/<token>/) and the folder is deployed with the
# Vercel CLI, exactly as the other packages are. The workflow fetches them with the DL_TOKEN repository secret and
# refuses a zip whose sha256 does not match.
#
# Where things are read from (never in the repo): the token in ~/.config/igneum/dl-token, the downloads folder in
# ~/.config/igneum/dlsite-dir (one line, the path of the dlsite directory; IGNEUM_DLSITE overrides), the Vercel login
# in ~/.config/igneum/vercel.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release}"
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
CL_WORKER="$ROOT/proto-opencl/igneum-worker-opencl.exe"
TOKEN_FILE="$HOME/.config/igneum/dl-token"
DLSITE="${IGNEUM_DLSITE:-}"
[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true
DEPLOY=1
[ "${1:-}" = "--no-deploy" ] && DEPLOY=0
[ -f "$TOKEN_FILE" ] || { echo "no $TOKEN_FILE (the downloads token)" >&2; exit 1; }
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
[ -n "$DLSITE" ] && [ -d "$DLSITE/dl/$TOKEN" ] || { echo "no downloads folder: set IGNEUM_DLSITE or write the dlsite path to ~/.config/igneum/dlsite-dir (it must hold dl/<token>/)" >&2; exit 1; }
[ -f "$REL/igneumd.exe" ] || { echo "no $REL/igneumd.exe; cross-compile the node first (proto-cuda/windows-node/cross-build.sh)" >&2; exit 1; }
[ -f "$REL/igneum-miner.exe" ] || { echo "no $REL/igneum-miner.exe; cross-compile the miner first" >&2; exit 1; }
STAGE="$(mktemp -d)/payload-inputs"
mkdir -p "$STAGE"
cp "$REL/igneumd.exe" "$REL/igneum-miner.exe" "$STAGE/"
for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do
name="$(basename "$dll")"
if [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true
else echo "note: $name not in the mingw toolchain (the node exe is linked -static, so it may not need it)"; fi
done
if [ -f "$NVRTC_DIR/igneum-worker-cuda.exe" ]; then
cp "$NVRTC_DIR/igneum-worker-cuda.exe" "$STAGE/"
for f in "$NVRTC_DIR"/redist/bin/nvrtc*.dll "$NVRTC_DIR"/redist/LICENSE-*.txt "$NVRTC_DIR/THIRD-PARTY.md"; do [ -f "$f" ] && cp "$f" "$STAGE/"; done
ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh)"
else echo "warning: no $NVRTC_DIR/igneum-worker-cuda.exe (run $NVRTC_DIR/build-windows.sh); the app will build the CUDA worker on the PC"; fi
[ -f "$CL_WORKER" ] && cp "$CL_WORKER" "$STAGE/" || echo "warning: no $CL_WORKER"
# the manifest: what is in the zip, from where, when
NODE_COMMIT="$(git -C "$ROOT/vendor/igneum-node-v4" rev-parse --short HEAD 2>/dev/null || git -C "$ROOT/vendor/igneum-node" rev-parse --short HEAD 2>/dev/null || echo unknown)"
REPO_COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
{
echo '{'
echo " \"built_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
echo " \"node_source_commit\": \"$NODE_COMMIT\","
echo " \"repo_commit\": \"$REPO_COMMIT\","
echo ' "files": {'
first=1
for f in "$STAGE"/*; do
name="$(basename "$f")"
sum="$(shasum -a 256 "$f" | cut -d' ' -f1)"
bytes="$(stat -f %z "$f")"
[ $first = 1 ] || echo ','
first=0
printf ' "%s": { "sha256": "%s", "bytes": %s }' "$name" "$sum" "$bytes"
done
echo
echo ' }'
echo '}'
} > "$STAGE/inputs.json"
DEST="$DLSITE/dl/$TOKEN"
OUT="$DEST/payload-inputs.zip"
rm -f "$OUT"
(cd "$(dirname "$STAGE")" && zip -qr "$OUT" "payload-inputs" -x '*.DS_Store')
shasum -a 256 "$OUT" | awk '{print $1}' > "$DEST/payload-inputs.sha256"
cp "$STAGE/inputs.json" "$DEST/payload-inputs.json"
echo "payload-inputs.zip: $(stat -f %z "$OUT") bytes, sha256 $(cat "$DEST/payload-inputs.sha256")"
cat "$DEST/payload-inputs.json"
rm -rf "$(dirname "$STAGE")"
if [ "$DEPLOY" = 1 ]; then
echo "deploying $DLSITE"
(cd "$DLSITE" && npx vercel@latest --global-config "$HOME/.config/igneum/vercel" deploy --prod --yes 2>&1 | grep -v "$TOKEN" || true)
code="$(curl -s -o /dev/null -w '%{http_code}' "https://dl.igneum.network/dl/$TOKEN/payload-inputs.json")"
echo "https://dl.igneum.network/dl/<token>/payload-inputs.json -> HTTP $code"
[ "$code" = 200 ] || { echo "the manifest is not reachable yet; check the deploy output" >&2; exit 1; }
else
echo "not deployed (--no-deploy): cd $DLSITE && npx vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes"
fi
echo "Next: the GitHub build picks it up on the next push to master (or: gh workflow run windows.yml --repo igneum-network/igneum)."

View file

@ -23,6 +23,17 @@ Set-NodeTarget $rpcHost $rpcPort # the miners always dial the node on this PC
Log "igneum launcher $packageVersion ($chainVersion) on $machine (run $stamp): node $rpcListen, p2p $p2pListen, peers $($peerNodes -join ' '), appdir $appDir, unsynced mining $(if ($unsyncedMining) { 'on' } else { 'off' }), identities $minersPerVendor per vendor"
Log "If Windows Firewall asks about igneumd.exe: tick Private networks and click Allow access (the Mac node dials this PC on port $p2pPort). If you clicked Cancel once, run ALLOW-FIREWALL.bat."
# DRY_RUN=1 (CI, .github/workflows/windows.yml; or a quick check on a PC): print the plan and start nothing.
if ($env:DRY_RUN -eq '1') {
Log 'DRY_RUN=1: printing the plan, starting nothing'
Log "node command: `"$nodeExe`" $((Get-NodeArgs) -join ' ')"
Log "node binary present: $(Test-Path $nodeExe); miner binary present: $(Test-Path $miner)"
$plan = Find-Hardware
Log "miners to start: $(if ($plan.Count) { $plan -join ', ' } else { 'none' }) ($minersPerVendor identities per vendor, payout $(if ($payoutEvm) { $payoutEvm } else { 'derived from the PC name' }))"
Log 'dry run done'
exit 0
}
# ---- 1. the node ----------------------------------------------------------------------------------------------------------
$script:node.on = $true

View file

@ -0,0 +1,69 @@
# Static check of every .bat and .cmd under the given folders for the parenthesis bug class seen on 3 October 2026:
# a bare ")" that closes nothing, or an "if (...) (" block left open. cmd.exe has no dry-parse mode, so this mirrors
# what its parser does with parentheses: caret escapes (^( and ^)) are not parentheses, nothing inside double quotes
# or a for /f ('...') command string counts, rem and :: lines are comments. The depth is tracked across lines; it must
# never go below zero and must be zero at the end of the file. Prints file:line:message and exits 1 on any finding.
#
# A built-in negative test checks fixtures\bad-bare-paren.bat.txt first and refuses to pass unless it is flagged.
#
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-bat.ps1 [-Root <repo>]
param(
[string]$Root = '',
[string[]]$Folders = @('proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node',
'proving/windows-wsl2', 'relay/clients', 'packaging/windows', 'app/windows',
'proto-cuda', 'proto-opencl')
)
$ErrorActionPreference = 'Stop'
if (-not $Root) { $Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path }
function Check-Batch([string]$path) {
$findings = @()
$depth = 0
$n = 0
foreach ($raw in [IO.File]::ReadAllLines($path)) {
$n += 1
$line = $raw.TrimEnd("`r")
if ($line -match '^\s*(@?rem\b|::)') { continue }
$s = $line -replace '\^.', '' # caret escapes: ^( ^) ^> ^< ^& ^|
$s = $s -replace '"[^"]*"', '' # double-quoted strings
$s = $s -replace "'[^']*'", '' # for /f ('command') strings
foreach ($ch in $s.ToCharArray()) {
if ($ch -eq '(') { $depth += 1 }
elseif ($ch -eq ')') {
$depth -= 1
if ($depth -lt 0) { $findings += @{ line = $n; msg = "')' closes nothing (bare parenthesis)" }; $depth = 0 }
}
}
}
if ($depth -gt 0) { $findings += @{ line = $n; msg = "$depth unclosed '(' at the end of the file" } }
return ,$findings
}
# ---- 1. the negative test ----
$fixture = Join-Path $PSScriptRoot 'fixtures\bad-bare-paren.bat.txt'
$fx = Check-Batch $fixture
if ($fx.Count -eq 0) { Write-Host "::error::self-test failed: $fixture was not flagged"; exit 2 }
Write-Host ("self-test: fixture bad-bare-paren.bat.txt is flagged as expected at line {0}: {1}" -f $fx[0].line, $fx[0].msg)
# ---- 2. every .bat and .cmd ----
$files = @()
foreach ($f in $Folders) {
$dir = Join-Path $Root $f
if (-not (Test-Path $dir)) { Write-Host "note: no folder $f"; continue }
$files += Get-ChildItem -Path $dir -Recurse -Include '*.bat', '*.cmd' -File | Where-Object { $_.FullName -notmatch '\\igneum-windows-app\\|\\build\\|\\dist\\|\\packs\\|\\emu\\|\\nvrtc\\redist\\' }
}
$files = $files | Sort-Object FullName -Unique
$bad = 0
foreach ($file in $files) {
$rel = $file.FullName.Substring($Root.Length).TrimStart('\', '/')
$found = Check-Batch $file.FullName
if ($found.Count -eq 0) { Write-Host "ok $rel"; continue }
foreach ($x in $found) {
$bad += 1
Write-Host "FAIL ${rel}:$($x.line): $($x.msg)"
Write-Host "::error file=$($rel -replace '\\', '/'),line=$($x.line)::$($x.msg)"
}
}
Write-Host ("{0} batch files checked, {1} findings" -f $files.Count, $bad)
if ($bad -gt 0) { exit 1 }
exit 0

View file

@ -0,0 +1,68 @@
# Parses every .ps1 under the given folders with the PowerShell parser of the PowerShell that runs this script and
# fails on any parse error, printed as file:line:message. Run it under Windows PowerShell 5.1 (powershell.exe), the
# PowerShell on the project lead's PCs: 5.1 rejects "$name: text" (a drive-qualified variable reference) where newer parsers may
# not, and that class caused the two launcher failures of 4 October 2026 (commits 82275e6 and 018f739).
#
# A built-in negative test parses fixtures\bad-drive-ref.ps1.txt first and refuses to pass unless that file FAILS with
# the 5.1 message, so a green run proves the check bites.
#
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-ps51.ps1 [-Root <repo>] [-AnyVersion]
param(
[string]$Root = '',
[string[]]$Folders = @('proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node',
'proving/windows-wsl2', 'relay/clients', 'packaging/windows', 'app/windows', 'tools/ci/windows'),
[switch]$AnyVersion
)
$ErrorActionPreference = 'Stop'
if (-not $Root) { $Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path }
$v = $PSVersionTable.PSVersion
Write-Host "PowerShell $v ($($PSVersionTable.PSEdition))"
if ($v.Major -ne 5 -and -not $AnyVersion) {
Write-Host "::error::this check must run under Windows PowerShell 5.1 (powershell.exe), not PowerShell $v; pass -AnyVersion to override"
exit 2
}
function Parse-File([string]$path) {
$tokens = $null
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors)
return ,@($errors)
}
# ---- 1. the negative test: the fixture with "$x: y" must fail to parse ----
$fixture = Join-Path $PSScriptRoot 'fixtures\bad-drive-ref.ps1.txt'
$fixtureErrors = Parse-File $fixture
if ($fixtureErrors.Count -eq 0) {
Write-Host "::error::self-test failed: $fixture parsed without errors, so this parser does not catch the 5.1 drive-qualified variable bug"
exit 2
}
$msg = $fixtureErrors[0].Message
Write-Host ("self-test: fixture bad-drive-ref.ps1.txt fails as expected at line {0}: {1}" -f $fixtureErrors[0].Extent.StartLineNumber, $msg)
if ($msg -notmatch 'Variable reference is not valid') {
Write-Host "::error::self-test: the fixture failed, but not with the 'Variable reference is not valid' message (got: $msg)"
exit 2
}
# ---- 2. every .ps1 under the folders ----
$files = @()
foreach ($f in $Folders) {
$dir = Join-Path $Root $f
if (-not (Test-Path $dir)) { Write-Host "note: no folder $f"; continue }
$files += Get-ChildItem -Path $dir -Recurse -Filter '*.ps1' -File | Where-Object { $_.FullName -notmatch '\\igneum-windows-app\\|\\build\\|\\dist\\' }
}
$bad = 0
foreach ($file in ($files | Sort-Object FullName)) {
$rel = $file.FullName.Substring($Root.Length).TrimStart('\', '/')
$errs = Parse-File $file.FullName
if ($errs.Count -eq 0) { Write-Host "ok $rel"; continue }
foreach ($e in $errs) {
$bad += 1
$line = $e.Extent.StartLineNumber
$col = $e.Extent.StartColumnNumber
Write-Host "FAIL ${rel}:${line}:${col}: $($e.Message)"
Write-Host "::error file=$($rel -replace '\\', '/'),line=$line,col=${col}::$($e.Message)"
}
}
Write-Host ("{0} files parsed, {1} parse errors" -f $files.Count, $bad)
if ($bad -gt 0) { exit 1 }
exit 0

View file

@ -0,0 +1,10 @@
@echo off
rem Negative fixture for check-bat.ps1 (named .txt so nothing runs it). The bare ")" on line 9 closes nothing:
rem cmd.exe reports ") was unexpected at this time." and stops. Seen on 3 October 2026 in a launcher bat.
if not exist "%~dp0igneumd.exe" (
echo igneumd.exe is missing ^(extract the whole zip^).
exit /b 1
)
echo ready
)
echo never reached

View file

@ -0,0 +1,7 @@
# Negative fixture for check-ps51.ps1 (named .txt so no PowerShell glob picks it up). Windows PowerShell 5.1 must
# refuse this file: "$machine: text" is read as a drive-qualified variable reference, the bug class of commits
# 82275e6 and 018f739 on 4 October 2026. The fix in real code is "${machine}: text".
$machine = $env:COMPUTERNAME
$vendorName = 'nvidia'
$label = "igneum-evm-payout-v0:$machine:$vendorName"
Write-Host "$label: derived"