[CmdletBinding()] param() $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent $symbols = @{ Success = [char]0x2705 Running = [char]0x23F3 Fail = [char]0x274C Info = [char]0x2139 } $repositoryUrl = 'https://github.com/bunny-lab-io/Borealis.git' function Write-ProgressStep { param ( [string]$Message, [string]$Status = $symbols["Info"] ) Write-Host "`r$Status $Message... " -NoNewline } function Run-Step { param ( [string] $Message, [scriptblock]$Script ) Write-ProgressStep -Message $Message -Status "$($symbols.Running)" try { & $Script if ($LASTEXITCODE -eq 0 -or $?) { Write-Host "`r$($symbols.Success) $Message " } else { throw "Non-zero exit code" } } catch { Write-Host "`r$($symbols.Fail) $Message - Failed: $_ " -ForegroundColor Red throw } } function Get-GitExecutablePath { param( [string]$ProjectRoot ) $candidates = @() if ($ProjectRoot) { $candidates += (Join-Path $ProjectRoot 'Dependencies\git\cmd\git.exe') $candidates += (Join-Path $ProjectRoot 'Dependencies\git\bin\git.exe') } foreach ($candidate in ($candidates | Select-Object -Unique)) { try { if (Test-Path $candidate -PathType Leaf) { return $candidate } } catch {} } return '' } function Invoke-GitCommand { param( [Parameter(Mandatory = $true)] [string]$GitExe, [Parameter(Mandatory = $true)] [string]$WorkingDirectory, [Parameter(Mandatory = $true)] [string[]]$Arguments ) if ([string]::IsNullOrWhiteSpace($GitExe) -or -not (Test-Path $GitExe -PathType Leaf)) { throw "Git executable not found at '$GitExe'" } if (-not (Test-Path $WorkingDirectory -PathType Container)) { throw "Working directory '$WorkingDirectory' does not exist." } $fullArgs = @('-C', $WorkingDirectory) + $Arguments $output = & $GitExe @fullArgs 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { $joined = ($Arguments -join ' ') $message = "git $joined failed with exit code $exitCode." if ($output) { $message = "$message Output: $output" } throw $message } return $output } function Stop-AgentScheduledTasks { param( [string[]]$TaskNames ) $stopped = @() foreach ($name in $TaskNames) { $taskExists = $false try { $null = Get-ScheduledTask -TaskName $name -ErrorAction Stop $taskExists = $true } catch { try { schtasks.exe /Query /TN "$name" 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { $taskExists = $true } } catch {} } if (-not $taskExists) { continue } Write-Host "Stopping scheduled task: $name" -ForegroundColor Yellow $stopped += $name try { Stop-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue } catch {} try { schtasks.exe /End /TN "$name" /F 2>$null | Out-Null } catch {} try { for ($i = 0; $i -lt 20; $i++) { $info = Get-ScheduledTaskInfo -TaskName $name -ErrorAction Stop if ($info.State -ne 'Running' -and $info.State -ne 'Queued') { break } Start-Sleep -Milliseconds 500 } } catch {} } return ,$stopped } function Start-AgentScheduledTasks { param( [string[]]$TaskNames ) foreach ($name in $TaskNames) { Write-Host "Restarting scheduled task: $name" -ForegroundColor Green try { Start-ScheduledTask -TaskName $name -ErrorAction Stop | Out-Null continue } catch {} try { schtasks.exe /Run /TN "$name" 2>$null | Out-Null } catch {} } } function Get-BorealisServerUrl { param( [string]$AgentRoot ) $serverBaseUrl = $env:BOREALIS_SERVER_URL if (-not $serverBaseUrl) { try { if (-not $AgentRoot) { $AgentRoot = $scriptDir } $settingsDir = Join-Path $AgentRoot 'Settings' $serverUrlFile = Join-Path $settingsDir 'server_url.txt' if (Test-Path $serverUrlFile -PathType Leaf) { $content = Get-Content -Path $serverUrlFile -Raw -ErrorAction Stop if ($content) { $serverBaseUrl = $content.Trim() } } } catch {} } if (-not $serverBaseUrl) { $serverBaseUrl = 'http://localhost:5000' } return $serverBaseUrl.Trim() } function Get-AgentServiceId { param( [string]$AgentRoot ) if (-not $AgentRoot) { $AgentRoot = $scriptDir } $settingsDir = Join-Path $AgentRoot 'Settings' $candidates = @( (Join-Path $settingsDir 'agent_settings_svc.json') (Join-Path $settingsDir 'agent_settings_user.json') (Join-Path $settingsDir 'agent_settings.json') ) foreach ($path in $candidates) { try { if (Test-Path $path -PathType Leaf) { $raw = Get-Content -Path $path -Raw -ErrorAction Stop if (-not $raw) { continue } $cfg = $raw | ConvertFrom-Json -ErrorAction Stop $value = ($cfg.agent_id) if ($value) { return ($value.ToString()).Trim() } } } catch {} } return '' } function Get-AgentGuid { param( [string]$AgentRoot ) $candidates = @() if (-not $AgentRoot) { $AgentRoot = $scriptDir } if ($AgentRoot) { $candidates += (Join-Path $AgentRoot 'agent_GUID') } $defaultPath = Join-Path $scriptDir 'Agent\Borealis\agent_GUID' if ($defaultPath -and ($candidates -notcontains $defaultPath)) { $candidates += $defaultPath } foreach ($path in ($candidates | Select-Object -Unique)) { try { if (Test-Path $path -PathType Leaf) { $value = (Get-Content -Path $path -Raw -ErrorAction Stop) if ($value) { return $value.Trim() } } } catch {} } return '' } function Get-RepositoryCommitHash { param( [Parameter(Mandatory = $true)] [string]$ProjectRoot, [string]$AgentRoot, [string]$GitExe ) $candidates = @() if ($ProjectRoot -and ($candidates -notcontains $ProjectRoot)) { $candidates += $ProjectRoot } if ($AgentRoot -and ($candidates -notcontains $AgentRoot)) { $candidates += $AgentRoot } if ($ProjectRoot) { $agentRootCandidate = Join-Path $ProjectRoot 'Agent\Borealis' if ((Test-Path $agentRootCandidate -PathType Container) -and ($candidates -notcontains $agentRootCandidate)) { $candidates += $agentRootCandidate } } if ($GitExe -and (Test-Path $GitExe -PathType Leaf)) { foreach ($root in $candidates) { try { if (-not (Test-Path (Join-Path $root '.git') -PathType Container)) { continue } $revParse = Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $root -Arguments @('rev-parse','HEAD') if ($revParse) { $candidate = ($revParse | Select-Object -Last 1) if ($candidate) { return ($candidate.Trim()) } } } catch {} } } foreach ($root in $candidates) { try { $gitDir = Join-Path $root '.git' $fetchHead = Join-Path $gitDir 'FETCH_HEAD' if (-not (Test-Path $fetchHead -PathType Leaf)) { continue } foreach ($line in Get-Content -Path $fetchHead -ErrorAction Stop) { $trim = ($line).Trim() if (-not $trim -or $trim.StartsWith('#')) { continue } $split = $trim.Split(@("`t", ' '), [StringSplitOptions]::RemoveEmptyEntries) if ($split.Count -gt 0) { $candidate = $split[0].Trim() if ($candidate) { return $candidate } } } } catch {} } foreach ($root in $candidates) { try { $gitDir = Join-Path $root '.git' $headPath = Join-Path $gitDir 'HEAD' if (-not (Test-Path $headPath -PathType Leaf)) { continue } $head = (Get-Content -Path $headPath -Raw -ErrorAction Stop).Trim() if (-not $head) { continue } if ($head -match '^ref:\s*(.+)$') { $ref = $Matches[1].Trim() if ($ref) { $refPath = $gitDir foreach ($part in ($ref -split '/')) { if ($part) { $refPath = Join-Path $refPath $part } } if (Test-Path $refPath -PathType Leaf) { $commit = (Get-Content -Path $refPath -Raw -ErrorAction Stop).Trim() if ($commit) { return $commit } } $packedRefs = Join-Path $gitDir 'packed-refs' if (Test-Path $packedRefs -PathType Leaf) { foreach ($line in Get-Content -Path $packedRefs -ErrorAction Stop) { $trim = ($line).Trim() if (-not $trim -or $trim.StartsWith('#') -or $trim.StartsWith('^')) { continue } $parts = $trim.Split(' ', 2) if ($parts.Count -ge 2 -and $parts[1].Trim() -eq $ref) { $candidate = $parts[0].Trim() if ($candidate) { return $candidate } } } } } } else { $detached = $head.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) if ($detached.Length -gt 0) { $candidate = $detached[0].Trim() if ($candidate) { return $candidate } } } } catch {} } if ($AgentRoot) { $stored = Get-StoredAgentHash -AgentRoot $AgentRoot if ($stored) { return $stored } } return '' } function Get-StoredAgentHash { param( [string]$AgentRoot ) if (-not $AgentRoot) { return '' } try { $settingsDir = Join-Path $AgentRoot 'Settings' $hashFile = Join-Path $settingsDir 'agent_hash.txt' if (Test-Path $hashFile -PathType Leaf) { $value = (Get-Content -Path $hashFile -Raw -ErrorAction Stop).Trim() return $value } } catch {} return '' } function Set-StoredAgentHash { param( [string]$AgentRoot, [string]$AgentHash ) if ([string]::IsNullOrWhiteSpace($AgentRoot) -or [string]::IsNullOrWhiteSpace($AgentHash)) { return } try { $settingsDir = Join-Path $AgentRoot 'Settings' if (-not (Test-Path $settingsDir -PathType Container)) { New-Item -ItemType Directory -Force -Path $settingsDir | Out-Null } $hashFile = Join-Path $settingsDir 'agent_hash.txt' Set-Content -Path $hashFile -Value $AgentHash.Trim() -Encoding UTF8 } catch {} } function Set-GitFetchHeadHash { param( [string]$ProjectRoot, [string]$CommitHash, [string]$BranchName = 'main' ) if ([string]::IsNullOrWhiteSpace($ProjectRoot) -or [string]::IsNullOrWhiteSpace($CommitHash)) { return } try { $gitDir = Join-Path $ProjectRoot '.git' if (-not (Test-Path $gitDir -PathType Container)) { New-Item -ItemType Directory -Force -Path $gitDir | Out-Null } $fetchHead = Join-Path $gitDir 'FETCH_HEAD' $branchSegment = if ([string]::IsNullOrWhiteSpace($BranchName)) { '' } else { "`tbranch '$BranchName'" } $content = "{0}{1}" -f ($CommitHash.Trim()), $branchSegment Set-Content -Path $fetchHead -Value $content -Encoding UTF8 } catch {} } function Get-ServerCurrentRepoHash { param( [Parameter(Mandatory = $true)] [string]$ServerBaseUrl ) if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { return $null } $base = $ServerBaseUrl.TrimEnd('/') $uri = "$base/api/repo/current_hash" $headers = @{ 'User-Agent' = 'borealis-agent-updater' } try { $resp = Invoke-WebRequest -Uri $uri -Method Get -Headers $headers -UseBasicParsing -ErrorAction Stop $json = $resp.Content | ConvertFrom-Json return $json } catch { return $null } } function Submit-AgentHash { param( [Parameter(Mandatory = $true)] [string]$ServerBaseUrl, [Parameter(Mandatory = $true)] [string]$AgentId, [Parameter(Mandatory = $true)] [string]$AgentHash, [string]$AgentGuid ) if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentHash)) { return } $base = $ServerBaseUrl.TrimEnd('/') $uri = "$base/api/agent/hash" $payloadBody = @{ agent_hash = $AgentHash } if (-not [string]::IsNullOrWhiteSpace($AgentId)) { $payloadBody.agent_id = $AgentId } if (-not [string]::IsNullOrWhiteSpace($AgentGuid)) { $payloadBody.agent_guid = $AgentGuid } $payload = $payloadBody | ConvertTo-Json -Depth 3 $headers = @{ 'User-Agent' = 'borealis-agent-updater' } $resp = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop try { $json = $resp.Content | ConvertFrom-Json return $json } catch { return $null } } function Sync-AgentHashRecord { param( [string]$ProjectRoot, [string]$AgentRoot, [string]$AgentHash, [string]$ServerBaseUrl, [string]$AgentId, [string]$AgentGuid, [string]$BranchName = 'main' ) if ([string]::IsNullOrWhiteSpace($AgentHash)) { return } if ($ProjectRoot) { Set-GitFetchHeadHash -ProjectRoot $ProjectRoot -CommitHash $AgentHash -BranchName $BranchName } if ($AgentRoot) { Set-StoredAgentHash -AgentRoot $AgentRoot -AgentHash $AgentHash } if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { return } Write-Host ("Submitting agent hash to server: {0}" -f $AgentHash) if ([string]::IsNullOrWhiteSpace($AgentId) -and [string]::IsNullOrWhiteSpace($AgentGuid)) { Write-Host "Agent identifier unavailable; skipping agent hash submission." -ForegroundColor DarkYellow return } try { $submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash -AgentGuid $AgentGuid if ($submitResult -and ($submitResult.status -eq 'ok')) { Write-Host "Server agent_hash database record updated successfully." } elseif ($submitResult -and ($submitResult.status -eq 'ignored')) { Write-Host "Server ignored agent_hash update (agent not registered)." -ForegroundColor DarkYellow } elseif ($submitResult) { Write-Host "Server agent_hash update response unrecognized." -ForegroundColor DarkYellow } } catch { Write-Verbose ("Failed to submit agent hash: {0}" -f $_.Exception.Message) } } function Invoke-BorealisUpdate { param( [Parameter(Mandatory = $true)] [string]$GitExe, [Parameter(Mandatory = $true)] [string]$RepositoryUrl, [Parameter(Mandatory = $true)] [string]$TargetHash, [string]$BranchName = 'main', [switch]$Silent ) if ([string]::IsNullOrWhiteSpace($TargetHash)) { throw 'Target commit hash is required for Borealis update.' } $preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR" $preserveBackupPath = Join-Path $scriptDir "Update_Staging\Tesseract-OCR" $ansibleEePath = Join-Path $scriptDir "Agent\Ansible_EE" $ansibleEeBackupPath = Join-Path $scriptDir "Update_Staging\Ansible_EE" Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" { if (Test-Path $preservePath) { $stagingPath = Join-Path $scriptDir "Update_Staging" if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } Move-Item -Path $preservePath -Destination $preserveBackupPath -Force } } Run-Step "Updating: Preserve Ansible Execution Environment" { if (Test-Path $ansibleEePath) { $stagingPath = Join-Path $scriptDir "Update_Staging" if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } if (Test-Path $ansibleEeBackupPath) { Remove-Item -Path $ansibleEeBackupPath -Recurse -Force -ErrorAction SilentlyContinue } Move-Item -Path $ansibleEePath -Destination $ansibleEeBackupPath -Force } } Run-Step "Updating: Clean Up Folders to Prepare for Update" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ` (Join-Path $scriptDir "Data"), ` (Join-Path $scriptDir "Server\web-interface\src"), ` (Join-Path $scriptDir "Server\web-interface\build"), ` (Join-Path $scriptDir "Server\web-interface\public"), ` (Join-Path $scriptDir "Server\Borealis"), ` (Join-Path $scriptDir '.git') } $stagingPath = Join-Path $scriptDir "Update_Staging" $cloneDir = Join-Path $stagingPath 'repo' Run-Step "Updating: Create Update Staging Folder" { if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } if (Test-Path $cloneDir) { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $cloneDir } } Run-Step "Updating: Clone Repository Source" { $cloneArgs = @('clone','--no-tags','--depth','1') if (-not [string]::IsNullOrWhiteSpace($BranchName)) { $cloneArgs += @('--branch', $BranchName) } $cloneArgs += @($RepositoryUrl, $cloneDir) Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $stagingPath -Arguments $cloneArgs | Out-Null } Run-Step "Updating: Checkout Target Revision" { $normalizedHash = $TargetHash.Trim() $haveHash = $false try { Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('rev-parse', $normalizedHash) | Out-Null $haveHash = $true } catch { $haveHash = $false } if (-not $haveHash) { Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('fetch','origin',$normalizedHash) | Out-Null } if ([string]::IsNullOrWhiteSpace($BranchName)) { Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('checkout', $normalizedHash) | Out-Null } else { Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('checkout','-B',$BranchName,$normalizedHash) | Out-Null } } Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" { Get-ChildItem -Path $cloneDir -Force | ForEach-Object { $destination = Join-Path $scriptDir $_.Name if ($_.PSIsContainer) { Copy-Item -Path $_.FullName -Destination $destination -Recurse -Force } else { Copy-Item -Path $_.FullName -Destination $scriptDir -Force } } } Run-Step "Updating: Restore Tesseract-OCR Folder" { $restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints" if (Test-Path $preserveBackupPath) { if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null } Move-Item -Path $preserveBackupPath -Destination $restorePath -Force } } Run-Step "Updating: Restore Ansible Execution Environment" { $restorePath = Join-Path $scriptDir "Agent" if (Test-Path $ansibleEeBackupPath) { if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null } Move-Item -Path $ansibleEeBackupPath -Destination $restorePath -Force } } Run-Step "Updating: Clean Up Update Staging Folder" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $stagingPath } if (-not $Silent) { Write-Host "Unattended Borealis update completed." -ForegroundColor Green } } function Invoke-BorealisAgentUpdate { Write-Host "===============================================" Write-Host " Borealis - Automation Platform Updater Script " Write-Host "===============================================" $agentRootCandidate = Join-Path $scriptDir 'Agent\Borealis' $agentRoot = $scriptDir if (Test-Path $agentRootCandidate -PathType Container) { try { $agentRoot = (Resolve-Path -Path $agentRootCandidate -ErrorAction Stop).Path } catch { $agentRoot = $agentRootCandidate } } $agentGuid = Get-AgentGuid -AgentRoot $agentRoot if ($agentGuid) { Write-Host ("Agent GUID: {0}" -f $agentGuid) } else { Write-Host "Warning: No agent GUID detected - Please deploy the agent, associating it with a Borealis server then try running the updater script again." -ForegroundColor Yellow Write-Host "⚠️ Borealis update aborted." return } $gitExe = Get-GitExecutablePath -ProjectRoot $scriptDir $currentHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot -GitExe $gitExe $serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot $agentId = Get-AgentServiceId -AgentRoot $agentRoot $serverRepoInfo = Get-ServerCurrentRepoHash -ServerBaseUrl $serverBaseUrl $serverHash = '' $serverBranch = 'main' if ($serverRepoInfo) { try { $serverHash = (($serverRepoInfo.sha) -as [string]).Trim() } catch { $serverHash = '' } try { $branchCandidate = (($serverRepoInfo.branch) -as [string]).Trim() if ($branchCandidate) { $serverBranch = $branchCandidate } } catch { $serverBranch = 'main' } } $updateMode = $env:update_mode if ($updateMode) { $updateMode = $updateMode.ToLowerInvariant() } else { $updateMode = 'update' } $forceUpdate = $updateMode -eq 'force_update' if ($currentHash) { Write-Host ("Local Agent Hash: {0}" -f $currentHash) } else { Write-Host "Local Agent Hash: unavailable" } if ($serverHash) { Write-Host ("Borealis Server Hash: {0}" -f $serverHash) } else { Write-Host "Borealis Server Hash: unavailable" } $normalizedLocalHash = if ($currentHash) { $currentHash.Trim().ToLowerInvariant() } else { '' } $normalizedServerHash = if ($serverHash) { $serverHash.Trim().ToLowerInvariant() } else { '' } $hashesMatch = ($normalizedLocalHash -and $normalizedServerHash -and ($normalizedLocalHash -eq $normalizedServerHash)) $needsUpdate = $forceUpdate -or (-not $hashesMatch) if ($forceUpdate) { Write-Host "Force update requested; skipping hash comparison." -ForegroundColor Yellow } elseif (-not $serverHash) { Write-Host "Borealis server hash unavailable; cannot continue." -ForegroundColor Yellow Write-Host "⚠️ Borealis update aborted." return } elseif (-not $needsUpdate) { Write-Host "Local agent files already match the server repository hash." -ForegroundColor Green Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -BranchName $serverBranch Write-Host "✅ Borealis - Automation Platform Already Up-to-Date" return } else { Write-Host "Repository hash mismatch detected; update required." } if (-not ($gitExe) -or -not (Test-Path $gitExe -PathType Leaf)) { Write-Host "Bundled Git dependency not found. Run '.\\Borealis.ps1 -Agent -AgentAction repair' to bootstrap dependencies and try again." -ForegroundColor Yellow Write-Host "⚠️ Borealis update aborted." return } $mutex = $null $gotMutex = $false $managedTasks = @() try { $mutex = New-Object System.Threading.Mutex($false, 'Global\BorealisUpdate') $gotMutex = $mutex.WaitOne(0) if (-not $gotMutex) { Write-Verbose 'Another update is already running (mutex held). Exiting quietly.' Write-Host "⚠️ Borealis update already in progress on this device." return } $staging = Join-Path $scriptDir 'Update_Staging' $managedTasks = Stop-AgentScheduledTasks -TaskNames @('Borealis Agent','Borealis Agent (UserHelper)') $updateSucceeded = $false try { Invoke-BorealisUpdate -GitExe $gitExe -RepositoryUrl $repositoryUrl -TargetHash $serverHash -BranchName $serverBranch -Silent $updateSucceeded = $true } finally { if ($managedTasks.Count -gt 0) { Start-AgentScheduledTasks -TaskNames $managedTasks } } if (-not $updateSucceeded) { throw 'Borealis update failed.' } $postUpdateInfo = Get-ServerCurrentRepoHash -ServerBaseUrl $serverBaseUrl if ($postUpdateInfo) { try { $refreshedSha = (($postUpdateInfo.sha) -as [string]).Trim() if ($refreshedSha) { $serverHash = $refreshedSha } } catch {} try { $branchCandidate = (($postUpdateInfo.branch) -as [string]).Trim() if ($branchCandidate) { $serverBranch = $branchCandidate } } catch {} } $newHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot -GitExe $gitExe $normalizedNewHash = if ($newHash) { $newHash.Trim().ToLowerInvariant() } else { '' } $normalizedServerHash = if ($serverHash) { $serverHash.Trim().ToLowerInvariant() } else { '' } if ($normalizedServerHash -and (-not $normalizedNewHash -or $normalizedNewHash -ne $normalizedServerHash)) { $newHash = $serverHash $normalizedNewHash = $normalizedServerHash } elseif (-not $newHash -and $serverHash) { $newHash = $serverHash } if ($newHash) { Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -BranchName $serverBranch } else { Write-Host "Unable to determine repository hash for submission; server hash not updated." -ForegroundColor DarkYellow } Write-Host "✅ Borealis - Automation Platform Successfully Updated" } finally { if ($mutex -and $gotMutex) { $mutex.ReleaseMutex() | Out-Null } if ($mutex) { $mutex.Dispose() } } } Invoke-BorealisAgentUpdate