Prepare NetX 0.4.14 Windows release

This commit is contained in:
oliver 2026-10-12 15:54:53 +08:00
parent af8d5a8d37
commit a5c9a8f58d
10 changed files with 320 additions and 42 deletions

View file

@ -38,11 +38,18 @@ jobs:
shell: pwsh shell: pwsh
run: | run: |
$ver = (Get-Content pyproject.toml | Select-String '^\s*version\s*=').Line -replace '.*"(.+)"','$1' $ver = (Get-Content pyproject.toml | Select-String '^\s*version\s*=').Line -replace '.*"(.+)"','$1'
if ($env:GITHUB_REF_NAME -ne "v$ver") { throw "Release tag $env:GITHUB_REF_NAME does not match pyproject version $ver" }
ISCC.exe "/DMyAppVersion=$ver" packaging/installer/netx.iss ISCC.exe "/DMyAppVersion=$ver" packaging/installer/netx.iss
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" }
$setup = "packaging/release/NetX-Setup-$ver.exe"
if (-not (Test-Path $setup) -or (Get-Item $setup).Length -lt 1MB) { throw "Missing or invalid installer: $setup" }
$hash = (Get-FileHash $setup -Algorithm SHA256).Hash.ToLowerInvariant()
Set-Content "$setup.sha256" -Value $hash -Encoding ascii -NoNewline
- name: Upload to GitHub Release - name: Upload to GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
packaging/release/NetX-Setup-*.exe packaging/release/NetX-Setup-*.exe
packaging/release/NetX-Setup-*.exe.sha256
generate_release_notes: true generate_release_notes: true

View file

