mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 06:03:18 +08:00
Prepare NetX 0.4.14 Windows release
This commit is contained in:
parent
af8d5a8d37
commit
a5c9a8f58d
10 changed files with 320 additions and 42 deletions
7
.github/workflows/release-windows.yml
vendored
7
.github/workflows/release-windows.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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") `
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
124
packaging/service_lifecycle.ps1
Normal file
124
packaging/service_lifecycle.ps1
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue