fix: make packaged updater handshake Windows-safe
ForgeFlow quality gate / quality (push) Canceled after 0s
ForgeFlow quality gate / quality (push) Canceled after 0s
This commit is contained in:
@@ -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." }
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user