Release ForgeFlow 0.6.1
This commit is contained in:
@@ -6,7 +6,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"
|
||||
@@ -17,8 +18,9 @@ $backup = $null
|
||||
function Write-UpdateLog {
|
||||
param([string]$Message)
|
||||
$line = "$(Get-Date -Format o) $Message"
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LogPath) | Out-Null
|
||||
Add-Content -Path $LogPath -Value $line -Encoding UTF8
|
||||
$directory = Split-Path -Parent $LogPath
|
||||
if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
|
||||
Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
function Write-UpdateState {
|
||||
@@ -27,6 +29,7 @@ function Write-UpdateState {
|
||||
[string]$Message = "",
|
||||
[hashtable]$Extra = @{}
|
||||
)
|
||||
|
||||
$payload = [ordered]@{
|
||||
schemaVersion = 1
|
||||
updateId = $UpdateId
|
||||
@@ -39,13 +42,28 @@ function Write-UpdateState {
|
||||
updatedAt = (Get-Date).ToUniversalTime().ToString("o")
|
||||
}
|
||||
foreach ($key in $Extra.Keys) { $payload[$key] = $Extra[$key] }
|
||||
|
||||
$directory = Split-Path -Parent $StatusPath
|
||||
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
||||
if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
|
||||
$temporary = "$StatusPath.$PID.tmp"
|
||||
$json = $payload | ConvertTo-Json -Depth 8
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($temporary, $json, $utf8NoBom)
|
||||
Move-Item -LiteralPath $temporary -Destination $StatusPath -Force
|
||||
|
||||
try {
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Robocopy {
|
||||
@@ -85,6 +103,11 @@ try {
|
||||
Write-UpdateLog "ForgeFlow source update helper started for version $ExpectedVersion."
|
||||
Write-UpdateState -State "started" -Message "The external update helper started successfully." -Extra @{ helperPid = $PID; startedAt = (Get-Date).ToUniversalTime().ToString("o") }
|
||||
|
||||
if ($HandshakeOnly) {
|
||||
Write-UpdateLog "Handshake-only verification completed successfully."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-UpdateState -State "waiting-for-exit" -Message "Waiting for the running ForgeFlow process to exit."
|
||||
$deadline = (Get-Date).AddMinutes(2)
|
||||
while (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue) {
|
||||
@@ -92,7 +115,7 @@ try {
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
$actualHash = (Get-FileHash -Path $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$actualHash = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $ExpectedSha256.ToLowerInvariant()) { throw "Update archive checksum mismatch." }
|
||||
|
||||
$working = Join-Path ([IO.Path]::GetTempPath()) ("forgeflow-update-" + [guid]::NewGuid().ToString("N"))
|
||||
@@ -143,6 +166,13 @@ try {
|
||||
try {
|
||||
$restart = Start-ForgeFlow -WorkingDirectory $SourcePath
|
||||
Write-UpdateLog "Update validated successfully. ForgeFlow was restarted directly with Electron PID $($restart.Id)."
|
||||
Write-UpdateState -State "success" -Message "ForgeFlow $ExpectedVersion was installed and restarted successfully." -Extra @{
|
||||
installedVersion = $ExpectedVersion
|
||||
completedAt = $completedAt
|
||||
restartLaunched = $true
|
||||
restartPid = $restart.Id
|
||||
restartError = $null
|
||||
}
|
||||
} catch {
|
||||
$restartError = $_.Exception.Message
|
||||
Write-UpdateLog "Update validated successfully, but automatic restart failed: $restartError"
|
||||
@@ -159,10 +189,10 @@ try {
|
||||
}
|
||||
catch {
|
||||
$failureMessage = $_.Exception.Message
|
||||
Write-UpdateLog ("Update failed: " + $failureMessage)
|
||||
Write-UpdateState -State "failed" -Message $failureMessage -Extra @{ failedAt = (Get-Date).ToUniversalTime().ToString("o") }
|
||||
try { Write-UpdateLog ("Update failed: " + $failureMessage) } catch {}
|
||||
try { Write-UpdateState -State "failed" -Message $failureMessage -Extra @{ failedAt = (Get-Date).ToUniversalTime().ToString("o") } } catch {}
|
||||
try {
|
||||
if ($backup -and (Test-Path $backup)) {
|
||||
if ($backup -and (Test-Path -LiteralPath $backup)) {
|
||||
Write-UpdateLog "Restoring previous source version."
|
||||
Invoke-Robocopy -From $backup -To $SourcePath
|
||||
Install-ForgeFlowDependencies -WorkingDirectory $SourcePath
|
||||
@@ -176,6 +206,12 @@ catch {
|
||||
try {
|
||||
$rollbackRestart = Start-ForgeFlow -WorkingDirectory $SourcePath
|
||||
Write-UpdateLog "Rollback restored and ForgeFlow restarted directly with Electron PID $($rollbackRestart.Id)."
|
||||
Write-UpdateState -State "rolled-back" -Message $failureMessage -Extra @{
|
||||
completedAt = $rollbackCompletedAt
|
||||
restartLaunched = $true
|
||||
restartPid = $rollbackRestart.Id
|
||||
restartError = $null
|
||||
}
|
||||
} catch {
|
||||
$rollbackRestartError = $_.Exception.Message
|
||||
Write-UpdateLog ("Rollback restart failed: " + $rollbackRestartError)
|
||||
@@ -188,8 +224,8 @@ catch {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-UpdateLog ("Rollback failed: " + $_.Exception.Message)
|
||||
Write-UpdateState -State "failed" -Message ("$failureMessage Rollback also failed: " + $_.Exception.Message) -Extra @{ completedAt = (Get-Date).ToUniversalTime().ToString("o") }
|
||||
try { Write-UpdateLog ("Rollback failed: " + $_.Exception.Message) } catch {}
|
||||
try { Write-UpdateState -State "failed" -Message ("$failureMessage Rollback also failed: " + $_.Exception.Message) -Extra @{ completedAt = (Get-Date).ToUniversalTime().ToString("o") } } catch {}
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
+6
-2
@@ -19,7 +19,7 @@ const required = [
|
||||
'docs/ARCHITECTURE.md', 'docs/SECURITY.md', 'docs/ROADMAP.md', 'docs/SETUP_GUIDE.md',
|
||||
'docs/UPDATING.md', 'docs/DIAGNOSTICS.md', 'docs/DEPLOYMENT_SETUP.md', 'docs/SSH_UNRAID_DEPLOYMENT.md',
|
||||
'docs/LUMAOPS_SERVER_AUDIT.md', 'docs/STATUS_ENDPOINT.md', 'docs/TEST_MATRIX.md', 'docs/RELEASE_NOTES_0.4.0.md', 'docs/RELEASE_NOTES_0.4.1.md', 'docs/RELEASE_NOTES_0.4.2.md', 'docs/RELEASE_NOTES_0.4.3.md',
|
||||
'docs/RELEASE_AUDIT_0.6.0.md', 'docs/RELEASE_NOTES_0.5.0.md', 'docs/RELEASE_NOTES_0.5.1.md', 'docs/RELEASE_NOTES_0.5.2.md', 'docs/RELEASE_NOTES_0.5.3.md', 'docs/RELEASE_NOTES_0.5.4.md', 'docs/RELEASE_NOTES_0.6.0.md',
|
||||
'docs/RELEASE_AUDIT_0.6.0.md', 'docs/RELEASE_NOTES_0.6.1.md', 'docs/RELEASE_NOTES_0.5.0.md', 'docs/RELEASE_NOTES_0.5.1.md', 'docs/RELEASE_NOTES_0.5.2.md', 'docs/RELEASE_NOTES_0.5.3.md', 'docs/RELEASE_NOTES_0.5.4.md', 'docs/RELEASE_NOTES_0.6.0.md',
|
||||
'Publish-ForgeFlow-Release.ps1', 'docs/RELEASE_NOTES_0.4.4.md', 'docs/RELEASE_NOTES_0.4.5.md',
|
||||
'examples/gitea-actions/deploy.yml', 'examples/gitea-actions/rollback.yml',
|
||||
'examples/server/forgeflow-deploy', 'examples/server/forgeflow-targets.conf',
|
||||
@@ -30,7 +30,7 @@ const required = [
|
||||
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.6.0') throw new Error(`Expected package version 0.6.0, got ${packageJson.version}.`);
|
||||
if (packageJson.version !== '0.6.1') throw new Error(`Expected package version 0.6.1, got ${packageJson.version}.`);
|
||||
const sourceManifest = await readFile(path.join(root, 'SOURCE_MANIFEST.txt'), 'utf8');
|
||||
if (!sourceManifest.startsWith(`ForgeFlow ${packageJson.version} source manifest\n`)) throw new Error('SOURCE_MANIFEST.txt does not match the package version.');
|
||||
for (const group of ['dependencies', 'devDependencies']) {
|
||||
@@ -85,6 +85,7 @@ const setupGuide = await readFile(path.join(root, 'docs/SETUP_GUIDE.md'), 'utf8'
|
||||
const sshGuide = await readFile(path.join(root, 'docs/SSH_UNRAID_DEPLOYMENT.md'), 'utf8');
|
||||
const audit = await readFile(path.join(root, 'docs/LUMAOPS_SERVER_AUDIT.md'), 'utf8');
|
||||
const releaseNotes = await readFile(path.join(root, 'docs/RELEASE_NOTES_0.6.0.md'), 'utf8');
|
||||
const updaterReleaseNotes = await readFile(path.join(root, 'docs/RELEASE_NOTES_0.6.1.md'), 'utf8');
|
||||
if (!setupGuide.includes('Gitea access token') || !setupGuide.includes('diagnostic bundle')) {
|
||||
throw new Error('Setup guide is missing required connection or diagnostics instructions.');
|
||||
}
|
||||
@@ -97,6 +98,9 @@ if (!audit.includes('d42d4a7f08240c478d07466e3fabec654dc71367') || !audit.includ
|
||||
for (const phrase of ['DockerMan', 'HEAD.lock', 'deployment reconciliation', 'Portfolio', 'safety branch', 'high-contrast ITWorx']) {
|
||||
if (!releaseNotes.includes(phrase)) throw new Error(`Release notes are missing: ${phrase}`);
|
||||
}
|
||||
for (const phrase of ['Windows PowerShell 5.1', 'File.Replace', 'handshake-only', 'updateId']) {
|
||||
if (!updaterReleaseNotes.includes(phrase)) throw new Error(`Updater release notes are missing: ${phrase}`);
|
||||
}
|
||||
const updateHelperPath = path.join(root, 'scripts/apply-source-update.ps1');
|
||||
const updateHelperBytes = await readFile(updateHelperPath);
|
||||
if (updateHelperBytes[0] === 0xef && updateHelperBytes[1] === 0xbb && updateHelperBytes[2] === 0xbf) throw new Error('PowerShell update helper must not contain a UTF-8 BOM.');
|
||||
|
||||
Reference in New Issue
Block a user