From e58f376643c8c4e2fb95b320381a0786a536e895 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 3 Aug 2026 13:47:07 -0400 Subject: [PATCH] fix(installer): the adopt-existing guard never fired Stage 4 decided whether the IIS objects it was about to reconcile were its own by testing for .installed-version. Stage 2 writes that file, and stage 2 always runs first in a '-Stage all' install - so by the time the guard looked, the stamp it had just written made every server look like one this installer built, including the hand-built ones the guard exists to protect. Stage 2 now records whether a stamp was present BEFORE it writes its own, and stage 4 reads that observation. Running stage 4 alone still tests the file, which is correct there: no stage 2 has run to disturb it. Found by review, not by test - the guard has no coverage, because exercising it needs a live IIS. --- deploy/windows/installer/shopdb-install.ps1 | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/deploy/windows/installer/shopdb-install.ps1 b/deploy/windows/installer/shopdb-install.ps1 index c9d216b..5c46b35 100644 --- a/deploy/windows/installer/shopdb-install.ps1 +++ b/deploy/windows/installer/shopdb-install.ps1 @@ -149,6 +149,11 @@ try { if (-not (Test-Path $script:LogDir)) { New-Item -ItemType Directory -Path catch { $script:LogDir = $env:TEMP } $script:LogPath = Join-Path $script:LogDir ("shopdb-install-{0}.log" -f (Get-Date -Format 'yyyyMMdd-HHmmss')) $script:Created = New-Object System.Collections.ArrayList # for rollback +# Was this server already stamped by a previous run of THIS installer, as observed +# BEFORE stage 2 writes its own stamp? Stage 4 needs the answer to know whether the +# IIS objects it is about to reconcile are its own. $null means nothing has looked +# yet, which is the case when stage 4 is run on its own. +$script:PreexistingInstallStamp = $null function Write-Log { param([string] $Message, [string] $Level = 'INFO') @@ -763,6 +768,12 @@ function Invoke-Stage2 { if (-not (Test-Path $AppSource)) { Fail "application payload not found: $AppSource" } Assert-BundleIntegrity Assert-VenvMatchesWheelhouse + # Captured HERE, before the stamp below is written. Stage 4's guard against + # touching an installation this did not create tested the stamp directly, and + # stage 2 always runs first in a '-Stage all' install - so the stamp it had + # just written made every server look like one of ours and the guard never + # fired on the case it exists for. + $script:PreexistingInstallStamp = Test-Path (Join-Path $AppRoot '.installed-version') # --- Python, all users -------------------------------------------------- # A per-user install lands in %LOCALAPPDATA%, which the IIS app-pool identity @@ -1493,8 +1504,15 @@ has to come from the bundle either way. # -MountAlias would delete a live mount with no prompt and no error, and the # first sign would be the site 404ing. # - # 'Ours' means there is a version stamp, which only this installer writes. - $weBuiltThis = Test-Path (Join-Path $AppRoot '.installed-version') + # 'Ours' means there was a version stamp BEFORE this run started - only this + # installer writes one. Stage 2 records that observation; testing the file + # here instead would read the stamp stage 2 has already written. + $weBuiltThis = if ($null -ne $script:PreexistingInstallStamp) { + $script:PreexistingInstallStamp + } else { + # Stage 4 run on its own, so stage 2 has not written anything this run. + Test-Path (Join-Path $AppRoot '.installed-version') + } if (-not $WhatIfOnly) { $doomed = @() if ($MountAlias) {