fix: make packaged updater handshake Windows-safe
ForgeFlow quality gate / quality (push) Canceled after 0s

This commit is contained in:
NuklearRabbit
2026-08-01 15:09:47 +02:00
parent 44aa452a76
commit acad1f8932
10 changed files with 91 additions and 25 deletions
+24 -5
View File
@@ -7,7 +7,8 @@ param(
[Parameter(Mandatory = $true)][int]$ParentPid,
[Parameter(Mandatory = $true)][string]$LogPath,
[Parameter(Mandatory = $true)][string]$StatusPath,
[Parameter(Mandatory = $true)][string]$UpdateId
[Parameter(Mandatory = $true)][string]$UpdateId,
[switch]$HandshakeOnly
)
$ErrorActionPreference = "Stop"
@@ -27,12 +28,26 @@ function Write-UpdateState {
updatedAt = [DateTime]::UtcNow.ToString("o")
}
if ($State -in @("success", "failed", "rolled-back")) { $payload.completedAt = [DateTime]::UtcNow.ToString("o") }
$directory = Split-Path -Parent $StatusPath
if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
$temporary = "$StatusPath.$PID.tmp"
$payload | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $temporary -Encoding UTF8
if (Test-Path -LiteralPath $StatusPath) { [IO.File]::Replace($temporary, $StatusPath, $null) }
else { Move-Item -LiteralPath $temporary -Destination $StatusPath }
$backup = "$StatusPath.$PID.bak"
$json = $payload | ConvertTo-Json -Depth 4
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[IO.File]::WriteAllText($temporary, $json, $utf8NoBom)
try {
if ([IO.File]::Exists($StatusPath)) {
[IO.File]::Replace($temporary, $StatusPath, $backup)
[IO.File]::Delete($backup)
} else {
[IO.File]::Move($temporary, $StatusPath)
}
} catch {
[IO.File]::Copy($temporary, $StatusPath, $true)
[IO.File]::Delete($temporary)
if ([IO.File]::Exists($backup)) { [IO.File]::Delete($backup) }
}
}
function Write-Log([string]$Message) {
"{0} {1}" -f [DateTime]::UtcNow.ToString("o"), $Message | Add-Content -LiteralPath $LogPath -Encoding UTF8
}
@@ -40,6 +55,10 @@ function Write-Log([string]$Message) {
try {
Write-UpdateState -State "started" -Message "Binary updater owns the update request."
Write-Log "Validating ForgeFlow $ExpectedVersion binary update."
if ($HandshakeOnly) {
Write-Log "Handshake-only verification completed successfully."
exit 0
}
$actualSha256 = (Get-FileHash -LiteralPath $BinaryPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) { throw "Binary update SHA-256 verification failed." }
if (-not (Test-Path -LiteralPath $CurrentExecutable -PathType Leaf)) { throw "Current ForgeFlow executable was not found." }
+8 -3
View File
@@ -54,15 +54,20 @@ function Write-UpdateState {
if ([System.IO.File]::Exists($StatusPath)) {
# Windows PowerShell 5.1 does not reliably let Move-Item -Force replace
# an existing file. File.Replace is atomic on the local NTFS volume.
[System.IO.File]::Replace($temporary, $StatusPath, $null)
$backup = "$StatusPath.$PID.bak"
[System.IO.File]::Replace($temporary, $StatusPath, $backup)
} else {
[System.IO.File]::Move($temporary, $StatusPath)
}
} catch {
# Some filesystems do not implement File.Replace. Copy with overwrite is
# the deterministic fallback; the temporary file is removed afterwards.
[System.IO.File]::Copy($temporary, $StatusPath, $true)
[System.IO.File]::Delete($temporary)
if ([System.IO.File]::Exists($temporary)) {
[System.IO.File]::Copy($temporary, $StatusPath, $true)
[System.IO.File]::Delete($temporary)
}
} finally {
if ([System.IO.File]::Exists($backup)) { [System.IO.File]::Delete($backup) }
}
}
+7 -2
View File
@@ -83,6 +83,7 @@ const required = [
"docs/RELEASE_NOTES_0.10.1.md",
"docs/RELEASE_NOTES_0.10.2.md",
"docs/RELEASE_NOTES_0.10.3.md",
"docs/RELEASE_NOTES_0.10.4.md",
"docs/UPDATING.md",
"docs/DIAGNOSTICS.md",
"docs/DEPLOYMENT_SETUP.md",
@@ -121,9 +122,9 @@ for (const file of required) await access(path.join(root, file));
const packageJson = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"),
);
if (packageJson.version !== "0.10.3")
if (packageJson.version !== "0.10.4")
throw new Error(
`Expected package version 0.10.3, got ${packageJson.version}.`,
`Expected package version 0.10.4, got ${packageJson.version}.`,
);
const sourceManifest = await readFile(
path.join(root, "SOURCE_MANIFEST.txt"),
@@ -449,6 +450,10 @@ const release0103 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.3.md
for (const phrase of ["concurrently", "debounce", "animation frame", "Git Validator", "stale or forged"]) {
if (!release0103.includes(phrase)) throw new Error(`0.10.3 release notes are missing: ${phrase}`);
}
const release0104 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.4.md"), "utf8");
for (const phrase of ["Windows PowerShell 5.1", "atomic status", "handshake-only", "existing installations"]) {
if (!release0104.includes(phrase)) throw new Error(`0.10.4 release notes are missing: ${phrase}`);
}
const configSource = await readFile(path.join(root, "src/main/config-store.cjs"), "utf8");
for (const mode of ["server-git", "push-bundle", "monitor-only"]) {
if (!configSource.includes(mode)) throw new Error(`Deployment configuration is missing mode: ${mode}`);