Skip to content

Commit 3a0f1b2

Browse files
authored
Merge pull request microsoft#1958 from tyrielv/tyrielv/streamlined-upgrade
Non-disruptive upgrade: staged install with in-process file copy
2 parents a4e8cdc + 116c1bb commit 3a0f1b2

6 files changed

Lines changed: 1009 additions & 47 deletions

File tree

.github/workflows/build.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,17 @@ jobs:
247247
with:
248248
skip: ${{ needs.validate.outputs.skip }}
249249

250+
upgrade_tests:
251+
name: Upgrade Tests
252+
needs: [validate, build]
253+
uses: ./.github/workflows/upgrade-tests.yaml
254+
with:
255+
skip: ${{ needs.validate.outputs.skip }}
256+
250257
result:
251258
runs-on: ubuntu-latest
252259
name: Build, Unit and Functional Tests Successful
253-
needs: [functional_tests]
260+
needs: [functional_tests, upgrade_tests]
254261

255262
steps:
256263
- name: Success! # for easier identification of successful runs in the Checks Required for Pull Requests
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
name: Upgrade Tests
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
skip:
7+
description: 'URL of a previous successful run; if non-empty, all steps are skipped'
8+
required: false
9+
type: string
10+
default: ''
11+
lkg_release_tag:
12+
description: 'Tag of the last known good release to upgrade from (default: latest release)'
13+
required: false
14+
type: string
15+
default: ''
16+
17+
permissions:
18+
contents: read
19+
actions: read
20+
21+
jobs:
22+
upgrade_test:
23+
runs-on: windows-2025
24+
name: Upgrade
25+
timeout-minutes: 30
26+
27+
strategy:
28+
matrix:
29+
configuration: [ Debug ]
30+
scenario:
31+
- staging-upgrade
32+
- clean-upgrade
33+
- double-staging
34+
- staging-then-clean
35+
- mount-safety-deferral
36+
fail-fast: false
37+
38+
steps:
39+
- name: Skip this job if there is a previous successful run
40+
if: inputs.skip != ''
41+
id: skip
42+
uses: actions/github-script@v9
43+
with:
44+
script: |
45+
core.info(`Skipping: There already is a successful run: ${{ inputs.skip }}`)
46+
return true
47+
48+
# -- Artifacts --
49+
50+
- name: Download LKG release installer
51+
if: steps.skip.outputs.result != 'true'
52+
shell: pwsh
53+
env:
54+
GITHUB_TOKEN: ${{ github.token }}
55+
run: |
56+
$tag = "${{ inputs.lkg_release_tag }}"
57+
if (-not $tag) {
58+
$tag = gh api repos/microsoft/VFSForGit/releases/latest --jq '.tag_name'
59+
Write-Host "Auto-detected latest release: $tag"
60+
}
61+
New-Item -ItemType Directory -Path gvfs-lkg -Force | Out-Null
62+
gh release download $tag --repo microsoft/VFSForGit --pattern "SetupGVFS*.exe" --dir gvfs-lkg
63+
64+
- name: Download Git installer
65+
if: steps.skip.outputs.result != 'true'
66+
uses: actions/download-artifact@v8
67+
with:
68+
name: MicrosoftGit
69+
path: git
70+
71+
- name: Download current GVFS installer
72+
if: steps.skip.outputs.result != 'true'
73+
uses: actions/download-artifact@v8
74+
with:
75+
name: GVFS_${{ matrix.configuration }}
76+
path: gvfs-new
77+
78+
# -- Setup --
79+
80+
- name: Install Git
81+
if: steps.skip.outputs.result != 'true'
82+
shell: cmd
83+
run: git\install.bat
84+
85+
- name: Enable ProjFS
86+
if: steps.skip.outputs.result != 'true'
87+
shell: pwsh
88+
run: |
89+
$feature = Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS
90+
if ($feature.State -ne 'Enabled') {
91+
Enable-WindowsOptionalFeature -Online -FeatureName Client-ProjFS -NoRestart
92+
}
93+
94+
# -- Test Execution --
95+
96+
- name: Run upgrade test - ${{ matrix.scenario }}
97+
if: steps.skip.outputs.result != 'true'
98+
shell: pwsh
99+
run: |
100+
$ErrorActionPreference = 'Stop'
101+
102+
$lkgInstaller = (Get-ChildItem gvfs-lkg\SetupGVFS*.exe).FullName
103+
$newInstaller = (Get-ChildItem gvfs-new\SetupGVFS*.exe).FullName
104+
$installDir = "C:\Program Files\VFS for Git"
105+
$testRepo = "https://dev.azure.com/gvfs/ci/_git/ForTests"
106+
$enlistment = "C:\gvfs-upgrade-test"
107+
108+
function Install-GVFS($installer, [string[]]$extraArgs = @()) {
109+
$logDir = "C:\temp\gvfs-install-logs"
110+
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
111+
$logFile = Join-Path $logDir "gvfs-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
112+
$allArgs = @("/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/LOG=$logFile") + $extraArgs
113+
Write-Host "Installing: $installer $($allArgs -join ' ')"
114+
# Start without -Wait: Inno Setup launches child processes
115+
# (e.g. GVFS.Service.UI) that stay running, causing -Wait to
116+
# hang. Instead, wait only for the installer process itself.
117+
$proc = Start-Process -FilePath $installer -ArgumentList $allArgs -PassThru
118+
$proc.WaitForExit()
119+
if ($proc.ExitCode -ne 0) {
120+
Get-Content $logFile -Tail 30 -ErrorAction SilentlyContinue
121+
throw "Installer failed with exit code $($proc.ExitCode)"
122+
}
123+
Write-Host "Installed successfully"
124+
}
125+
126+
function Assert-ServiceRunning {
127+
$svc = sc.exe query GVFS.Service 2>&1 | Select-String "STATE"
128+
if ($svc -notmatch "RUNNING") { throw "GVFS.Service is not running: $svc" }
129+
}
130+
131+
function Mount-TestRepo {
132+
if (Test-Path $enlistment) {
133+
& "$installDir\gvfs.exe" mount $enlistment 2>&1 | Write-Host
134+
} else {
135+
& "$installDir\gvfs.exe" clone $testRepo $enlistment 2>&1 | Write-Host
136+
}
137+
if ($LASTEXITCODE -ne 0) { throw "Mount/clone failed" }
138+
$mountProc = Get-Process -Name "GVFS.Mount" -ErrorAction SilentlyContinue
139+
if (-not $mountProc) { throw "No GVFS.Mount process after mount" }
140+
return $mountProc.Id
141+
}
142+
143+
function Assert-MountAlive($expectedPid) {
144+
$proc = Get-Process -Id $expectedPid -ErrorAction SilentlyContinue
145+
if (-not $proc -or $proc.ProcessName -ne "GVFS.Mount") {
146+
throw "Mount process $expectedPid is no longer running"
147+
}
148+
# Verify the mount is functional by accessing a file
149+
$readmePath = Join-Path $enlistment "src\Readme.md"
150+
if (-not (Test-Path $readmePath)) {
151+
throw "Mount is running but cannot access $readmePath"
152+
}
153+
}
154+
155+
function Unmount-TestRepo {
156+
& "$installDir\gvfs.exe" unmount $enlistment 2>&1
157+
Start-Sleep -Seconds 3
158+
}
159+
160+
function Restart-Service {
161+
sc.exe stop GVFS.Service | Out-Null
162+
Start-Sleep -Seconds 10
163+
sc.exe start GVFS.Service | Out-Null
164+
Start-Sleep -Seconds 10
165+
Assert-ServiceRunning
166+
}
167+
168+
function Assert-PendingUpgrade($expected) {
169+
$exists = Test-Path "$installDir\PendingUpgrade"
170+
if ($exists -ne $expected) {
171+
throw "PendingUpgrade directory: expected=$expected, actual=$exists"
172+
}
173+
}
174+
175+
# =============================================
176+
# Test scenarios
177+
# =============================================
178+
179+
switch ("${{ matrix.scenario }}") {
180+
181+
"staging-upgrade" {
182+
Write-Host "=== Scenario: Staging upgrade e2e ==="
183+
# Install LKG, mount, staging upgrade, unmount, verify completion
184+
Install-GVFS $lkgInstaller
185+
Assert-ServiceRunning
186+
$mountPid = Mount-TestRepo
187+
188+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=true")
189+
Assert-MountAlive $mountPid
190+
Assert-PendingUpgrade $true
191+
192+
Unmount-TestRepo
193+
Restart-Service
194+
Assert-PendingUpgrade $false
195+
Write-Host "PASS: Staging upgrade completed"
196+
}
197+
198+
"clean-upgrade" {
199+
Write-Host "=== Scenario: Clean upgrade (traditional) ==="
200+
Install-GVFS $lkgInstaller
201+
Assert-ServiceRunning
202+
Mount-TestRepo | Write-Host
203+
204+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=false")
205+
Assert-PendingUpgrade $false
206+
Assert-ServiceRunning
207+
Write-Host "PASS: Clean upgrade completed"
208+
}
209+
210+
"double-staging" {
211+
Write-Host "=== Scenario: Double staging install ==="
212+
# Install LKG, mount, staging install twice, verify second overwrites
213+
Install-GVFS $lkgInstaller
214+
Assert-ServiceRunning
215+
$mountPid = Mount-TestRepo
216+
217+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=true")
218+
Assert-MountAlive $mountPid
219+
Assert-PendingUpgrade $true
220+
221+
# Second staging install should overwrite PendingUpgrade
222+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=true")
223+
Assert-MountAlive $mountPid
224+
Assert-PendingUpgrade $true
225+
226+
Unmount-TestRepo
227+
Restart-Service
228+
Assert-PendingUpgrade $false
229+
Write-Host "PASS: Double staging handled correctly"
230+
}
231+
232+
"staging-then-clean" {
233+
Write-Host "=== Scenario: Staging then clean install ==="
234+
# Install LKG, mount, staging install, unmount, clean install
235+
# Verify PendingUpgrade is cleaned up by clean install
236+
Install-GVFS $lkgInstaller
237+
Assert-ServiceRunning
238+
$mountPid = Mount-TestRepo
239+
240+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=true")
241+
Assert-MountAlive $mountPid
242+
Assert-PendingUpgrade $true
243+
244+
Unmount-TestRepo
245+
# Now clean install — should remove PendingUpgrade
246+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=false")
247+
Assert-PendingUpgrade $false
248+
Assert-ServiceRunning
249+
Write-Host "PASS: Staging then clean install handled correctly"
250+
}
251+
252+
"mount-safety-deferral" {
253+
Write-Host "=== Scenario: Mount safety deferral ==="
254+
# Install LKG, mount, staging install, restart service WITH mount
255+
# running — upgrade should be deferred
256+
Install-GVFS $lkgInstaller
257+
Assert-ServiceRunning
258+
$mountPid = Mount-TestRepo
259+
260+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=true")
261+
Assert-MountAlive $mountPid
262+
Assert-PendingUpgrade $true
263+
264+
# Restart service WITHOUT unmounting — upgrade should defer
265+
Restart-Service
266+
Assert-MountAlive $mountPid
267+
Assert-PendingUpgrade $true
268+
Write-Host "Upgrade correctly deferred while mount running"
269+
270+
# Now unmount and restart — should complete
271+
Unmount-TestRepo
272+
Restart-Service
273+
Assert-PendingUpgrade $false
274+
Write-Host "PASS: Mount safety deferral works correctly"
275+
}
276+
277+
default {
278+
throw "Unknown scenario: ${{ matrix.scenario }}"
279+
}
280+
}
281+
282+
- name: Upload service logs
283+
if: always() && steps.skip.outputs.result != 'true'
284+
uses: actions/upload-artifact@v7
285+
continue-on-error: true
286+
with:
287+
name: UpgradeTest_Logs_${{ matrix.scenario }}
288+
path: |
289+
C:\ProgramData\GVFS\GVFS.Service\Logs\
290+
C:\temp\gvfs-install-logs\

0 commit comments

Comments
 (0)