@ -1,4 +1,4 @@
# NetX Windows packaging # NetX Windows packaging
**Linux is unchanged:** keep using your own PostgreSQL and `NETX_DATABASE_URL` with `scripts/start_netx.sh`. Nothing under this folder is required on Linux. **Linux is unchanged:** keep using your own PostgreSQL and `NETX_DATABASE_URL` with `scripts/start_netx.sh`. Nothing under this folder is required on Linux.
@ -20,20 +20,20 @@ Zip packages are **not** published. Releases ship **Setup.exe only**.
**Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`. **Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`.
**Offline:** Setup ships portable PostgreSQL, **`python/runtime` + `.venv`** (portable; not tied to the build PC’s user profile), and WinSW. **First install** shows the Database page (built-in or external PostgreSQL; external credentials validated with `psql SELECT 1`). **Re-running Setup when `%ProgramData%\NetX\.env` already has `NETX_DB_MODE`** skips the DB wizard and updates program files in place — **no GitHub/EDB download on the target PC**. Service install uses bundled WinSW (pass `-AllowDownload` only on a build/dev machine if the binary is missing). Auto-update still needs network later, and is unchecked by default. **Offline:** Setup ships portable PostgreSQL, **`python/runtime` + `.venv`** (portable; not tied to the build PC’s user profile), and WinSW. **First install** shows the Database page (built-in or external PostgreSQL; external credentials validated with `psql SELECT 1`). **Re-running Setup when `%ProgramData%\NetX\.env` has `NETX_DB_MODE` or a non-empty legacy `NETX_DATABASE_URL`** skips the DB wizard by default. Update stops and resumes an installed Windows service or startup task while replacing files. Service install uses bundled WinSW. Auto-update still needs network later, and is unchecked by default.
**OS:** Windows 10/11 or Windows Server **2016+** recommended. Packaging scripts are UTF-8 **with BOM** so Chinese UI works on Windows PowerShell 5.x. Bundled Python in current releases is **3.13+**, which does **not** support Windows Server 2012 R2 — use Server 2016+ or a newer desktop OS. **OS:** Windows 10/11 or Windows Server **2016+** recommended. Packaging scripts are UTF-8 **with BOM** so Chinese UI works on Windows PowerShell 5.x. The Windows release workflow pins bundled Python to **3.12**, so local release builds should use the same version for consistent artifacts.
## Build (developer machine) ## Build (developer machine)
Prerequisites: Python 3.11+, Node 20+, PowerShell, network (to download PG binaries once). Prerequisites: Python **3.12.x**, Node 20+, PowerShell, network (to download PostgreSQL binaries once).
```powershell ```powershell
cd netx cd netx
# Optional: download portable Postgres into packaging\postgres\pgsql # Required once per clean build machine: download portable PostgreSQL into packaging\postgres\pgsql
powershell -ExecutionPolicy Bypass -File .\packaging\download_postgres.ps1 powershell -ExecutionPolicy Bypass -File .\packaging\download_postgres.ps1
# Build web + stage (-CreateVenv ships a ready .venv; large). No zip by default. # Build web + offline stage containing Python, .venv, PostgreSQL and WinSW. No zip by default.
powershell -ExecutionPolicy Bypass -File .\packaging\build_release.ps1 -CreateVenv powershell -ExecutionPolicy Bypass -File .\packaging\build_release.ps1 -CreateVenv
``` ```
@ -118,15 +118,14 @@ Stops services, replaces program folders (`netx_api`, `web`, `packaging`, `postg
.\packaging\check_update.ps1 -Apply .\packaging\check_update.ps1 -Apply
``` ```
Both sides should publish the same **Setup.exe**. `check_update.ps1` prefers `NetX-Setup-*.exe` (legacy `win64.zip` still works if present). **Code mirror alone is not enough** — Forgejo pull-mirror syncs git/tags only; Release assets must be uploaded separately (or clients can only fall back to GitHub). Both release hosts must publish the same **Setup.exe and `.sha256` sidecar**. `check_update.ps1` prefers `NetX-Setup-*.exe` (legacy `win64.zip` is still accepted); it verifies the Setup against a manifest hash or the adjacent checksum asset. Old releases without a sidecar remain supported with a warning. **Code mirror alone is not enough** — Forgejo pull-mirror syncs git/tags only; release assets are uploaded separately.
### Maintainer release checklist (Windows) ### Maintainer release checklist (Windows)
Do this on the **dev PC on the home LAN** after bumping version: Release from a clean, version-matched checkout in this order:
1. **Build** — `.\packaging\build_release.ps1 -CreateVenv` + Inno Setup → `NetX-Setup-*.exe` 1. **Build and publish GitHub** — ` .\packaging\publish_release.ps1 -Version x.y.z` builds, verifies and uploads Setup.exe plus SHA-256.
2. **GitHub** — `.\packaging\publish_release.ps1 -Version x.y.z` (uploads Setup.exe only) 2. **Forgejo** — publish the same Setup.exe and checksum sidecar (default: public domain `https://git.avelo.top`):
3. **Forgejo** — upload the same Setup.exe (default: public domain `https://git.avelo.top`):
```powershell ```powershell
# One-time: User env var (never commit the token) # One-time: User env var (never commit the token)
@ -148,7 +147,7 @@ Do this on the **dev PC on the home LAN** after bumping version:
| Update probe (default) | GitHub + `https://git.avelo.top/.../releases/latest` | | Update probe (default) | GitHub + `https://git.avelo.top/.../releases/latest` |
Verify: Verify:
- Confirm each release has `NetX-Setup-x.y.z.exe` and `NetX-Setup-x.y.z.exe.sha256`.
- https://git.avelo.top/hansjone/netx/releases - https://git.avelo.top/hansjone/netx/releases
- Probe: `.\packaging\check_update.ps1` → both `github` and `forgejo` reachable with the new version - Probe: `.\packaging\check_update.ps1` → both `github` and `forgejo` reachable with the new version

View file

@ -107,7 +107,7 @@ foreach ($name in @(
"download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1", "download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1",
"stop_netx_app.ps1", "update_netx.ps1", "check_update.ps1", "stop_netx_app.ps1", "update_netx.ps1", "check_update.ps1",
"launch_shortcut.ps1", "netx_tray.ps1", "install_autostart.ps1", "install_service.ps1", "launch_shortcut.ps1", "netx_tray.ps1", "install_autostart.ps1", "install_service.ps1",
"service_run.ps1", "install_update_task.ps1", "uninstall_prepare.ps1", "uninstall_delete_data.ps1", "sign_release.ps1", "service_run.ps1", "service_lifecycle.ps1", "install_update_task.ps1", "uninstall_prepare.ps1", "uninstall_delete_data.ps1", "sign_release.ps1",
"build_release.ps1", "publish_release.ps1", "publish_forgejo_release.ps1", "README.md", "manifest.example.json" "build_release.ps1", "publish_release.ps1", "publish_forgejo_release.ps1", "README.md", "manifest.example.json"
)) { )) {
$src = Join-Path $PSScriptRoot $name $src = Join-Path $PSScriptRoot $name
@ -173,7 +173,9 @@ foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd", "
if ($CreateVenv) { if ($CreateVenv) {
Write-Host "==> Creating portable python/runtime + .venv (must not reference build-machine paths)" Write-Host "==> Creating portable python/runtime + .venv (must not reference build-machine paths)"
$pyPath = Get-NetxSystemPython $pyPath = Get-NetxSystemPython
if (-not $pyPath) { throw "python_not_found_for_venv: need Python 3.11+ (not WindowsApps stub)" } if (-not $pyPath) { throw "python_not_found_for_venv: need Python 3.12.x (not WindowsApps stub)" }
$pyVersion = (& $pyPath -c "import sys; print('%d.%d' % sys.version_info[:2])" 2>$null | Out-String).Trim()
if ($pyVersion -ne '3.12') { throw "python_version_mismatch: Windows releases require Python 3.12.x for reproducible runtimes; found $pyVersion at $pyPath" }
Write-Host " Build Python: $pyPath" Write-Host " Build Python: $pyPath"
$prefix = (& $pyPath -c "import sys; print(sys.base_prefix)" 2>$null | Out-String).Trim() $prefix = (& $pyPath -c "import sys; print(sys.base_prefix)" 2>$null | Out-String).Trim()
@ -250,4 +252,4 @@ Write-Host ""
Write-Host "Ship:" Write-Host "Ship:"
Write-Host " Exe: compile packaging\installer\netx.iss with Inno Setup (needs stage above)" Write-Host " Exe: compile packaging\installer\netx.iss with Inno Setup (needs stage above)"
Write-Host " (Zip packages are not published; use -CreateZip only for local testing.)" Write-Host " (Zip packages are not published; use -CreateZip only for local testing.)"
Write-Host "Python: install 3.11+ on target, or rebuild with -CreateVenv and ship .venv" Write-Host "Python: bundled python/runtime + .venv; target PCs do not need Python installed"

View file

@ -376,13 +376,43 @@ try {
} }
Invoke-WebRequest -Uri $result.download_url -OutFile $pkgPath -Headers $dlHeaders -UseBasicParsing Invoke-WebRequest -Uri $result.download_url -OutFile $pkgPath -Headers $dlHeaders -UseBasicParsing
} }
if ($result.sha256 -and $result.sha256 -notmatch 'REPLACE' -and $result.sha256.Trim()) { $expectedHash = [string]$result.sha256
$hash = (Get-FileHash -Path $pkgPath -Algorithm SHA256).Hash.ToLowerInvariant() if (-not $expectedHash -or $expectedHash -match 'REPLACE') {
$expect = $result.sha256.ToLowerInvariant() $checksumUri = [uri]$result.download_url
if ($hash -ne $expect) { $checksumBuilder = New-Object -TypeName System.UriBuilder -ArgumentList $checksumUri
throw "sha256_mismatch: got $hash expected $expect" $checksumBuilder.Path = $checksumBuilder.Path + '.sha256'
$checksumBuilder.Query = ''
$checksumUrl = $checksumBuilder.Uri.AbsoluteUri
$checksumHeaders = @{ 'User-Agent' = 'NetX-UpdateCheck' }
$isChecksumGithub = ($checksumUri.Host -match '(^|\.)github\.com$' -or $checksumUri.Host -match '(^|\.)githubusercontent\.com$')
if ($UpdateToken -and -not $isChecksumGithub) { $checksumHeaders['Authorization'] = "token $UpdateToken" }
$checksumFile = "$pkgPath.sha256"
try {
Invoke-WebRequest -Uri $checksumUrl -OutFile $checksumFile -Headers $checksumHeaders -UseBasicParsing
$expectedHash = (Get-Content -LiteralPath $checksumFile -Raw).Trim() -split '\s+' | Select-Object -First 1
} catch {
$statusCode = 0
if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
$statusCode = [int]$_.Exception.Response.StatusCode
}
if ($statusCode -eq 404) {
Write-Host "[WARN] Release has no SHA-256 sidecar; accepting legacy asset without checksum verification." -ForegroundColor Yellow
} else {
throw
}
} finally {
Remove-Item -LiteralPath $checksumFile -Force -ErrorAction SilentlyContinue
} }
Write-Host "==> SHA256 OK" }
if ($expectedHash) {
$expectedHash = $expectedHash.Trim().ToLowerInvariant()
if ($expectedHash -notmatch '^[0-9a-f]{64}$') { throw 'invalid_sha256_metadata' }
$actualHash = (Get-FileHash -LiteralPath $pkgPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -ne $expectedHash) {
Remove-Item -LiteralPath $pkgPath -Force -ErrorAction SilentlyContinue
throw "sha256_mismatch: got $actualHash expected $expectedHash"
}
Write-Host '==> SHA256 OK'
} }
# Program Files installs need admin; elevate once (avoid loop via NETX_UPDATE_ELEVATED). # Program Files installs need admin; elevate once (avoid loop via NETX_UPDATE_ELEVATED).
@ -422,10 +452,29 @@ try {
& powershell -NoProfile -ExecutionPolicy Bypass -File $repairPs1 ` & powershell -NoProfile -ExecutionPolicy Bypass -File $repairPs1 `
-ProgramRoot $prog -DataRoot $data -ProgramRoot $prog -DataRoot $data
} }
Write-Host "==> Restarting NetX after Setup" $managedRunnerActive = $false
& (Join-Path $PSScriptRoot "start_netx_app.ps1") ` $netxService = Get-Service -Name "NetX" -ErrorAction SilentlyContinue
-ProgramRoot $prog -DataRoot $data -SkipBrowser if ($netxService -and $netxService.Status -eq "Running") { $managedRunnerActive = $true }
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not $managedRunnerActive) {
try {
$netxTask = Get-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" -ErrorAction Stop
$managedRunnerActive = ([string]$netxTask.State -eq "Running")
} catch {}
}
if ($managedRunnerActive) {
Write-Host "==> Waiting for the previously running NetX service/task"
$netxPort = 8890
$netxEnv = Read-DotEnv -Path (Join-Path $data '.env')
if ($netxEnv['NETX_PORT']) { try { $netxPort = [int]$netxEnv['NETX_PORT'] } catch {} }
if (-not (Wait-NetxApiHealthy -HostName "127.0.0.1" -Port $netxPort -TimeoutSec 180)) {
throw "updated_service_unhealthy: NetX service/task was resumed but /health did not become ready"
}
} else {
Write-Host "==> Restarting NetX after Setup"
& (Join-Path $PSScriptRoot "start_netx_app.ps1") `
-ProgramRoot $prog -DataRoot $data -SkipBrowser
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
} else { } else {
Write-Host "==> Applying legacy zip update via update_netx.ps1" Write-Host "==> Applying legacy zip update via update_netx.ps1"
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") ` & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") `

View file

@ -5,7 +5,7 @@
#define MyAppName "NetX" #define MyAppName "NetX"
#ifndef MyAppVersion #ifndef MyAppVersion
#define MyAppVersion "0.4.13" #define MyAppVersion "0.4.14"
#endif #endif
#define MyAppPublisher "NetX" #define MyAppPublisher "NetX"
#define MyAppURL "https://github.com/hansjone/netx" #define MyAppURL "https://github.com/hansjone/netx"
@ -140,6 +140,9 @@ Source: "{#StageDir}\postgres\pgsql\bin\liblz4.dll"; Flags: dontcopy
Source: "{#StageDir}\postgres\pgsql\bin\libzstd.dll"; Flags: dontcopy Source: "{#StageDir}\postgres\pgsql\bin\libzstd.dll"; Flags: dontcopy
Source: "{#StageDir}\postgres\pgsql\bin\libxml2.dll"; Flags: dontcopy Source: "{#StageDir}\postgres\pgsql\bin\libxml2.dll"; Flags: dontcopy
Source: "{#StageDir}\postgres\pgsql\bin\libxslt.dll"; Flags: dontcopy Source: "{#StageDir}\postgres\pgsql\bin\libxslt.dll"; Flags: dontcopy
; These helpers are extracted for PrepareToInstall, before the new program tree is copied.
Source: "{#StageDir}\packaging\_common.ps1"; Flags: dontcopy
Source: "{#StageDir}\packaging\service_lifecycle.ps1"; Flags: dontcopy
[Dirs] [Dirs]
; users-modify so tray / non-admin reconfigure can update .env and runtime files. ; users-modify so tray / non-admin reconfigure can update .env and runtime files.
@ -277,9 +280,27 @@ begin
end; end;
function DetectExistingConfiguredInstall(): Boolean; function DetectExistingConfiguredInstall(): Boolean;
var
Lines: TArrayOfString;
i: Integer;
Line: String;
begin begin
{ Configured install = ProgramData\.env already has NETX_DB_MODE. } { NETX_DATABASE_URL-only installs predate NETX_DB_MODE and use an external DB. }
Result := EnvHasDbMode(); Result := EnvHasDbMode();
if Result then
Exit;
if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then
Exit;
for i := 0 to GetArrayLength(Lines) - 1 do
begin
Line := Trim(Lines[i]);
if (Pos('NETX_DATABASE_URL=', Line) = 1) and
(Length(Line) > Length('NETX_DATABASE_URL=')) then
begin
Result := True;
Exit;
end;
end;
end; end;
procedure SyncSkipDbFromInstallMode(); procedure SyncSkipDbFromInstallMode();
@ -343,18 +364,41 @@ end;
procedure StopNetxBeforeFileReplace(); procedure StopNetxBeforeFileReplace();
var var
ResultCode: Integer; ResultCode: Integer;
StopScript: String; LifecycleScript: String;
Params: String; Params: String;
begin begin
StopScript := ExpandConstant('{app}\packaging\stop_netx_app.ps1'); ExtractTemporaryFile('_common.ps1');
if not FileExists(StopScript) then ExtractTemporaryFile('service_lifecycle.ps1');
LifecycleScript := ExpandConstant('{tmp}\service_lifecycle.ps1');
Params :=
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
' -Action StopForUpdate' +
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then
RaiseException('Could not stop NetX before replacing program files.');
if ResultCode <> 0 then
RaiseException('Could not stop NetX services/tasks before replacing program files. See ProgramData\\NetX\\data\\runtime\\service_lifecycle.log.');
end;
procedure ResumeNetxServiceAfterInstall();
var
ResultCode: Integer;
LifecycleScript: String;
Params: String;
begin
LifecycleScript := ExpandConstant('{app}\packaging\service_lifecycle.ps1');
if not FileExists(LifecycleScript) then
Exit; Exit;
Params := Params :=
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + StopScript + '"' + '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
' -Action ResumeAfterUpdate' +
' -ProgramRoot "' + ExpandConstant('{app}') + '"' + ' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"' + ' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
' -KillTray'; if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then
Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode); RaiseException('NetX was updated, but its previously running service could not be restarted.');
if ResultCode <> 0 then
RaiseException('NetX was updated, but its previously running service could not be restarted. See ProgramData\\NetX\\data\\runtime\\service_lifecycle.log.');
end; end;
function NetxShouldStartTray(): Boolean; function NetxShouldStartTray(): Boolean;
@ -988,6 +1032,9 @@ end;
function InitializeUninstall(): Boolean; function InitializeUninstall(): Boolean;
var var
DeleteSrc: String; DeleteSrc: String;
LifecycleScript: String;
Params: String;
ResultCode: Integer;
begin begin
Result := True; Result := True;
GDeleteNetxData := False; GDeleteNetxData := False;
@ -1008,6 +1055,23 @@ begin
GDeleteNetxData := GDeleteNetxData :=
MsgBox(ExpandConstant('{cm:UninstallDeleteData}'), mbConfirmation, MB_YESNO) = IDYES; MsgBox(ExpandConstant('{cm:UninstallDeleteData}'), mbConfirmation, MB_YESNO) = IDYES;
end; end;
LifecycleScript := ExpandConstant('{app}\packaging\service_lifecycle.ps1');
if FileExists(LifecycleScript) then
begin
Params :=
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
' -Action RemoveForUninstall' +
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
if (not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode)) or
(ResultCode <> 0) then
begin
MsgBox('Could not stop/remove the NetX Windows service or startup task. Uninstall has been cancelled to protect the installation. See ProgramData\\NetX\\data\\runtime\\service_lifecycle.log.', mbError, MB_OK);
Result := False;
Exit;
end;
end;
end; end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
@ -1069,5 +1133,6 @@ begin
UninstallKey := UninstallKey :=
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1'; 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1';
RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString'); RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString');
ResumeNetxServiceAfterInstall();
end; end;
end; end;

View file

@ -1,11 +1,11 @@
{ {
"channel": "stable", "channel": "stable",
"latest": "0.4.13", "latest": "0.4.14",
"min_compatible": "0.3.0", "min_compatible": "0.3.0",
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.13", "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.14",
"windows": { "windows": {
"url": "https://github.com/hansjone/netx/releases/download/v0.4.13/NetX-Setup-0.4.13.exe", "url": "https://github.com/hansjone/netx/releases/download/v0.4.14/NetX-Setup-0.4.14.exe",
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.13/NetX-Setup-0.4.13.exe", "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.14/NetX-Setup-0.4.14.exe",
"package": "setup", "package": "setup",
"sha256": "", "sha256": "",
"size": 0 "size": 0

View file

@ -28,6 +28,7 @@ if (-not $Version) {
$tag = "v$Version" $tag = "v$Version"
$relDir = if ($ReleaseDir) { $ReleaseDir } else { Join-Path $PSScriptRoot "release" } $relDir = if ($ReleaseDir) { $ReleaseDir } else { Join-Path $PSScriptRoot "release" }
$setup = Join-Path $relDir "NetX-Setup-$Version.exe" $setup = Join-Path $relDir "NetX-Setup-$Version.exe"
$checksum = "$setup.sha256"
if (-not $Token) { if (-not $Token) {
$Token = $env:NETX_FORGEJO_TOKEN $Token = $env:NETX_FORGEJO_TOKEN
@ -39,7 +40,8 @@ if (-not $Token) {
if (-not (Test-Path $setup)) { if (-not (Test-Path $setup)) {
throw "missing_setup: $setup (run build_release.ps1 + ISCC first)" throw "missing_setup: $setup (run build_release.ps1 + ISCC first)"
} }
$hash = (Get-FileHash -LiteralPath $setup -Algorithm SHA256).Hash.ToLowerInvariant()
Set-Content -LiteralPath $checksum -Value $hash -Encoding ascii -NoNewline
$apiBase = "$ForgejoBase/api/v1/repos/$Owner/$Repo" $apiBase = "$ForgejoBase/api/v1/repos/$Owner/$Repo"
$headers = @{ $headers = @{
"Authorization" = "token $Token" "Authorization" = "token $Token"
@ -122,6 +124,7 @@ function Upload-ForgejoAsset {
} }
Upload-ForgejoAsset -Path $setup Upload-ForgejoAsset -Path $setup
Upload-ForgejoAsset -Path $checksum
$releaseUrl = "$ForgejoBase/$Owner/$Repo/releases/tag/$tag" $releaseUrl = "$ForgejoBase/$Owner/$Repo/releases/tag/$tag"
Write-Host "" Write-Host ""

View file

@ -17,12 +17,20 @@ if (-not $Version) {
$tag = "v$Version" $tag = "v$Version"
$releaseDir = Join-Path $PSScriptRoot "release" $releaseDir = Join-Path $PSScriptRoot "release"
$setup = Join-Path $releaseDir "NetX-Setup-$Version.exe" $setup = Join-Path $releaseDir "NetX-Setup-$Version.exe"
$checksum = "$setup.sha256"
$stage = Join-Path $releaseDir "netx-win64"
function Assert-NativeSuccess([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
Write-Host "==> NetX publish $tag" Write-Host "==> NetX publish $tag"
if (-not $SkipBuild) { if (-not $SkipBuild) {
if (Test-Path -LiteralPath $setup) { Remove-Item -LiteralPath $setup -Force }
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build_release.ps1") ` & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build_release.ps1") `
-Version $Version -CreateVenv -Version $Version -CreateVenv
Assert-NativeSuccess 'Build stage'
} }
if (-not $SkipInstaller) { if (-not $SkipInstaller) {
@ -38,8 +46,10 @@ if (-not $SkipInstaller) {
throw "innosetup_not_found: install with winget install JRSoftware.InnoSetup" throw "innosetup_not_found: install with winget install JRSoftware.InnoSetup"
} }
Write-Host "==> Compiling installer: $iscc" Write-Host "==> Compiling installer: $iscc"
if (Test-Path -LiteralPath $setup) { Remove-Item -LiteralPath $setup -Force }
& $iscc "/DMyAppVersion=$Version" (Join-Path $PSScriptRoot "installer\netx.iss") & $iscc "/DMyAppVersion=$Version" (Join-Path $PSScriptRoot "installer\netx.iss")
if (-not (Test-Path $setup)) { Assert-NativeSuccess 'Inno Setup compilation'
if (-not (Test-Path -LiteralPath $setup) -or (Get-Item -LiteralPath $setup).Length -lt 1MB) {
throw "installer_build_failed: expected $setup" throw "installer_build_failed: expected $setup"
} }
Write-Host "==> Setup.exe: $setup" -ForegroundColor Green Write-Host "==> Setup.exe: $setup" -ForegroundColor Green
@ -49,11 +59,26 @@ if (-not (Test-Path $setup)) {
throw "missing_setup: $setup" throw "missing_setup: $setup"
} }
$stageVersionPath = Join-Path $stage 'version.json'
if (-not (Test-Path -LiteralPath $stageVersionPath)) { throw "missing_release_stage: $stageVersionPath" }
$stageVersion = (Get-Content -LiteralPath $stageVersionPath -Raw | ConvertFrom-Json).version
if ([string]$stageVersion -ne $Version) {
throw "release_version_mismatch: requested=$Version stage=$stageVersion"
}
if ((Get-Item -LiteralPath $setup).LastWriteTimeUtc -lt (Get-Item -LiteralPath $stageVersionPath).LastWriteTimeUtc) {
throw "stale_installer: $setup predates the release stage; rebuild or recompile before publishing"
}
if ($Sign) { if ($Sign) {
Write-Host "==> Signing release artifacts" Write-Host "==> Signing release artifacts"
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "sign_release.ps1") -Files @($setup) & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "sign_release.ps1") -Files @($setup)
Assert-NativeSuccess 'Code signing'
} }
$hash = (Get-FileHash -LiteralPath $setup -Algorithm SHA256).Hash.ToLowerInvariant()
Set-Content -LiteralPath $checksum -Value $hash -Encoding ascii -NoNewline
Write-Host "==> SHA-256 $hash"
if ($SkipGitHub) { if ($SkipGitHub) {
Write-Host "==> Skip GitHub release (-SkipGitHub)" Write-Host "==> Skip GitHub release (-SkipGitHub)"
exit 0 exit 0
@ -72,7 +97,8 @@ $viewCode = $LASTEXITCODE
$ErrorActionPreference = $prevEap $ErrorActionPreference = $prevEap
if ($viewCode -eq 0) { if ($viewCode -eq 0) {
Write-Host "==> Release $tag exists; uploading assets" Write-Host "==> Release $tag exists; uploading assets"
gh release upload $tag $setup --clobber gh release upload $tag $setup $checksum --clobber
Assert-NativeSuccess 'GitHub asset upload'
} else { } else {
$notes = @" $notes = @"
## NetX $Version ## NetX $Version
@ -99,8 +125,11 @@ See [packaging/README.md](https://github.com/hansjone/netx/blob/main/packaging/R
$args = @("release", "create", $tag, "--title", "NetX $Version", "--notes", $notes) $args = @("release", "create", $tag, "--title", "NetX $Version", "--notes", $notes)
if ($Draft) { $args += "--draft" } if ($Draft) { $args += "--draft" }
$args += $setup $args += $setup
$args += $checksum
& gh @args & gh @args
Assert-NativeSuccess 'GitHub release creation'
} }
Write-Host "==> Published $tag" -ForegroundColor Green Write-Host "==> Published $tag" -ForegroundColor Green
gh release view $tag --web 2>$null gh release view $tag --web 2>$null
Assert-NativeSuccess 'GitHub release verification'

View file

@ -0,0 +1,124 @@
param(
[Parameter(Mandatory = $true)]
[ValidateSet('StopForUpdate', 'ResumeAfterUpdate', 'RemoveForUninstall')]
[string]$Action,
[string]$ProgramRoot = '',
[string]$DataRoot = ''
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\_common.ps1"
$prog = Get-NetxProgramRoot -Override $ProgramRoot
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
$runtime = Join-Path $data 'data\runtime'
$null = New-Item -ItemType Directory -Path $runtime -Force
$stateFile = Join-Path $runtime 'service-update-state.json'
$logFile = Join-Path $runtime 'service_lifecycle.log'
function Write-LifecycleLog([string]$Message) {
Add-Content -LiteralPath $logFile -Value "$(Get-Date -Format o) $Message" -Encoding utf8
}
function Get-NetxTask {
try { return Get-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -ErrorAction Stop }
catch { return $null }
}
function Stop-NetxRunners([switch]$Remove) {
$state = @{ serviceWasRunning = $false; taskWasRunning = $false }
$svc = Get-Service -Name 'NetX' -ErrorAction SilentlyContinue
if ($svc) {
$state.serviceWasRunning = ($svc.Status -ne 'Stopped')
$state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding utf8
if ($svc.Status -ne 'Stopped') {
Write-LifecycleLog 'Stopping Windows service NetX'
Stop-Service -Name 'NetX' -Force -ErrorAction Stop
(Get-Service -Name 'NetX').WaitForStatus('Stopped', [TimeSpan]::FromSeconds(60))
}
}
$task = Get-NetxTask
if ($task) {
$state.taskWasRunning = ([string]$task.State -eq 'Running')
$state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding utf8
if ($state.taskWasRunning) {
Write-LifecycleLog 'Stopping scheduled task NetXService'
Stop-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -ErrorAction Stop
$deadline = (Get-Date).AddSeconds(60)
do {
Start-Sleep -Milliseconds 500
$task = Get-NetxTask
} while ($task -and [string]$task.State -eq 'Running' -and (Get-Date) -lt $deadline)
if ($task -and [string]$task.State -eq 'Running') { throw 'NetXService scheduled task did not stop within 60 seconds.' }
}
}
if ($Remove) {
if ($svc) {
$winsw = Join-Path $prog 'packaging\winsw\NetX.exe'
if (Test-Path -LiteralPath $winsw) {
Write-LifecycleLog 'Uninstalling WinSW service'
& $winsw uninstall
if ($LASTEXITCODE -ne 0) { throw "WinSW uninstall failed: $LASTEXITCODE" }
} else {
throw "NetX service exists but WinSW executable is missing: $winsw"
}
}
if ($task) {
Unregister-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -Confirm:$false -ErrorAction Stop
}
}
return $state
}
try {
switch ($Action) {
'StopForUpdate' {
$state = Stop-NetxRunners
$state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding utf8
$stopScript = Join-Path $prog 'packaging\stop_netx_app.ps1'
if (Test-Path -LiteralPath $stopScript) {
& powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $stopScript `
-ProgramRoot $prog -DataRoot $data -KillTray
if ($LASTEXITCODE -ne 0) { throw "NetX application stop failed: $LASTEXITCODE" }
}
}
'ResumeAfterUpdate' {
if (-not (Test-Path -LiteralPath $stateFile)) { break }
$state = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json
if ($state.serviceWasRunning) {
Write-LifecycleLog 'Restarting Windows service NetX after update'
Start-Service -Name 'NetX' -ErrorAction Stop
}
if ($state.taskWasRunning) {
Write-LifecycleLog 'Restarting scheduled task NetXService after update'
Start-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -ErrorAction Stop
}
Remove-Item -LiteralPath $stateFile -Force
}
'RemoveForUninstall' {
$null = Stop-NetxRunners -Remove
$stopScript = Join-Path $prog 'packaging\stop_netx_app.ps1'
if (Test-Path -LiteralPath $stopScript) {
& powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $stopScript `
-ProgramRoot $prog -DataRoot $data -KillTray
if ($LASTEXITCODE -ne 0) { throw "NetX application stop failed: $LASTEXITCODE" }
}
}
}
Write-LifecycleLog "Action completed: $Action"
exit 0
} catch {
Write-LifecycleLog "Action failed: $Action; $($_.Exception.Message)"
if ($Action -eq 'StopForUpdate' -and (Test-Path -LiteralPath $stateFile)) {
try {
$prior = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json
if ($prior.serviceWasRunning) { Start-Service -Name 'NetX' -ErrorAction Stop }
if ($prior.taskWasRunning) { Start-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -ErrorAction Stop }
Remove-Item -LiteralPath $stateFile -Force
Write-LifecycleLog 'Restored service/task after update stop failure'
} catch {
Write-LifecycleLog "Could not restore service/task after update stop failure: $($_.Exception.Message)"
}
}
Write-Error $_
exit 1
}

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netx-ops" name = "netx-ops"
version = "0.4.13" version = "0.4.14"
description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"