From eb472c4739ab71299cd133977911b47c858a6dc1 Mon Sep 17 00:00:00 2001 From: vartaxe <11176349+vartaxe@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:28:58 +0200 Subject: [PATCH] Preserve reviewed cleanup and regression coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHECKSUMS.txt | 8 +- Scripts/Copy-OSDLogToFileShare.ps1 | 115 ++++++++++++++++++++--------- Tests/Runtime.Tests.ps1 | 100 +++++++++++++++++++++++-- docs/development.md | 14 ++++ docs/release-process.md | 2 +- 5 files changed, 194 insertions(+), 45 deletions(-) diff --git a/CHECKSUMS.txt b/CHECKSUMS.txt index 30b6afe..c0a123c 100644 --- a/CHECKSUMS.txt +++ b/CHECKSUMS.txt @@ -7,6 +7,7 @@ 229B99CB364794905604F3DF1AF743818B87BEF3285AEA2D8F9E2EF7110BEBE6 LICENSE 22D6D77DC30B753FC74E513EFE398EF4DA6E8A6AE62D0D3DE0C922F03A1ABC75 .github/CODEOWNERS 29648B9E8D00A9B27A6264289E5B10703B14E80C036E91FF8C00E8BBCD2E5800 RELEASE-NOTES.md +2BDA9BE3955A5C5CB94DA6A61A251776DC3135B4235357788EC32D93C6659EF6 docs/release-process.md 2CC8992E8DA313F6CBC6D7751BB734BBECBA643B795B37D34057B4DE553F8C7F .gitignore 2E2102953B196CE96FA75D81444AF56E3A249A5E5B07C9FB181F65BC0DB09489 .editorconfig 33E160714B21F67E4CF6A4758756277A6DB8EFA7F5FD181A1176C1D159B7D2C4 _layouts/default.html @@ -14,11 +15,11 @@ 35388D74DC4D7174B5AC058CCE5FB512C88240DF4DB086EE0A81C9AFA3613717 .vscode/tasks.json 38A871D48BEB9A98E14B4D06B3056556C77924C448FC9DFCF70BF1DE22405F54 assets/banner-compact.svg 3A844B374BDB13EAFC32E96ACD1562E463BC740867741CCA374B086C65F90D4F .gitattributes +3F0BE5B3E0E2374698A1CA90E69F31B7B0B11A5B5801C40DBF9FB11EAD777FAF Tests/Runtime.Tests.ps1 440BC84BE1D09C5CD51366D32552B8318B8EB03D67ED3E02F8B4B179E087834D docs/logging.md 468E4648770DF273D018BC90F5DABD85E00BD5BBE6EFD9106EB436E278A8FE78 .github/workflows/ci.yml 4B1611351ADB567EAB568A68F6AFFA34BEC6652B600FABD327DDD9046A8B73FF docs/compatibility.md 51CA47A0CB49C782EC8BE60AA583A6EA828160347E1F44649506C300AC097670 assets/banner.svg -52E276805DC7BF24E12221266965614B2BBFC50270CFF3C2AD2B103B755CEF0F Scripts/Copy-OSDLogToFileShare.ps1 56531E99444653248D308A7918DAE6A7D8F8C0FB3B41FE055200FDA8538BECC6 assets/branding.json 59854984853104DF5C353E2F681A15FC7924742F9A2E468C29AF248DCE45CE03 VERSION 5EDEC4DF1E3DCD2B4995EFC7C49F18A2FB25E954426A59F9B93DC4FF4F74A712 assets/task-sequence-flow-compact.svg @@ -29,6 +30,7 @@ 6B6C7716FFCC77DF0FFA9D5E082E5040EDD2C3659C3842983D049E623B3821EF examples/README.md 6EC4415F361E7B41773C095592E4E5C2FC2A4F7D0D6F5C74BB1EA01A356A1C92 .github/ISSUE_TEMPLATE/bug_report.yml 6F255CD182D60581CAAC0CE468C47C32A26A9A1034EAED66031CF728412A908A Tests/Branding.Tests.ps1 +7B5EE02DF0E20B53A806BED3B2760281439E8CC656EA62024F92E5F1BD3AA626 docs/development.md 7C35ABD79FE1F5346C10969D7F5CD0DEDF5D2CE3850D997AE3A3DB387FBFD565 CODE_OF_CONDUCT.md 7C48241074306AA35FF34F2E4C313B993ADA30A1033085D2D9CD00766908825F assets/README.md 7F00EF620F17FF1658F28201FA68203052E7F3F5DFED5593314157368E458C2B assets/copylog-flow-compact.svg @@ -40,14 +42,13 @@ 9D274A652C495A1368B760FFBCCD0CF5BA61A2C2F263B37A8AAF3C2607DC4F71 docs/distribution.md 9FE08577B65ED213A330F8B3BDA113478BEF85A043AB5A298A0939038BE918CB .github/ISSUE_TEMPLATE/config.yml A047D4BF5DC341B6E460F0D3EABD52AA884ED92F0EE5A2B86847B3AEB3486609 assets/folder-arrow-right.svg -AE0EBC700A81F090A59935F13672E9109DE6616D49E8FADC36003028DEB94202 docs/development.md +A24A15757CBDD393EE1029D1D00FFA91913F2DAE9E596657EB2BFB78B513DC69 Scripts/Copy-OSDLogToFileShare.ps1 B742325E364BD5B9A311B00BEC3016E30B13274A1A4E8326A131A00E54EA7204 assets/favicon.svg B8487D129B5573E9D7C7239FBF916D73373FC331F431D397194801FD08CA4812 SUPPORT.md B89C96E15A6A3E62D37649DDDD383BE8A7FECA7CA3ABD3F2911956BA39E1671D _config.yml BB11493CAE1C3A5399A3259591473A35C71682AF903DE64C739AD296C5A4B014 .github/workflows/release.yml BE400903F0FDBF54CA82A05AA16EC426300331EB21E28E894AD149B36DD8AE98 build/Invoke-Validation.ps1 BF528DC4807B5CD45B9513F4154551F3BD8BFDAD9BB4BA12A80C3FCBB7283063 .vscode/extensions.json -C1D445D112ECB795B5BC9E2ADE0F7289AEC0F4B20AF6274B28F02F8D90C64721 Tests/Runtime.Tests.ps1 C92B63FE51F8793EECE9953ECD496046F6180F740D50E60C36628ECACF09EF0C Tests/Validation.Tests.ps1 CD206D807913077F8D0786FB344CC37758AE3080922EAA7AEFF0B78ABAB0665C .github/ISSUE_TEMPLATE/feature_request.yml CE5DD8FA7FF72122B07DB4CA7CD5A35656ED0D1A813EC8568AA448F3227401AF CONTRIBUTING.md @@ -56,7 +57,6 @@ D53A73DF9FAEC678D2471E63C070672BE9A6EAC355B451DF9BF3776802A47446 assets/task-se DF3A074261544800B739DEE93B071DE5383EFE796B874D7673AD69A5548F7A4A .github/FUNDING.yml E04EB0FF50646904CEF5B2C142F5D84084DC5AA9A339D2D3A6BA9FCF0D2A1D7E index.md E204B2838AA20F8FEBDF74129FDC504C69809CF7044D03E90866E36BAD023DA6 SECURITY.md -ED325EAC9FDC9E81E30D660A7F94659C0D0D6993193E1287D1C273395E1CE8D0 docs/release-process.md EE0B804F022C45EAC607A57181B672537C569787448D8C4BB31222CB50F7D43D Tests/Documentation.Tests.ps1 FB8E54D166ABBAE9FE74FBC83C3CEC617C245EE5330E9E09724DEE35D7C0C313 docs/troubleshooting.md FBAF37306C261D6213F85AAE6A8B26233906E6BC48F345F9BC546D89538D226A docs/deployment.md diff --git a/Scripts/Copy-OSDLogToFileShare.ps1 b/Scripts/Copy-OSDLogToFileShare.ps1 index 6858693..e8b4350 100644 --- a/Scripts/Copy-OSDLogToFileShare.ps1 +++ b/Scripts/Copy-OSDLogToFileShare.ps1 @@ -213,7 +213,8 @@ function Get-SafeErrorMessage { 'Per-connection SMB controls are unavailable. An explicit AllowUnverifiedSmb decision is required.', 'Per-connection NTLM blocking is unavailable. An explicit AllowNtlmV2 decision with an existing NTLMv2-only client policy is required.', 'AllowNtlmV2 requires an existing LmCompatibilityLevel of 3 through 5. No policy was changed.', - 'An SMB mapping already exists for the destination. It will not be reused or removed.' + 'An SMB mapping already exists for the destination. It will not be reused or removed.', + 'SMB connection cleanup failed after verified upload. Pending local archive retained.' ) if ($Exception.Message -cin $KnownMessages) { return $Exception.Message @@ -407,6 +408,26 @@ function Add-LogSource { }) } +function Get-LogSourceResult { + param( + [Parameter(Mandatory = $true)] + $Source, + + [Parameter(Mandatory = $true)] + [string]$Status, + + [AllowEmptyString()] + [string]$Message = '' + ) + + return [pscustomobject]@{ + Name = $Source.Name + Source = $Source.Path + Status = $Status + Message = $Message + } +} + function Copy-LogSource { param( [Parameter(Mandatory = $true)] @@ -424,22 +445,12 @@ function Copy-LogSource { try { if ([string]::IsNullOrWhiteSpace($Source.Path)) { - [void]$ManifestItems.Add([pscustomobject]@{ - Name = $Source.Name - Source = $Source.Path - Status = 'NotFound' - Message = 'Source path is empty.' - }) + [void]$ManifestItems.Add((Get-LogSourceResult -Source $Source -Status 'NotFound' -Message 'Source path is empty.')) return } if (-not (Test-Path -LiteralPath $Source.Path)) { - [void]$ManifestItems.Add([pscustomobject]@{ - Name = $Source.Name - Source = $Source.Path - Status = 'NotFound' - Message = '' - }) + [void]$ManifestItems.Add((Get-LogSourceResult -Source $Source -Status 'NotFound')) return } @@ -461,21 +472,11 @@ function Copy-LogSource { Copy-Item -LiteralPath $Source.Path -Destination $Destination -Force -ErrorAction Stop } - [void]$ManifestItems.Add([pscustomobject]@{ - Name = $Source.Name - Source = $Source.Path - Status = 'Collected' - Message = '' - }) + [void]$ManifestItems.Add((Get-LogSourceResult -Source $Source -Status 'Collected')) } catch { $SafeError = Get-SafeErrorMessage -ErrorRecord $_ - [void]$ManifestItems.Add([pscustomobject]@{ - Name = $Source.Name - Source = $Source.Path - Status = 'Error' - Message = $SafeError - }) + [void]$ManifestItems.Add((Get-LogSourceResult -Source $Source -Status 'Error' -Message $SafeError)) Write-Log -Level 'WARN' -Message "Source collection failed: $($Source.Path); $SafeError" } } @@ -696,6 +697,9 @@ function Send-Archive { $DriveName = 'OSDLOG' + ([guid]::NewGuid().ToString('N').Substring(0, 8)) $Mapped = $false $DriveCreated = $false + $RemoteArchive = $null + $TransferError = $null + $CleanupFailures = New-Object System.Collections.ArrayList try { if (-not $AllowUnverifiedSmb -and $null -eq (Get-Command Get-SmbConnection -ErrorAction SilentlyContinue)) { @@ -731,24 +735,55 @@ function Send-Archive { throw "Remote archive size mismatch. Local=$LocalLength Remote=$RemoteLength" } - return $RemoteArchive + } + catch { + $TransferError = $_ } finally { - if ($null -ne $MappingOptions) { - $MappingOptions.Clear() + try { + if ($Mapped) { + Remove-SmbMapping -RemotePath $PathInfo.ShareRoot -Force -ErrorAction Stop + } + } + catch { + [void]$CleanupFailures.Add('SMB mapping') + Write-Log -Level 'ERROR' -Message "SMB mapping cleanup failed: $(Get-SafeErrorMessage -ErrorRecord $_)" + } + try { + if ($DriveCreated) { + Remove-PSDrive -Name $DriveName -Force -ErrorAction Stop + } } - if ($Mapped) { - Remove-SmbMapping -RemotePath $PathInfo.ShareRoot -Force -ErrorAction Stop + catch { + [void]$CleanupFailures.Add('PSDrive') + Write-Log -Level 'ERROR' -Message "PSDrive cleanup failed: $(Get-SafeErrorMessage -ErrorRecord $_)" } - if ($DriveCreated) { - Remove-PSDrive -Name $DriveName -Force -ErrorAction Stop + try { + if ($null -ne $MappingOptions) { + $MappingOptions.Clear() + } } + catch { + [void]$CleanupFailures.Add('mapping options') + Write-Log -Level 'ERROR' -Message "SMB mapping option cleanup failed: $(Get-SafeErrorMessage -ErrorRecord $_)" + } + } + + if ($null -ne $TransferError) { + throw $TransferError + } + + return [pscustomobject]@{ + RemoteArchive = $RemoteArchive + CleanupSucceeded = ($CleanupFailures.Count -eq 0) + CleanupFailures = @($CleanupFailures) } } $ExitCode = 1 $StagingRoot = $null $ArchivePath = $null +$UploadVerifiedWithCleanupFailure = $false try { Add-Type -AssemblyName System.IO.Compression.FileSystem @@ -855,9 +890,13 @@ try { for ($Attempt = 1; $Attempt -le $RetryCount; $Attempt++) { try { Write-Log -Message "Upload attempt $Attempt of $RetryCount." - $RemoteArchive = Send-Archive -ArchivePath $ArchivePath -DestinationShare $DestinationShare -Credential $script:Credential -TimeoutSeconds $TimeoutSeconds - Write-Log -Message "Upload verified: $RemoteArchive" + $UploadResult = Send-Archive -ArchivePath $ArchivePath -DestinationShare $DestinationShare -Credential $script:Credential -TimeoutSeconds $TimeoutSeconds + Write-Log -Message "Upload verified: $($UploadResult.RemoteArchive)" $Uploaded = $true + if (-not $UploadResult.CleanupSucceeded) { + $UploadVerifiedWithCleanupFailure = $true + Write-Log -Level 'ERROR' -Message "Upload was verified, but SMB connection cleanup failed: $($UploadResult.CleanupFailures -join ', ')." + } break } catch { @@ -871,6 +910,9 @@ try { if (-not $Uploaded) { throw "Upload failed after $RetryCount attempt(s). Pending local archive retained: $ArchivePath" } + if ($UploadVerifiedWithCleanupFailure) { + throw 'SMB connection cleanup failed after verified upload. Pending local archive retained.' + } Remove-Item -LiteralPath $StagingRoot -Recurse -Force -ErrorAction Stop Remove-Item -LiteralPath $ArchivePath -Force -ErrorAction Stop @@ -885,7 +927,10 @@ finally { $script:Credential = $null } -if ($ExitCode -eq 0) { +if ($UploadVerifiedWithCleanupFailure) { + Write-Output 'OSD log archive uploaded and verified, but SMB connection cleanup failed. See CopyOSDLogs.log.' +} +elseif ($ExitCode -eq 0) { Write-Output 'OSD log archive uploaded and verified successfully.' } else { diff --git a/Tests/Runtime.Tests.ps1 b/Tests/Runtime.Tests.ps1 index 4466362..3882c85 100644 --- a/Tests/Runtime.Tests.ps1 +++ b/Tests/Runtime.Tests.ps1 @@ -44,6 +44,23 @@ Describe 'Runtime behavior' { } Describe 'Collection and archive behavior' { + It 'keeps the manifest result schema for ' -Tag 'Cleanup' -ForEach @( + @{ Status = 'NotFound'; Message = 'Source path is empty.' } + @{ Status = 'Collected'; Message = '' } + @{ Status = 'Error'; Message = 'Safe failure category.' } + ) { + $Source = [pscustomobject]@{ Name = 'TS'; Path = 'C:\source[1]'; Recurse = $true } + $Output = @(Get-LogSourceResult -Source $Source -Status $Status -Message $Message) + $Output.Count | Should -Be 1 + $Output[0] | Should -BeOfType [pscustomobject] + ($Output[0].PSObject.Properties.Name -join ',') | Should -BeExactly 'Name,Source,Status,Message' + $Output[0].Name | Should -BeExactly $Source.Name + $Output[0].Source | Should -BeExactly $Source.Path + $Output[0].Status | Should -BeExactly $Status + $Output[0].Message | Should -BeExactly $Message + (Get-LogSourceResult -Source $Source -Status $Status).Message | Should -BeExactly '' + } + It 'accepts an initially empty source list and missing optional paths' { $Sources = New-Object System.Collections.ArrayList Add-LogSource -List $Sources -Name Missing -Path '' @@ -60,10 +77,31 @@ Describe 'Runtime behavior' { $Source = [pscustomobject]@{ Name = 'TS'; Path = $SourceRoot; Recurse = $true } Copy-LogSource -Source $Source -DestinationRoot $TestDrive -ManifestItems $Manifest $Manifest[0].Status | Should -Be 'Collected' + $Manifest.Count | Should -Be 1 + $Manifest[0].Name | Should -BeExactly 'TS' + $Manifest[0].Source | Should -BeExactly $SourceRoot + $Manifest[0].Message | Should -BeExactly '' Get-Content -LiteralPath (Join-Path $TestDrive 'TS\smsts[1].log') | Should -Be 'first' Get-Content -LiteralPath (Join-Path $TestDrive 'TS\nested\more.log') | Should -Be 'second' } + It 'records a missing nonempty path without creating a destination or emitting output' -Tag 'Cleanup' { + $Source = [pscustomobject]@{ + Name = 'Missing' + Path = Join-Path $TestDrive 'not-present[1]' + Recurse = $true + } + $Manifest = New-Object System.Collections.ArrayList + $Output = @(Copy-LogSource -Source $Source -DestinationRoot $TestDrive -ManifestItems $Manifest) + $Output.Count | Should -Be 0 + $Manifest.Count | Should -Be 1 + $Manifest[0].Name | Should -BeExactly $Source.Name + $Manifest[0].Source | Should -BeExactly $Source.Path + $Manifest[0].Status | Should -BeExactly 'NotFound' + $Manifest[0].Message | Should -BeExactly '' + Test-Path -LiteralPath (Join-Path $TestDrive 'Missing') | Should -BeFalse + } + It 'does not copy child directories for a nonrecursive source' { $SourceRoot = Join-Path $TestDrive 'flat-source' [void](New-Item -ItemType Directory -Path (Join-Path $SourceRoot 'nested') -Force) @@ -90,12 +128,17 @@ Describe 'Runtime behavior' { $Manifest = New-Object System.Collections.ArrayList Copy-LogSource -Source ([pscustomobject]@{Name = 'Absent'; Path = ''; Recurse = $true }) -DestinationRoot $TestDrive -ManifestItems $Manifest $Manifest[0].Status | Should -Be 'NotFound' + $Manifest[0].Message | Should -BeExactly 'Source path is empty.' $File = Join-Path $TestDrive 'denied.log' Set-Content $File 'content' Mock Copy-Item { throw 'Access denied' } $Output = @(Copy-LogSource -Source ([pscustomobject]@{Name = 'Denied'; Path = $File; Recurse = $false }) -DestinationRoot $TestDrive -ManifestItems $Manifest) $Output.Count | Should -Be 0 $Manifest[1].Status | Should -Be 'Error' + $Manifest.Count | Should -Be 2 + $Manifest[1].Name | Should -BeExactly 'Denied' + $Manifest[1].Source | Should -BeExactly $File + $Manifest[1].Message | Should -BeExactly 'Operation failed (RuntimeException); raw exception details are omitted to protect credentials.' } It 'writes a real ZIP containing the manifest and collected content' { @@ -492,7 +535,7 @@ Describe 'Runtime behavior' { $script:AllowUnverifiedSmb = $true { Send-Archive @script:Upload } | Should -Throw '*AllowNtlmV2*' $script:AllowNtlmV2 = $true - Send-Archive @script:Upload | Should -Be '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + (Send-Archive @script:Upload).RemoteArchive | Should -Be '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' Should -Invoke New-PSDrive -Times 1 -Exactly Should -Invoke Remove-PSDrive -Times 1 -Exactly } @@ -555,9 +598,24 @@ Describe 'Runtime behavior' { It 'returns only the remote path on successful byte-size verification' { $Output = @(Send-Archive @script:Upload) $Output.Count | Should -Be 1 - $Output[0] | Should -Be '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + $Output[0].RemoteArchive | Should -Be '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + $Output[0].CleanupSucceeded | Should -BeTrue Should -Invoke New-SmbMapping -Times 1 -Exactly -ParameterFilter { $RequireIntegrity -and $RequirePrivacy } } + It 'reports PSDrive cleanup failure after a verified copy without copying again' -Tag Regression { + $script:MappingCommand = $null + $script:AllowUnverifiedSmb = $true + $script:AllowNtlmV2 = $true + Mock Remove-PSDrive { throw 'Cleanup denied' } + + $Result = Send-Archive @script:Upload + + $Result.RemoteArchive | Should -Be '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + $Result.CleanupSucceeded | Should -BeFalse + $Result.CleanupFailures | Should -Contain 'PSDrive' + Should -Invoke Copy-Item -Times 1 -Exactly + Should -Invoke Remove-PSDrive -Times 1 -Exactly + } } Describe 'Orchestration with mocked Task Sequence and network' { @@ -587,7 +645,13 @@ Describe 'Runtime behavior' { ProgramDataRoot=$script:ProgramDataRoot; SystemDriveRoot=$script:SystemDriveRoot } } Mock Start-Sleep {} - Mock Send-Archive { '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' } + Mock Send-Archive { + [pscustomobject]@{ + RemoteArchive = '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + CleanupSucceeded = $true + CleanupFailures = @() + } + } [void](New-Item -Path (Join-Path $script:WindowsRoot 'CCM\Logs') -ItemType Directory -Force) Set-Content -LiteralPath (Join-Path $script:WindowsRoot 'CCM\Logs\client.log') -Value 'client log' } @@ -631,7 +695,11 @@ Describe 'Runtime behavior' { Mock Send-Archive { $script:ObservedCredential = $Credential $script:ObservedScriptCredential = [object]::ReferenceEquals($Credential, $script:Credential) - '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + [pscustomobject]@{ + RemoteArchive = '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + CleanupSucceeded = $true + CleanupFailures = @() + } } $Shadow = New-Object System.Management.Automation.PSCredential ( @@ -709,6 +777,24 @@ Describe 'Runtime behavior' { $script:ObservedExitCode | Should -Be 1 @(Get-ChildItem -LiteralPath $script:Pending -Filter *.zip).Count | Should -Be 1 } + It 'does not re-upload after a verified copy reports SMB cleanup failure' -Tag Regression { + Mock Send-Archive { + [pscustomobject]@{ + RemoteArchive = '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + CleanupSucceeded = $false + CleanupFailures = @('PSDrive') + } + } + + & $script:MainBody + + $script:ObservedExitCode | Should -Be 1 + Should -Invoke Send-Archive -Times 1 -Exactly + Should -Invoke Start-Sleep -Times 0 -Exactly + @(Get-ChildItem -LiteralPath $script:Pending -Filter *.zip).Count | Should -Be 1 + (Get-Content -LiteralPath $script:LogPath -Raw) | Should -Match 'Upload was verified, but SMB connection cleanup failed: PSDrive' + (Get-Content -LiteralPath $script:LogPath -Raw) | Should -Not -Match 'Upload attempt 1 failed' + } It 'succeeds after a transient failure without exhausting the retry limit' { $script:UploadCalls = 0 Mock Send-Archive { @@ -716,7 +802,11 @@ Describe 'Runtime behavior' { if ($script:UploadCalls -eq 1) { throw 'Transient failure' } - '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + [pscustomobject]@{ + RemoteArchive = '\\fileserver.contoso.com\OSDLogs$\Logs\logs.zip' + CleanupSucceeded = $true + CleanupFailures = @() + } } & $script:MainBody $script:ObservedExitCode | Should -Be 0 diff --git a/docs/development.md b/docs/development.md index f3b4317..2f45274 100644 --- a/docs/development.md +++ b/docs/development.md @@ -66,3 +66,17 @@ PSScriptAnalyzer, Pester, version/tag, and checksum gates. After any file change normalize line endings and regenerate `CHECKSUMS.txt` last using the documented [release process](release-process.md). A banner-only change still changes source hashes and must not silently replace an already published release ZIP. + +## Bounded runtime cleanup + +`Get-LogSourceResult` is the single constructor for the collection manifest's +`Name`, `Source`, `Status`, and `Message` fields. `Copy-LogSource` still appends +one result at the original success, missing-source, or error boundary, suppresses +collection output, and sanitizes failures before recording or logging them. +The helper stays inside the standalone production script; no runtime dependency, +parameter, retry, upload, cleanup, or exit-code behavior changes. + +Regression tests cover the result schema and collection outcomes. The release +guide now identifies the single maintained script and its assignment/help version +values, rather than implying duplicate production copies. Validation remains +mocked or local-only; no live SMB or ConfigMgr validation is claimed. diff --git a/docs/release-process.md b/docs/release-process.md index 8fd5473..9329555 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -10,7 +10,7 @@ Publish **v1.0.0** as a prerelease candidate. Verify its downloaded assets, then ## Prepare a release -1. Keep `VERSION`, both production-script version literals, badges, examples, and the current changelog aligned to the intended three-part numeric version. +1. Keep `VERSION`, the production script's version assignment and help header, badges, examples, and the current changelog aligned to the intended three-part numeric version. The standalone production script is maintained only in `Scripts/Copy-OSDLogToFileShare.ps1`. 2. Add a nonempty `## v1.0.0` section to `RELEASE-NOTES.md` for this release. The workflow selects the exact tag's section. For a later version, replace the numeric version everywhere it identifies current release content. 3. Use Windows PowerShell 5.1 with pinned Pester 6.2.0 and PSScriptAnalyzer 1.25.0 modules from an approved source; do not bypass publisher verification. Pester 6.2.0 supports Windows PowerShell 5.1; follow its official v5-to-v6 migration guidance when changing test behavior. See [validation prerequisites](validation.md#prerequisites-and-scope). 4. Finish all changes, normalize maintained text to the [checkout convention](#checksum-checkout-convention), and regenerate `CHECKSUMS.txt` **last**.