mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 17:20:46 +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
|
|
@ -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.
|
||||
|
||||
|
|
@ -20,20 +20,20 @@ Zip packages are **not** published. Releases ship **Setup.exe only**.
|
|||
|
||||
**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)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
|
|
@ -118,15 +118,14 @@ Stops services, replaces program folders (`netx_api`, `web`, `packaging`, `postg
|
|||
.\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)
|
||||
|
||||
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`
|
||||
2. **GitHub** — `.\packaging\publish_release.ps1 -Version x.y.z` (uploads Setup.exe only)
|
||||
3. **Forgejo** — upload the same Setup.exe (default: public domain `https://git.avelo.top`):
|
||||
1. **Build and publish GitHub** — ` .\packaging\publish_release.ps1 -Version x.y.z` builds, verifies and uploads Setup.exe plus SHA-256.
|
||||
2. **Forgejo** — publish the same Setup.exe and checksum sidecar (default: public domain `https://git.avelo.top`):
|
||||
|
||||
```powershell
|
||||
# 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` |
|
||||
|
||||
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
|
||||
- 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",
|
||||
"stop_netx_app.ps1", "update_netx.ps1", "check_update.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"
|
||||
)) {
|
||||
$src = Join-Path $PSScriptRoot $name
|
||||
|
|
@ -173,7 +173,9 @@ foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd", "
|
|||
if ($CreateVenv) {
|
||||
Write-Host "==> Creating portable python/runtime + .venv (must not reference build-machine paths)"
|
||||
$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"
|
||||
|
||||
$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 " 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 "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
|
||||
}
|
||||
if ($result.sha256 -and $result.sha256 -notmatch 'REPLACE' -and $result.sha256.Trim()) {
|
||||
$hash = (Get-FileHash -Path $pkgPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$expect = $result.sha256.ToLowerInvariant()
|
||||
if ($hash -ne $expect) {
|
||||
throw "sha256_mismatch: got $hash expected $expect"
|
||||
$expectedHash = [string]$result.sha256
|
||||
if (-not $expectedHash -or $expectedHash -match 'REPLACE') {
|
||||
$checksumUri = [uri]$result.download_url
|
||||
$checksumBuilder = New-Object -TypeName System.UriBuilder -ArgumentList $checksumUri
|
||||
$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).
|
||||
|
|
@ -422,10 +452,29 @@ try {
|
|||
& powershell -NoProfile -ExecutionPolicy Bypass -File $repairPs1 `
|
||||
-ProgramRoot $prog -DataRoot $data
|
||||
}
|
||||
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 }
|
||||
$managedRunnerActive = $false
|
||||
$netxService = Get-Service -Name "NetX" -ErrorAction SilentlyContinue
|
||||
if ($netxService -and $netxService.Status -eq "Running") { $managedRunnerActive = $true }
|
||||
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 {
|
||||
Write-Host "==> Applying legacy zip update via update_netx.ps1"
|
||||
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") `
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#define MyAppName "NetX"
|
||||
#ifndef MyAppVersion
|
||||
#define MyAppVersion "0.4.13"
|
||||
#define MyAppVersion "0.4.14"
|
||||
#endif
|
||||
#define MyAppPublisher "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\libxml2.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]
|
||||
; users-modify so tray / non-admin reconfigure can update .env and runtime files.
|
||||
|
|
@ -277,9 +280,27 @@ begin
|
|||
end;
|
||||
|
||||
function DetectExistingConfiguredInstall(): Boolean;
|
||||
var
|
||||
Lines: TArrayOfString;
|
||||
i: Integer;
|
||||
Line: String;
|
||||
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();
|
||||
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;
|
||||
|
||||
procedure SyncSkipDbFromInstallMode();
|
||||
|
|
@ -343,18 +364,41 @@ end;
|
|||
procedure StopNetxBeforeFileReplace();
|
||||
var
|
||||
ResultCode: Integer;
|
||||
StopScript: String;
|
||||
LifecycleScript: String;
|
||||
Params: String;
|
||||
begin
|
||||
StopScript := ExpandConstant('{app}\packaging\stop_netx_app.ps1');
|
||||
if not FileExists(StopScript) then
|
||||
ExtractTemporaryFile('_common.ps1');
|
||||
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;
|
||||
Params :=
|
||||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + StopScript + '"' +
|
||||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
|
||||
' -Action ResumeAfterUpdate' +
|
||||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"' +
|
||||
' -KillTray';
|
||||
Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
|
||||
if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||||
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;
|
||||
|
||||
function NetxShouldStartTray(): Boolean;
|
||||
|
|
@ -988,6 +1032,9 @@ end;
|
|||
function InitializeUninstall(): Boolean;
|
||||
var
|
||||
DeleteSrc: String;
|
||||
LifecycleScript: String;
|
||||
Params: String;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := True;
|
||||
GDeleteNetxData := False;
|
||||
|
|
@ -1008,6 +1055,23 @@ begin
|
|||
GDeleteNetxData :=
|
||||
MsgBox(ExpandConstant('{cm:UninstallDeleteData}'), mbConfirmation, MB_YESNO) = IDYES;
|
||||
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;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
|
|
@ -1069,5 +1133,6 @@ begin
|
|||
UninstallKey :=
|
||||
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1';
|
||||
RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString');
|
||||
ResumeNetxServiceAfterInstall();
|
||||
end;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"channel": "stable",
|
||||
"latest": "0.4.13",
|
||||
"latest": "0.4.14",
|
||||
"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": {
|
||||
"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.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.14/NetX-Setup-0.4.14.exe",
|
||||
"package": "setup",
|
||||
"sha256": "",
|
||||
"size": 0
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ if (-not $Version) {
|
|||
$tag = "v$Version"
|
||||
$relDir = if ($ReleaseDir) { $ReleaseDir } else { Join-Path $PSScriptRoot "release" }
|
||||
$setup = Join-Path $relDir "NetX-Setup-$Version.exe"
|
||||
$checksum = "$setup.sha256"
|
||||
|
||||
if (-not $Token) {
|
||||
$Token = $env:NETX_FORGEJO_TOKEN
|
||||
|
|
@ -39,7 +40,8 @@ if (-not $Token) {
|
|||
if (-not (Test-Path $setup)) {
|
||||
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"
|
||||
$headers = @{
|
||||
"Authorization" = "token $Token"
|
||||
|
|
@ -122,6 +124,7 @@ function Upload-ForgejoAsset {
|
|||
}
|
||||
|
||||
Upload-ForgejoAsset -Path $setup
|
||||
Upload-ForgejoAsset -Path $checksum
|
||||
|
||||
$releaseUrl = "$ForgejoBase/$Owner/$Repo/releases/tag/$tag"
|
||||
Write-Host ""
|
||||
|
|
|
|||
|
|
@ -17,12 +17,20 @@ if (-not $Version) {
|
|||
$tag = "v$Version"
|
||||
$releaseDir = Join-Path $PSScriptRoot "release"
|
||||
$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"
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
if (Test-Path -LiteralPath $setup) { Remove-Item -LiteralPath $setup -Force }
|
||||
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build_release.ps1") `
|
||||
-Version $Version -CreateVenv
|
||||
Assert-NativeSuccess 'Build stage'
|
||||
}
|
||||
|
||||
if (-not $SkipInstaller) {
|
||||
|
|
@ -38,8 +46,10 @@ if (-not $SkipInstaller) {
|
|||
throw "innosetup_not_found: install with winget install JRSoftware.InnoSetup"
|
||||
}
|
||||
Write-Host "==> Compiling installer: $iscc"
|
||||
if (Test-Path -LiteralPath $setup) { Remove-Item -LiteralPath $setup -Force }
|
||||
& $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"
|
||||
}
|
||||
Write-Host "==> Setup.exe: $setup" -ForegroundColor Green
|
||||
|
|
@ -49,11 +59,26 @@ if (-not (Test-Path $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) {
|
||||
Write-Host "==> Signing release artifacts"
|
||||
& 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) {
|
||||
Write-Host "==> Skip GitHub release (-SkipGitHub)"
|
||||
exit 0
|
||||
|
|
@ -72,7 +97,8 @@ $viewCode = $LASTEXITCODE
|
|||
$ErrorActionPreference = $prevEap
|
||||
if ($viewCode -eq 0) {
|
||||
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 {
|
||||
$notes = @"
|
||||
## 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)
|
||||
if ($Draft) { $args += "--draft" }
|
||||
$args += $setup
|
||||
$args += $checksum
|
||||
& gh @args
|
||||
Assert-NativeSuccess 'GitHub release creation'
|
||||
}
|
||||
|
||||
Write-Host "==> Published $tag" -ForegroundColor Green
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue