Repository navigation
fix: report bootstrap outcomes consistently across retries #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -165,7 +165,7 @@ function Update-SetupProgress { | |||||
| } | ||||||
|
|
||||||
| $script:ProgressState.lastUpdated = (Get-Date).ToString('o') | ||||||
| [pscustomobject]$script:ProgressState | ConvertTo-Json -Depth 4 | Set-Content -Path $ProgressFile -Encoding UTF8 -Force | ||||||
| [pscustomobject]$script:ProgressState | ConvertTo-Json -Depth 4 | Set-Content -Path $ProgressFile -Encoding UTF8 -Force -ErrorAction Stop | ||||||
| } | ||||||
|
|
||||||
| function Add-SummaryItem { | ||||||
|
|
@@ -187,85 +187,161 @@ function Add-SummaryItem { | |||||
| }) | ||||||
| } | ||||||
|
|
||||||
| function Get-BootstrapRunResult { | ||||||
| param([string]$FailureMessage = "") | ||||||
|
|
||||||
| $records = @( | ||||||
| foreach ($id in $SetupState.steps.Keys) { | ||||||
| if ($id -eq 'summary') { continue } | ||||||
| $step = $SetupState.steps[$id] | ||||||
| $included = $id -in $RunStepIds | ||||||
| $origin = if ($step.lastRun -and $step.lastRun -ge $RunStartedAt) { 'Current run' } else { 'Previous result' } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
pwsh -NoProfile -Command '
$runStarted = "2025-11-02T01:30:00.0000000-07:00"
$stepFinished = "2025-11-02T01:15:00.0000000-08:00"
"string=$($stepFinished -ge $runStarted)"
"instant=$(([datetimeoffset]$stepFinished) -ge ([datetimeoffset]$runStarted))"
'Repository: Timpan4/declarative-windows Length of output: 191 🤖 get_repo_knowledge executed:
Length of output: 1810 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- BootstrapRun.ps1 relevant definitions and callers ---'
rg -n -C 8 'RunStartedAt|lastRun|origin|Current run|Previous result' modules/BootstrapRun.ps1
printf '%s\n' '--- timestamp format and tests ---'
rg -n -C 4 'lastRun|RunStartedAt|Current run|Previous result|DateTimeOffset|datetimeoffset' . \
-g '*.ps1' -g '*.psm1' -g '*.psd1' -g '*.json' -g '*.yml' -g '*.yaml' -g '*.md' \
-g '*Tests*' -g '*test*' | head -n 240Repository: Timpan4/declarative-windows Length of output: 12747 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact source slice ---'
sed -n '160,215p' modules/BootstrapRun.ps1
printf '%s\n' '--- timestamp producers ---'
rg -n -C 6 'lastRun\s*=|lastRun:|RunStartedAt\s*=|RunStartedAt\b|Get-Date|ToString\(' modules . \
-g '*.ps1' -g '*.psm1' -g '*.psd1' -g '*.json' -g '*.yml' -g '*.yaml' \
| head -n 260Repository: Timpan4/declarative-windows Length of output: 17069 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '160,215p' modules/BootstrapRun.ps1
rg -n -C 5 'RunStartedAt|lastRun|Current run|Previous result' modules/BootstrapRun.ps1Repository: Timpan4/declarative-windows Length of output: 3871 Compare timestamps as instants.
Proposed fix- $origin = if ($step.lastRun -and $step.lastRun -ge $RunStartedAt) { 'Current run' } else { 'Previous result' }
+ $origin = if ($step.lastRun -and ([datetimeoffset]$step.lastRun) -ge ([datetimeoffset]$RunStartedAt)) { 'Current run' } else { 'Previous result' }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| [pscustomobject]@{ | ||||||
| Step = $id | ||||||
| Status = $step.status | ||||||
| Message = $step.message | ||||||
| Origin = $origin | ||||||
| Included = $included | ||||||
| } | ||||||
| } | ||||||
| ) | ||||||
| $problems = @( | ||||||
| foreach ($record in $records) { | ||||||
| if ($record.Included -and $record.Status -notin @('done', 'skipped')) { | ||||||
| [pscustomobject]@{ | ||||||
| Category = $record.Step | ||||||
| Item = $record.Status | ||||||
| Reason = $record.Message | ||||||
| Status = if ($record.Status -eq 'unverified') { 'WARN' } else { 'FAIL' } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| foreach ($item in $FailedItems) { $item } | ||||||
| foreach ($item in $SummaryItems) { | ||||||
| if ($item.Status -in @('FAIL', 'WARN')) { | ||||||
| [pscustomobject]@{ Category = $item.Step; Item = $item.Status; Reason = $item.Message; Status = $item.Status } | ||||||
| } | ||||||
| } | ||||||
| if ($FailureMessage) { | ||||||
| [pscustomobject]@{ Category = 'Bootstrap'; Item = 'Fatal error'; Reason = $FailureMessage; Status = 'FAIL' } | ||||||
| } | ||||||
| ) | ||||||
| $failed = @($problems | Where-Object { $_.Status -ne 'WARN' }).Count -gt 0 | ||||||
| $status = if ($failed) { 'Failed' } elseif ($problems.Count) { 'Completed with warnings' } else { 'Completed' } | ||||||
| if ($DryRun -and -not $FailureMessage) { $status = 'Preview'; $failed = $false } | ||||||
| [pscustomobject]@{ | ||||||
| StartedAt = $RunStartedAt | ||||||
| Mode = if ($OptionalAppsOnly) { 'Optional apps only' } else { 'Full setup' } | ||||||
| Status = $status | ||||||
| ExitCode = if ($failed) { 1 } else { 0 } | ||||||
| Records = $records | ||||||
| Problems = $problems | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| function Write-SummaryReport { | ||||||
| param([string]$DesktopPath) | ||||||
| param([string]$DesktopPath, [Parameter(Mandatory)][object]$Result) | ||||||
|
|
||||||
| $summaryPath = Join-Path $DesktopPath "Setup Summary.txt" | ||||||
| $summaryLines = @( | ||||||
| $lines = @( | ||||||
| "Declarative Windows Setup Summary", | ||||||
| "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')", | ||||||
| "Run started: $($Result.StartedAt)", | ||||||
| "Mode: $($Result.Mode)", | ||||||
| "Result: $($Result.Status)", | ||||||
| "" | ||||||
| ) | ||||||
|
|
||||||
| foreach ($item in $SummaryItems) { | ||||||
| $statusSymbol = switch ($item.Status) { | ||||||
| "OK" { "✓" } | ||||||
| "WARN" { "⚠" } | ||||||
| "FAIL" { "✗" } | ||||||
| default { $item.Status } | ||||||
| } | ||||||
| $summaryLines += "{0} {1}: {2}" -f $statusSymbol, $item.Step, $item.Message | ||||||
| foreach ($record in $Result.Records) { | ||||||
| $scope = if ($record.Included) { 'Included' } else { 'Not selected this run' } | ||||||
| $lines += "{0}: {1} - {2} [{3}; {4}]" -f $record.Step, $record.Status, $record.Message, $record.Origin, $scope | ||||||
| } | ||||||
|
|
||||||
| Set-Content -Path $summaryPath -Value $summaryLines -Force | ||||||
| foreach ($problem in $Result.Problems) { | ||||||
| $lines += "{0} {1}: {2} - {3}" -f $problem.Status, $problem.Category, $problem.Item, $problem.Reason | ||||||
| } | ||||||
| Set-Content -LiteralPath $summaryPath -Value $lines -Force -ErrorAction Stop | ||||||
| return $summaryPath | ||||||
| } | ||||||
|
|
||||||
| function Add-FailedItem { | ||||||
| param( | ||||||
| [Parameter(Mandatory)] | ||||||
| [string]$Category, | ||||||
|
|
||||||
| [Parameter(Mandatory)] | ||||||
| [string]$Item, | ||||||
|
|
||||||
| [string]$Reason = "" | ||||||
| [Parameter(Mandatory)][string]$Category, | ||||||
| [Parameter(Mandatory)][string]$Item, | ||||||
| [string]$Reason = "", | ||||||
| [ValidateSet('FAIL', 'WARN')][string]$Status = 'FAIL' | ||||||
| ) | ||||||
|
|
||||||
| $FailedItems.Add([pscustomobject]@{ | ||||||
| Category = $Category | ||||||
| Item = $Item | ||||||
| Reason = $Reason | ||||||
| Item = $Item | ||||||
| Reason = $Reason | ||||||
| Status = $Status | ||||||
| }) | ||||||
| } | ||||||
|
|
||||||
| function Write-FailedInstallsReport { | ||||||
| param([string]$DesktopPath) | ||||||
| param([string]$DesktopPath, [Parameter(Mandatory)][object]$Result) | ||||||
|
|
||||||
| $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" | ||||||
| $lines = @( | ||||||
| "Failed Installs - $timestamp", | ||||||
| "========================================" | ||||||
| "Setup failures and warnings", | ||||||
| "Run started: $($Result.StartedAt)", | ||||||
| "Mode: $($Result.Mode)", | ||||||
| "Result: $($Result.Status)", | ||||||
| "" | ||||||
| ) | ||||||
|
|
||||||
| if ($FailedItems.Count -eq 0) { | ||||||
| $lines += "" | ||||||
| $lines += "No failures recorded. Everything installed successfully." | ||||||
| if ($Result.Problems.Count -eq 0) { | ||||||
| $lines += "No failures or warnings in the selected steps. See Setup Summary.txt for skipped and previous results." | ||||||
| } | ||||||
| else { | ||||||
| $categories = $FailedItems | Select-Object -ExpandProperty Category -Unique | ||||||
| foreach ($category in $categories) { | ||||||
| $lines += "" | ||||||
| $lines += "${category}:" | ||||||
| foreach ($entry in ($FailedItems | Where-Object { $_.Category -eq $category })) { | ||||||
| $detail = if ($entry.Reason) { " - $($entry.Reason)" } else { "" } | ||||||
| $lines += " - $($entry.Item)$detail" | ||||||
| } | ||||||
| foreach ($problem in $Result.Problems) { | ||||||
| $lines += "{0} {1}: {2} - {3}" -f $problem.Status, $problem.Category, $problem.Item, $problem.Reason | ||||||
| } | ||||||
| $lines += "" | ||||||
| $lines += "========================================" | ||||||
| $lines += "Review the items above and install/apply them manually." | ||||||
| } | ||||||
|
|
||||||
| $lines | Set-Content -Path $FailedInstallsLog -Force | ||||||
|
|
||||||
| $reportPaths = @($FailedInstallsLog) | ||||||
| if ($DesktopPath) { | ||||||
| $desktopReport = Join-Path $DesktopPath "Failed Installs.txt" | ||||||
| $lines | Set-Content -Path $desktopReport -Force | ||||||
| return $desktopReport | ||||||
| $reportPaths += $desktopReport | ||||||
| } | ||||||
| $writeErrors = @( | ||||||
| foreach ($path in $reportPaths) { | ||||||
| try { $lines | Set-Content -LiteralPath $path -Force -ErrorAction Stop } | ||||||
| catch { $_.Exception.Message } | ||||||
| } | ||||||
| ) | ||||||
| if ($writeErrors.Count) { throw ($writeErrors -join '; ') } | ||||||
| return $reportPaths[-1] | ||||||
| } | ||||||
|
|
||||||
| return $FailedInstallsLog | ||||||
| function Complete-BootstrapRun { | ||||||
| param([string]$DesktopPath, [string]$FailureMessage = "") | ||||||
|
|
||||||
| $result = Get-BootstrapRunResult -FailureMessage $FailureMessage | ||||||
| $publicationErrors = @() | ||||||
| if (-not $DryRun) { | ||||||
| try { | ||||||
| $null = Write-FailedInstallsReport -DesktopPath $DesktopPath -Result $result | ||||||
| $null = Write-SummaryReport -DesktopPath $DesktopPath -Result $result | ||||||
| } | ||||||
| catch { | ||||||
| $publicationErrors += "Report generation failed: $($_.Exception.Message)" | ||||||
| $result = Get-BootstrapRunResult -FailureMessage ((@($FailureMessage) + $publicationErrors | Where-Object { $_ }) -join '; ') | ||||||
| } | ||||||
| } | ||||||
| try { | ||||||
| Update-SetupProgress -Phase $result.Status -Status "Windows setup bootstrap: $($result.Status)" -ResetPackage -Mode 'admin' | ||||||
| } | ||||||
| catch { | ||||||
| $publicationErrors += "Progress publication failed: $($_.Exception.Message)" | ||||||
| $result = Get-BootstrapRunResult -FailureMessage ((@($FailureMessage) + $publicationErrors | Where-Object { $_ }) -join '; ') | ||||||
| } | ||||||
| if ($publicationErrors.Count -and -not $DryRun) { | ||||||
| # Publish the corrected failure result to each destination still writable. | ||||||
| foreach ($writer in @('Write-FailedInstallsReport', 'Write-SummaryReport')) { | ||||||
| try { $null = & $writer -DesktopPath $DesktopPath -Result $result } | ||||||
| catch { Write-Log "Cannot publish corrected report: $($_.Exception.Message)" -Level ERROR } | ||||||
| } | ||||||
| } | ||||||
| $level = if ($result.ExitCode) { 'ERROR' } elseif ($result.Status -eq 'Completed') { 'SUCCESS' } else { 'WARNING' } | ||||||
| Write-Log "Windows Setup Bootstrap - $($result.Status)" -Level $level | ||||||
| return $result | ||||||
| } | ||||||
|
|
||||||
| function Invoke-BootstrapRunStep { | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add the required status markers to bootstrap outputs.
Complete-BootstrapRunis now the only report path. The generated reports andC:\Setup\install.loguse text status values but do not show✓,⚠, and✗. Add one status formatter and use it in both report writers andWrite-Log.As per coding guidelines,
bootstrap.ps1must create logs that show completed (✓), skipped (⚠), and failed (✗) status.🤖 Prompt for AI Agents
Source: Coding guidelines