From 22c1b556ffd7355e9bdf90c8629ae656cb300dbe Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 3 Feb 2026 16:03:58 -0700 Subject: [PATCH 1/5] Fix PSScriptAnalyzer warnings for manifest wildcards and missing BOM Replace wildcard exports in source manifest with explicit function and alias lists to satisfy PSUseToExportFieldsInManifest. Add UTF-8 BOM to three files containing non-ASCII characters (em-dashes in comments) to resolve PSUseBOMForUnicodeEncodedFile warnings. Co-Authored-By: Claude Opus 4.5 --- source/ConnectWiseAutomateAgent.psd1 | 72 +++++++++++++++++-- .../InstallUninstall/Uninstall-CWAA.ps1 | 4 +- source/Public/Service/Repair-CWAA.ps1 | 22 +++--- source/Public/Service/Test-CWAAHealth.ps1 | 4 +- 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/source/ConnectWiseAutomateAgent.psd1 b/source/ConnectWiseAutomateAgent.psd1 index 854d81d..e999bd5 100644 --- a/source/ConnectWiseAutomateAgent.psd1 +++ b/source/ConnectWiseAutomateAgent.psd1 @@ -25,8 +25,39 @@ PowerShellVersion = '3.0' # Functions to export from this module - # Wildcard for development; ModuleBuilder writes explicit list at build time - FunctionsToExport = @('*') + # ModuleBuilder overwrites this list at build time + FunctionsToExport = @( + 'ConvertFrom-CWAASecurity', + 'ConvertTo-CWAASecurity', + 'Get-CWAAError', + 'Get-CWAAInfo', + 'Get-CWAAInfoBackup', + 'Get-CWAALogLevel', + 'Get-CWAAProbeError', + 'Get-CWAAProxy', + 'Get-CWAASettings', + 'Hide-CWAAAddRemove', + 'Install-CWAA', + 'Invoke-CWAACommand', + 'New-CWAABackup', + 'Redo-CWAA', + 'Register-CWAAHealthCheckTask', + 'Rename-CWAAAddRemove', + 'Repair-CWAA', + 'Reset-CWAA', + 'Restart-CWAA', + 'Set-CWAALogLevel', + 'Set-CWAAProxy', + 'Show-CWAAAddRemove', + 'Start-CWAA', + 'Stop-CWAA', + 'Test-CWAAHealth', + 'Test-CWAAPort', + 'Test-CWAAServerConnectivity', + 'Uninstall-CWAA', + 'Unregister-CWAAHealthCheckTask', + 'Update-CWAA' + ) # Cmdlets to export from this module CmdletsToExport = @() @@ -35,8 +66,41 @@ VariablesToExport = @() # Aliases to export from this module - # Wildcard for development; ModuleBuilder discovers [Alias()] attributes at build time - AliasesToExport = @('*') + # ModuleBuilder discovers [Alias()] attributes at build time + AliasesToExport = @( + 'ConvertFrom-LTSecurity', + 'ConvertTo-LTSecurity', + 'Get-LTErrors', + 'Get-LTLogging', + 'Get-LTProbeErrors', + 'Get-LTProxy', + 'Get-LTServiceInfo', + 'Get-LTServiceInfoBackup', + 'Get-LTServiceSettings', + 'Hide-LTAddRemove', + 'Install-LTService', + 'Invoke-LTServiceCommand', + 'New-LTServiceBackup', + 'Redo-LTService', + 'Register-LTHealthCheckTask', + 'Reinstall-CWAA', + 'Reinstall-LTService', + 'Rename-LTAddRemove', + 'Repair-LTService', + 'Reset-LTService', + 'Restart-LTService', + 'Set-LTLogging', + 'Set-LTProxy', + 'Show-LTAddRemove', + 'Start-LTService', + 'Stop-LTService', + 'Test-LTHealth', + 'Test-LTPorts', + 'Test-LTServerConnectivity', + 'Uninstall-LTService', + 'Unregister-LTHealthCheckTask', + 'Update-LTService' + ) # Private data to pass to the module specified in RootModule/ModuleToProcess PrivateData = @{ diff --git a/source/Public/InstallUninstall/Uninstall-CWAA.ps1 b/source/Public/InstallUninstall/Uninstall-CWAA.ps1 index 20e978e..6a47c03 100644 --- a/source/Public/InstallUninstall/Uninstall-CWAA.ps1 +++ b/source/Public/InstallUninstall/Uninstall-CWAA.ps1 @@ -1,4 +1,4 @@ -function Uninstall-CWAA { +function Uninstall-CWAA { <# .SYNOPSIS Completely uninstalls the ConnectWise Automate Agent from the local computer. @@ -208,7 +208,7 @@ function Uninstall-CWAA { if ($PSCmdlet.ShouldProcess("$uninstaller", 'DownloadFile')) { Write-Debug "Downloading Agent_Uninstall.exe from $uninstaller" $Script:LTServiceNetWebClient.DownloadFile($uninstaller, "${env:windir}\temp\Agent_Uninstall.exe") - # Uninstall EXE is smaller than MSI — use 80 KB threshold + # Uninstall EXE is smaller than MSI � use 80 KB threshold if (-not (Test-CWAADownloadIntegrity -FilePath "${env:windir}\temp\Agent_Uninstall.exe" -FileName 'Agent_Uninstall.exe' -MinimumSizeKB 80)) { return } diff --git a/source/Public/Service/Repair-CWAA.ps1 b/source/Public/Service/Repair-CWAA.ps1 index e762d88..1ac2158 100644 --- a/source/Public/Service/Repair-CWAA.ps1 +++ b/source/Public/Service/Repair-CWAA.ps1 @@ -1,4 +1,4 @@ -function Repair-CWAA { +function Repair-CWAA { <# .SYNOPSIS Performs escalating remediation of the ConnectWise Automate agent. @@ -6,14 +6,14 @@ function Repair-CWAA { Checks the health of the installed Automate agent and takes corrective action using an escalating strategy: - 1. If the agent is installed and healthy — no action taken. - 2. If the agent is installed but has not checked in within HoursRestart — restarts + 1. If the agent is installed and healthy � no action taken. + 2. If the agent is installed but has not checked in within HoursRestart � restarts services and waits up to 2 minutes for the agent to recover. - 3. If the agent is still not checking in after HoursReinstall — reinstalls the agent + 3. If the agent is still not checking in after HoursReinstall � reinstalls the agent using Redo-CWAA. - 4. If the agent configuration is unreadable — uninstalls and reinstalls. - 5. If the installed agent points to the wrong server — reinstalls with the correct server. - 6. If the agent is not installed — performs a fresh install from provided parameters + 4. If the agent configuration is unreadable � uninstalls and reinstalls. + 5. If the installed agent points to the wrong server � reinstalls with the correct server. + 6. If the agent is not installed � performs a fresh install from provided parameters or from backup settings. All remediation actions are logged to the Windows Event Log (Application log, @@ -106,7 +106,7 @@ function Repair-CWAA { $agentServiceExists = [bool](Get-Service 'LTService' -ErrorAction SilentlyContinue) if ($agentServiceExists) { - #region Agent is installed — check health and remediate + #region Agent is installed � check health and remediate # Verify we can read agent configuration $agentInfo = $Null @@ -114,7 +114,7 @@ function Repair-CWAA { $agentInfo = Get-CWAAInfo -EA Stop -Verbose:$False -WhatIf:$False -Confirm:$False } Catch { - # Agent config is unreadable — uninstall so we can reinstall cleanly + # Agent config is unreadable � uninstall so we can reinstall cleanly Write-Warning "Unable to read agent configuration. Uninstalling for clean reinstall." Write-CWAAEventLog -EventId 4009 -EntryType Warning -Message "Agent configuration unreadable. Uninstalling for clean reinstall. Error: $($_.Exception.Message)" @@ -194,7 +194,7 @@ function Repair-CWAA { [datetime]$lastContact = $agentInfo.HeartbeatLastReceived } Catch { - # No valid contact timestamp — treat as very old + # No valid contact timestamp � treat as very old [datetime]$lastContact = (Get-Date).AddYears(-1) } } @@ -319,7 +319,7 @@ function Repair-CWAA { #endregion } else { - #region Agent is NOT installed — attempt install + #region Agent is NOT installed � attempt install Write-Verbose 'Agent service not found. Attempting installation.' Write-CWAAEventLog -EventId 4003 -EntryType Warning -Message 'Agent not installed. Attempting installation.' diff --git a/source/Public/Service/Test-CWAAHealth.ps1 b/source/Public/Service/Test-CWAAHealth.ps1 index a7d8cba..89bb89c 100644 --- a/source/Public/Service/Test-CWAAHealth.ps1 +++ b/source/Public/Service/Test-CWAAHealth.ps1 @@ -1,4 +1,4 @@ -function Test-CWAAHealth { +function Test-CWAAHealth { <# .SYNOPSIS Performs a read-only health assessment of the ConnectWise Automate agent. @@ -60,7 +60,7 @@ function Test-CWAAHealth { } Process { - # Defaults — populated progressively as checks succeed + # Defaults � populated progressively as checks succeed $agentInstalled = $False $servicesRunning = $False $lastContact = $Null From 8214729828b4e5cd3dec70916fce88c09d76fa22 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 3 Feb 2026 16:03:58 -0700 Subject: [PATCH 2/5] Fix PSScriptAnalyzer warnings for manifest wildcards and missing BOM Replace wildcard exports in source manifest with explicit function and alias lists to satisfy PSUseToExportFieldsInManifest. Add UTF-8 BOM to three files containing non-ASCII characters (em-dashes in comments) to resolve PSUseBOMForUnicodeEncodedFile warnings. Co-Authored-By: Claude Opus 4.5 --- source/ConnectWiseAutomateAgent.psd1 | 8 +++---- .../InstallUninstall/Uninstall-CWAA.ps1 | 4 ++-- source/Public/Service/Repair-CWAA.ps1 | 22 +++++++++---------- source/Public/Service/Test-CWAAHealth.ps1 | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/source/ConnectWiseAutomateAgent.psd1 b/source/ConnectWiseAutomateAgent.psd1 index 854d81d..60d33d7 100644 --- a/source/ConnectWiseAutomateAgent.psd1 +++ b/source/ConnectWiseAutomateAgent.psd1 @@ -25,8 +25,8 @@ PowerShellVersion = '3.0' # Functions to export from this module - # Wildcard for development; ModuleBuilder writes explicit list at build time - FunctionsToExport = @('*') + # ModuleBuilder overwrites this list at build time + FunctionsToExport = @('ConvertFrom-CWAASecurity','ConvertTo-CWAASecurity','Get-CWAAError','Get-CWAAInfo','Get-CWAAInfoBackup','Get-CWAALogLevel','Get-CWAAProbeError','Get-CWAAProxy','Get-CWAASettings','Hide-CWAAAddRemove','Install-CWAA','Invoke-CWAACommand','New-CWAABackup','Redo-CWAA','Register-CWAAHealthCheckTask','Rename-CWAAAddRemove','Repair-CWAA','Reset-CWAA','Restart-CWAA','Set-CWAALogLevel','Set-CWAAProxy','Show-CWAAAddRemove','Start-CWAA','Stop-CWAA','Test-CWAAHealth','Test-CWAAPort','Test-CWAAServerConnectivity','Uninstall-CWAA','Unregister-CWAAHealthCheckTask','Update-CWAA') # Cmdlets to export from this module CmdletsToExport = @() @@ -35,8 +35,8 @@ VariablesToExport = @() # Aliases to export from this module - # Wildcard for development; ModuleBuilder discovers [Alias()] attributes at build time - AliasesToExport = @('*') + # ModuleBuilder discovers [Alias()] attributes at build time + AliasesToExport = @('ConvertFrom-LTSecurity','ConvertTo-LTSecurity','Get-LTErrors','Get-LTLogging','Get-LTProbeErrors','Get-LTProxy','Get-LTServiceInfo','Get-LTServiceInfoBackup','Get-LTServiceSettings','Hide-LTAddRemove','Install-LTService','Invoke-LTServiceCommand','New-LTServiceBackup','Redo-LTService','Register-LTHealthCheckTask','Reinstall-CWAA','Reinstall-LTService','Rename-LTAddRemove','Repair-LTService','Reset-LTService','Restart-LTService','Set-LTLogging','Set-LTProxy','Show-LTAddRemove','Start-LTService','Stop-LTService','Test-LTHealth','Test-LTPorts','Test-LTServerConnectivity','Uninstall-LTService','Unregister-LTHealthCheckTask','Update-LTService') # Private data to pass to the module specified in RootModule/ModuleToProcess PrivateData = @{ diff --git a/source/Public/InstallUninstall/Uninstall-CWAA.ps1 b/source/Public/InstallUninstall/Uninstall-CWAA.ps1 index 20e978e..6a47c03 100644 --- a/source/Public/InstallUninstall/Uninstall-CWAA.ps1 +++ b/source/Public/InstallUninstall/Uninstall-CWAA.ps1 @@ -1,4 +1,4 @@ -function Uninstall-CWAA { +function Uninstall-CWAA { <# .SYNOPSIS Completely uninstalls the ConnectWise Automate Agent from the local computer. @@ -208,7 +208,7 @@ function Uninstall-CWAA { if ($PSCmdlet.ShouldProcess("$uninstaller", 'DownloadFile')) { Write-Debug "Downloading Agent_Uninstall.exe from $uninstaller" $Script:LTServiceNetWebClient.DownloadFile($uninstaller, "${env:windir}\temp\Agent_Uninstall.exe") - # Uninstall EXE is smaller than MSI — use 80 KB threshold + # Uninstall EXE is smaller than MSI � use 80 KB threshold if (-not (Test-CWAADownloadIntegrity -FilePath "${env:windir}\temp\Agent_Uninstall.exe" -FileName 'Agent_Uninstall.exe' -MinimumSizeKB 80)) { return } diff --git a/source/Public/Service/Repair-CWAA.ps1 b/source/Public/Service/Repair-CWAA.ps1 index e762d88..1ac2158 100644 --- a/source/Public/Service/Repair-CWAA.ps1 +++ b/source/Public/Service/Repair-CWAA.ps1 @@ -1,4 +1,4 @@ -function Repair-CWAA { +function Repair-CWAA { <# .SYNOPSIS Performs escalating remediation of the ConnectWise Automate agent. @@ -6,14 +6,14 @@ function Repair-CWAA { Checks the health of the installed Automate agent and takes corrective action using an escalating strategy: - 1. If the agent is installed and healthy — no action taken. - 2. If the agent is installed but has not checked in within HoursRestart — restarts + 1. If the agent is installed and healthy � no action taken. + 2. If the agent is installed but has not checked in within HoursRestart � restarts services and waits up to 2 minutes for the agent to recover. - 3. If the agent is still not checking in after HoursReinstall — reinstalls the agent + 3. If the agent is still not checking in after HoursReinstall � reinstalls the agent using Redo-CWAA. - 4. If the agent configuration is unreadable — uninstalls and reinstalls. - 5. If the installed agent points to the wrong server — reinstalls with the correct server. - 6. If the agent is not installed — performs a fresh install from provided parameters + 4. If the agent configuration is unreadable � uninstalls and reinstalls. + 5. If the installed agent points to the wrong server � reinstalls with the correct server. + 6. If the agent is not installed � performs a fresh install from provided parameters or from backup settings. All remediation actions are logged to the Windows Event Log (Application log, @@ -106,7 +106,7 @@ function Repair-CWAA { $agentServiceExists = [bool](Get-Service 'LTService' -ErrorAction SilentlyContinue) if ($agentServiceExists) { - #region Agent is installed — check health and remediate + #region Agent is installed � check health and remediate # Verify we can read agent configuration $agentInfo = $Null @@ -114,7 +114,7 @@ function Repair-CWAA { $agentInfo = Get-CWAAInfo -EA Stop -Verbose:$False -WhatIf:$False -Confirm:$False } Catch { - # Agent config is unreadable — uninstall so we can reinstall cleanly + # Agent config is unreadable � uninstall so we can reinstall cleanly Write-Warning "Unable to read agent configuration. Uninstalling for clean reinstall." Write-CWAAEventLog -EventId 4009 -EntryType Warning -Message "Agent configuration unreadable. Uninstalling for clean reinstall. Error: $($_.Exception.Message)" @@ -194,7 +194,7 @@ function Repair-CWAA { [datetime]$lastContact = $agentInfo.HeartbeatLastReceived } Catch { - # No valid contact timestamp — treat as very old + # No valid contact timestamp � treat as very old [datetime]$lastContact = (Get-Date).AddYears(-1) } } @@ -319,7 +319,7 @@ function Repair-CWAA { #endregion } else { - #region Agent is NOT installed — attempt install + #region Agent is NOT installed � attempt install Write-Verbose 'Agent service not found. Attempting installation.' Write-CWAAEventLog -EventId 4003 -EntryType Warning -Message 'Agent not installed. Attempting installation.' diff --git a/source/Public/Service/Test-CWAAHealth.ps1 b/source/Public/Service/Test-CWAAHealth.ps1 index a7d8cba..89bb89c 100644 --- a/source/Public/Service/Test-CWAAHealth.ps1 +++ b/source/Public/Service/Test-CWAAHealth.ps1 @@ -1,4 +1,4 @@ -function Test-CWAAHealth { +function Test-CWAAHealth { <# .SYNOPSIS Performs a read-only health assessment of the ConnectWise Automate agent. @@ -60,7 +60,7 @@ function Test-CWAAHealth { } Process { - # Defaults — populated progressively as checks succeed + # Defaults � populated progressively as checks succeed $agentInstalled = $False $servicesRunning = $False $lastContact = $Null From 8d783ed3f202c802e7e8c800b299649971937b61 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 3 Feb 2026 16:55:23 -0700 Subject: [PATCH 3/5] Consolidate CI pipeline and enforce local-first testing - Resolve manifest merge conflict: single-line explicit exports - Consolidate CI test+analyze into single smoke-test job - Fix test-local.ps1 severity mismatch: fail on warnings like CI - Add mandatory validation section to CLAUDE.md - Strengthen AGENTS.md workflow with explicit validation gates - Expand copilot-instructions.md with structured test command Co-Authored-By: Claude Opus 4.5 --- .github/copilot-instructions.md | 2 +- .github/workflows/ci.yml | 54 +++++++++------------ AGENTS.md | 20 ++++++-- CLAUDE.md | 20 ++++++++ Tests/test-local.ps1 | 11 +---- source/ConnectWiseAutomateAgent.psd1 | 72 ---------------------------- 6 files changed, 61 insertions(+), 118 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a6965f6..6d921ac 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,4 +4,4 @@ This is a PowerShell 3.0+ module for managing the ConnectWise Automate Windows a Key file locations: `source/Public/` (exported), `source/Private/` (internal), `Tests/`, `.build/`. -Before committing: `./Tests/test-local.ps1` +Before committing: run `./Scripts/Invoke-QuickTest.ps1 -IncludeAnalyzer -OutputFormat Structured` and verify `success` is `true`. Before pushing: `./Tests/test-local.ps1`. See AGENTS.md for full workflow. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98311ae..50c84fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,14 @@ # semantic versioning. # # Branch strategy: -# develop push → build → test → analyze → publish prerelease → GitHub Release -# main push → build → test → analyze → publish stable → GitHub Release -# pull requests → build → test → analyze (no publish, no release) +# develop push → build → smoke-test → publish prerelease → GitHub Release +# main push → build → smoke-test → publish stable → GitHub Release +# pull requests → build → smoke-test (no publish, no release) +# +# Testing strategy: +# Full validation (build + PSScriptAnalyzer + Pester) runs LOCALLY before every +# commit via ./Tests/test-local.ps1. CI provides a safety-net smoke test only. +# See AGENTS.md for the mandatory local validation workflow. # # Versioning: # GitVersion calculates the version from git history and branch. @@ -95,8 +100,8 @@ jobs: path: output/RequiredModules retention-days: 1 - test: - name: Test + smoke-test: + name: Smoke Test needs: build runs-on: windows-latest steps: @@ -114,6 +119,17 @@ jobs: name: required-modules path: output/RequiredModules + - name: Run PSScriptAnalyzer + shell: pwsh + run: | + $env:PSModulePath = "$(Resolve-Path output/RequiredModules)" + [IO.Path]::PathSeparator + $env:PSModulePath + Import-Module PSScriptAnalyzer + $results = Invoke-ScriptAnalyzer -Path source -Recurse -Settings .PSScriptAnalyzerSettings.psd1 + if ($results) { + $results | Format-Table -AutoSize + throw "PSScriptAnalyzer found $($results.Count) issue(s)" + } + - name: Run Pester tests shell: pwsh run: | @@ -131,36 +147,12 @@ jobs: retention-days: 7 if-no-files-found: ignore - analyze: - name: PSScriptAnalyzer - needs: build - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - name: Download required modules - uses: actions/download-artifact@v4 - with: - name: required-modules - path: output/RequiredModules - - - name: Run PSScriptAnalyzer - shell: pwsh - run: | - $env:PSModulePath = "$(Resolve-Path output/RequiredModules)" + [IO.Path]::PathSeparator + $env:PSModulePath - Import-Module PSScriptAnalyzer - $results = Invoke-ScriptAnalyzer -Path source -Recurse -Severity Error,Warning -Settings .PSScriptAnalyzerSettings.psd1 - if ($results) { - $results | Format-Table -AutoSize - throw "PSScriptAnalyzer found $($results.Count) issue(s)" - } - # ─── Publish jobs ─────────────────────────────────────────────────────── publish-prerelease: name: Publish Prerelease if: github.ref == 'refs/heads/develop' && github.event_name == 'push' - needs: [build, test, analyze] + needs: [build, smoke-test] runs-on: windows-latest environment: PSGallery steps: @@ -215,7 +207,7 @@ jobs: publish-stable: name: Publish Stable if: github.ref == 'refs/heads/main' && github.event_name == 'push' - needs: [build, test, analyze] + needs: [build, smoke-test] runs-on: windows-latest environment: PSGallery steps: diff --git a/AGENTS.md b/AGENTS.md index 215c738..48858da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ The build uses [Sampler](https://github.com/gaelcolas/Sampler) with ModuleBuilde ### CI/CD -CI is intentionally lightweight (smoke test, build, publish). Full testing is local. See the header comments in `.github/workflows/ci.yml` for branch strategy and gating rules. +CI runs build + a consolidated smoke test (PSScriptAnalyzer + Pester on one runner) + publish. It is a safety net -- local validation is the primary gate. See CLAUDE.md for the mandatory validation workflow and `.github/workflows/ci.yml` for branch strategy. ### Common Patterns @@ -175,6 +175,7 @@ See `Get-Help .\Tests\test-local.ps1` for flags: `-SkipBuild`, `-SkipTests`, `-S ### Key Rules +- **Local validation is mandatory before every commit.** Run `Invoke-QuickTest.ps1 -IncludeAnalyzer` during development and `test-local.ps1` before committing. CI is a safety net, not the primary gate. - No code is complete without passing tests. A function without a test is unfinished work. - PSScriptAnalyzer zero errors required. Run against `source/`. Always use `-IncludeAnalyzer` during development. - Dual-mode testing details: `Get-Help .\Tests\Invoke-AllTests.ps1` @@ -193,10 +194,19 @@ gh issue edit --add-label ai-in-progress --remove-label ai-ready 1. Claim the issue (above), create branch: `git checkout -b feature/-short-description` 2. Read the full issue body and acceptance criteria -3. Implement, add tests, iterate with `Invoke-QuickTest.ps1 -IncludeAnalyzer -OutputFormat Structured` -4. Run `./build.ps1 -Tasks build` then `./Tests/test-local.ps1` -- build + analyze + test must all pass -5. Commit referencing the issue: `Add feature X (fixes #123)` -6. Push, open PR, update label: `gh issue edit --add-label ai-review --remove-label ai-in-progress` +3. Implement changes, adding tests as you go +4. **After each meaningful change**, validate: + ```powershell + ./Scripts/Invoke-QuickTest.ps1 -IncludeAnalyzer -OutputFormat Structured + ``` + Parse the JSON `success` field. Fix all `failedTests` and `analyzerErrors` before proceeding. +5. **Before committing**, run the full pipeline: + ```powershell + ./Tests/test-local.ps1 + ``` + All three stages (build, analyze, test) must pass. Do not commit until they do. +6. Commit referencing the issue: `Add feature X (fixes #123)` +7. Push, open PR, update label: `gh issue edit --add-label ai-review --remove-label ai-in-progress` ### Guardrails diff --git a/CLAUDE.md b/CLAUDE.md index f99baf9..4c5960d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,26 @@ For project context, architecture, build commands, code conventions, and contribution workflow, read [AGENTS.md](AGENTS.md). That is the single source of truth for all AI agents. +## Mandatory Validation + +**After each meaningful change**, run: + +```powershell +./Scripts/Invoke-QuickTest.ps1 -IncludeAnalyzer -OutputFormat Structured +``` + +Parse the JSON output. If `success` is `false`, fix all `failedTests` and `analyzerErrors` before proceeding. + +**Before every commit**, run the full pipeline: + +```powershell +./Tests/test-local.ps1 +``` + +All three stages (build, analyze, test) must pass. Do not commit until they do. + +CI is a safety net only -- failures should never first appear there. + ## Session State Use `.claude/plan.md` as a personal scratchpad for the current session. It is gitignored and not shared across agents or users. diff --git a/Tests/test-local.ps1 b/Tests/test-local.ps1 index f83133e..7c002d0 100644 --- a/Tests/test-local.ps1 +++ b/Tests/test-local.ps1 @@ -84,15 +84,8 @@ if (-not $SkipAnalyze) { if ($results) { $results | Format-Table -AutoSize - $errors = @($results | Where-Object Severity -eq 'Error') - - if ($errors.Count -gt 0) { - Write-Host "PSScriptAnalyzer found $($errors.Count) error(s)" -ForegroundColor Red - exit 1 - } - else { - Write-Host "PSScriptAnalyzer warnings found (but no errors)`n" -ForegroundColor Yellow - } + Write-Host "PSScriptAnalyzer found $($results.Count) issue(s)" -ForegroundColor Red + exit 1 } else { Write-Host "PSScriptAnalyzer PASSED - no issues found`n" -ForegroundColor Green diff --git a/source/ConnectWiseAutomateAgent.psd1 b/source/ConnectWiseAutomateAgent.psd1 index a55dee7..60d33d7 100644 --- a/source/ConnectWiseAutomateAgent.psd1 +++ b/source/ConnectWiseAutomateAgent.psd1 @@ -26,42 +26,7 @@ # Functions to export from this module # ModuleBuilder overwrites this list at build time -<<<<<<< HEAD FunctionsToExport = @('ConvertFrom-CWAASecurity','ConvertTo-CWAASecurity','Get-CWAAError','Get-CWAAInfo','Get-CWAAInfoBackup','Get-CWAALogLevel','Get-CWAAProbeError','Get-CWAAProxy','Get-CWAASettings','Hide-CWAAAddRemove','Install-CWAA','Invoke-CWAACommand','New-CWAABackup','Redo-CWAA','Register-CWAAHealthCheckTask','Rename-CWAAAddRemove','Repair-CWAA','Reset-CWAA','Restart-CWAA','Set-CWAALogLevel','Set-CWAAProxy','Show-CWAAAddRemove','Start-CWAA','Stop-CWAA','Test-CWAAHealth','Test-CWAAPort','Test-CWAAServerConnectivity','Uninstall-CWAA','Unregister-CWAAHealthCheckTask','Update-CWAA') -======= - FunctionsToExport = @( - 'ConvertFrom-CWAASecurity', - 'ConvertTo-CWAASecurity', - 'Get-CWAAError', - 'Get-CWAAInfo', - 'Get-CWAAInfoBackup', - 'Get-CWAALogLevel', - 'Get-CWAAProbeError', - 'Get-CWAAProxy', - 'Get-CWAASettings', - 'Hide-CWAAAddRemove', - 'Install-CWAA', - 'Invoke-CWAACommand', - 'New-CWAABackup', - 'Redo-CWAA', - 'Register-CWAAHealthCheckTask', - 'Rename-CWAAAddRemove', - 'Repair-CWAA', - 'Reset-CWAA', - 'Restart-CWAA', - 'Set-CWAALogLevel', - 'Set-CWAAProxy', - 'Show-CWAAAddRemove', - 'Start-CWAA', - 'Stop-CWAA', - 'Test-CWAAHealth', - 'Test-CWAAPort', - 'Test-CWAAServerConnectivity', - 'Uninstall-CWAA', - 'Unregister-CWAAHealthCheckTask', - 'Update-CWAA' - ) ->>>>>>> 22c1b556ffd7355e9bdf90c8629ae656cb300dbe # Cmdlets to export from this module CmdletsToExport = @() @@ -71,44 +36,7 @@ # Aliases to export from this module # ModuleBuilder discovers [Alias()] attributes at build time -<<<<<<< HEAD AliasesToExport = @('ConvertFrom-LTSecurity','ConvertTo-LTSecurity','Get-LTErrors','Get-LTLogging','Get-LTProbeErrors','Get-LTProxy','Get-LTServiceInfo','Get-LTServiceInfoBackup','Get-LTServiceSettings','Hide-LTAddRemove','Install-LTService','Invoke-LTServiceCommand','New-LTServiceBackup','Redo-LTService','Register-LTHealthCheckTask','Reinstall-CWAA','Reinstall-LTService','Rename-LTAddRemove','Repair-LTService','Reset-LTService','Restart-LTService','Set-LTLogging','Set-LTProxy','Show-LTAddRemove','Start-LTService','Stop-LTService','Test-LTHealth','Test-LTPorts','Test-LTServerConnectivity','Uninstall-LTService','Unregister-LTHealthCheckTask','Update-LTService') -======= - AliasesToExport = @( - 'ConvertFrom-LTSecurity', - 'ConvertTo-LTSecurity', - 'Get-LTErrors', - 'Get-LTLogging', - 'Get-LTProbeErrors', - 'Get-LTProxy', - 'Get-LTServiceInfo', - 'Get-LTServiceInfoBackup', - 'Get-LTServiceSettings', - 'Hide-LTAddRemove', - 'Install-LTService', - 'Invoke-LTServiceCommand', - 'New-LTServiceBackup', - 'Redo-LTService', - 'Register-LTHealthCheckTask', - 'Reinstall-CWAA', - 'Reinstall-LTService', - 'Rename-LTAddRemove', - 'Repair-LTService', - 'Reset-LTService', - 'Restart-LTService', - 'Set-LTLogging', - 'Set-LTProxy', - 'Show-LTAddRemove', - 'Start-LTService', - 'Stop-LTService', - 'Test-LTHealth', - 'Test-LTPorts', - 'Test-LTServerConnectivity', - 'Uninstall-LTService', - 'Unregister-LTHealthCheckTask', - 'Update-LTService' - ) ->>>>>>> 22c1b556ffd7355e9bdf90c8629ae656cb300dbe # Private data to pass to the module specified in RootModule/ModuleToProcess PrivateData = @{ From bf28e3cd0d9f69878d09c9bc26ed7f2d297ab36a Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 3 Feb 2026 17:05:21 -0700 Subject: [PATCH 4/5] Fix release job exit code leak from gh release view When gh release view returns exit code 1 (release not found), PowerShell propagates that as the step exit code. Add explicit exit 0 since the skip/proceed logic is handled via step outputs, not exit codes. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50c84fa..dd6b8c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,8 +291,10 @@ jobs: Write-Host "Release $tag already exists. Skipping." "skip=true" >> $env:GITHUB_OUTPUT } else { + Write-Host "Release $tag not found. Will create." "skip=false" >> $env:GITHUB_OUTPUT } + exit 0 - name: Download release notes if: steps.check.outputs.skip != 'true' @@ -375,8 +377,10 @@ jobs: Write-Host "Release $tag already exists. Skipping." "skip=true" >> $env:GITHUB_OUTPUT } else { + Write-Host "Release $tag not found. Will create." "skip=false" >> $env:GITHUB_OUTPUT } + exit 0 - name: Download release notes if: steps.check.outputs.skip != 'true' From 1ffb35a4f4508086e067b4af60214071bd2b144b Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 3 Feb 2026 17:38:57 -0700 Subject: [PATCH 5/5] Exclude .psd1 from PSScriptAnalyzer to avoid NullRef bug PSScriptAnalyzer intermittently throws NullReferenceException when parsing .psd1 manifest files in CI. Analyze only .ps1/.psm1 files since ModuleBuilder overwrites the manifest at build time anyway. Updated in all three analyzer locations: CI smoke-test, test-local.ps1, and Invoke-QuickTest.ps1. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 4 +++- Scripts/Invoke-QuickTest.ps1 | 11 +++++++++-- Tests/test-local.ps1 | 9 ++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd6b8c4..0275ee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,9 @@ jobs: run: | $env:PSModulePath = "$(Resolve-Path output/RequiredModules)" + [IO.Path]::PathSeparator + $env:PSModulePath Import-Module PSScriptAnalyzer - $results = Invoke-ScriptAnalyzer -Path source -Recurse -Settings .PSScriptAnalyzerSettings.psd1 + # Analyze .ps1/.psm1 only; .psd1 excluded due to PSScriptAnalyzer NullRef bug + $files = Get-ChildItem -Path source -Include '*.ps1','*.psm1' -Recurse -File + $results = $files | ForEach-Object { Invoke-ScriptAnalyzer -Path $_.FullName -Settings .PSScriptAnalyzerSettings.psd1 } if ($results) { $results | Format-Table -AutoSize throw "PSScriptAnalyzer found $($results.Count) issue(s)" diff --git a/Scripts/Invoke-QuickTest.ps1 b/Scripts/Invoke-QuickTest.ps1 index 5a89d66..fe585e4 100644 --- a/Scripts/Invoke-QuickTest.ps1 +++ b/Scripts/Invoke-QuickTest.ps1 @@ -171,12 +171,19 @@ if ($IncludeAnalyzer) { } if ($analyzePath) { - $analyzeParams = @{ Path = $analyzePath; Recurse = $true } + # Analyze .ps1/.psm1 only; .psd1 excluded due to PSScriptAnalyzer NullRef bug + $analyzeParams = @{} if (Test-Path $settingsFile) { $analyzeParams['Settings'] = $settingsFile } - $analyzeResults = Invoke-ScriptAnalyzer @analyzeParams + if (Test-Path $analyzePath -PathType Leaf) { + $analyzeResults = Invoke-ScriptAnalyzer -Path $analyzePath @analyzeParams + } + else { + $files = Get-ChildItem -Path $analyzePath -Include '*.ps1','*.psm1' -Recurse -File + $analyzeResults = $files | ForEach-Object { Invoke-ScriptAnalyzer -Path $_.FullName @analyzeParams } + } if ($analyzeResults) { $analyzerErrors = @($analyzeResults | Where-Object Severity -eq 'Error' | ForEach-Object { diff --git a/Tests/test-local.ps1 b/Tests/test-local.ps1 index 7c002d0..e8cd4a8 100644 --- a/Tests/test-local.ps1 +++ b/Tests/test-local.ps1 @@ -72,15 +72,14 @@ if (-not $SkipAnalyze) { $sourcePath = Join-Path $ProjectRoot 'source' $settingsFile = Join-Path $ProjectRoot '.PSScriptAnalyzerSettings.psd1' - $analyzeParams = @{ - Path = $sourcePath - Recurse = $true - } + # Analyze .ps1/.psm1 only; .psd1 excluded due to PSScriptAnalyzer NullRef bug + $files = Get-ChildItem -Path $sourcePath -Include '*.ps1','*.psm1' -Recurse -File + $analyzeParams = @{} if (Test-Path $settingsFile) { $analyzeParams['Settings'] = $settingsFile } - $results = Invoke-ScriptAnalyzer @analyzeParams + $results = $files | ForEach-Object { Invoke-ScriptAnalyzer -Path $_.FullName @analyzeParams } if ($results) { $results | Format-Table -AutoSize