Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
60 changes: 29 additions & 31 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -114,6 +119,19 @@ 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
# 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)"
}

- name: Run Pester tests
shell: pwsh
run: |
Expand All @@ -131,36 +149,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:
Expand Down Expand Up @@ -215,7 +209,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:
Expand Down Expand Up @@ -299,8 +293,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'
Expand Down Expand Up @@ -383,8 +379,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'
Expand Down
20 changes: 15 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand All @@ -193,10 +194,19 @@ gh issue edit <number> --add-label ai-in-progress --remove-label ai-ready

1. Claim the issue (above), create branch: `git checkout -b feature/<number>-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 <number> --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 <number> --add-label ai-review --remove-label ai-in-progress`

### Guardrails

Expand Down
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 9 additions & 2 deletions Scripts/Invoke-QuickTest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 6 additions & 14 deletions Tests/test-local.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,19 @@ 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
$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
Expand Down
8 changes: 4 additions & 4 deletions source/ConnectWiseAutomateAgent.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @()
Expand All @@ -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 = @{
Expand Down
4 changes: 2 additions & 2 deletions source/Public/InstallUninstall/Uninstall-CWAA.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function Uninstall-CWAA {
function Uninstall-CWAA {
<#
.SYNOPSIS
Completely uninstalls the ConnectWise Automate Agent from the local computer.
Expand Down Expand Up @@ -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
}
Expand Down
22 changes: 11 additions & 11 deletions source/Public/Service/Repair-CWAA.ps1
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
function Repair-CWAA {
function Repair-CWAA {
<#
.SYNOPSIS
Performs escalating remediation of the ConnectWise Automate agent.
.DESCRIPTION
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,
Expand Down Expand Up @@ -106,15 +106,15 @@ 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
Try {
$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)"

Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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.'
Expand Down
4 changes: 2 additions & 2 deletions source/Public/Service/Test-CWAAHealth.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function Test-CWAAHealth {
function Test-CWAAHealth {
<#
.SYNOPSIS
Performs a read-only health assessment of the ConnectWise Automate agent.
Expand Down Expand Up @@ -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
Expand Down
Loading