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
124 changes: 124 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Release

on:
push:
tags: [ 'v*' ]
workflow_dispatch:
inputs:
version:
description: 'Version to build (without the leading v)'
required: true

permissions:
contents: write

jobs:
release:
name: Build and publish
runs-on: windows-latest

steps:
- uses: actions/checkout@v4

- name: Resolve version
id: version
shell: powershell
run: |
$version = if ($env:GITHUB_REF -like 'refs/tags/v*') {
$env:GITHUB_REF -replace '^refs/tags/v', ''
} else {
'${{ github.event.inputs.version }}'
}
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
Write-Host "Building version $version"

- name: Verify the manifest matches the tag
shell: powershell
run: |
$manifest = Import-PowerShellDataFile ./src/PCTools/PCTools.psd1
$tagged = '${{ steps.version.outputs.version }}'
if ($manifest.ModuleVersion -ne $tagged) {
throw "Tag v$tagged does not match ModuleVersion $($manifest.ModuleVersion). Bump the manifest before tagging."
}

- name: Install test dependencies
shell: powershell
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 5.5.0 -Force -SkipPublisherCheck -Scope CurrentUser
Install-Module PSScriptAnalyzer -RequiredVersion 1.22.0 -Force -Scope CurrentUser

# Never publish something that has not passed its own suite.
- name: Test and analyze
shell: powershell
run: ./build/Invoke-Build.ps1 -Task All

- name: Stage artifacts and checksums
shell: powershell
run: ./build/Invoke-Build.ps1 -Task Release -Version ${{ steps.version.outputs.version }}

# Signing runs only when a certificate is configured. Without it the
# release still ships, unsigned, with checksums - which is what the
# project has today.
- name: Sign scripts
if: ${{ secrets.SIGNING_CERTIFICATE != '' }}
env:
SIGNING_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
shell: powershell
run: |
$pfxPath = Join-Path $env:RUNNER_TEMP 'signing.pfx'
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGNING_CERTIFICATE))
$password = ConvertTo-SecureString $env:SIGNING_PASSWORD -AsPlainText -Force
$cert = Get-PfxCertificate -FilePath $pfxPath -Password $password

Get-ChildItem ./out -Include *.ps1, *.psm1, *.psd1 -Recurse | ForEach-Object {
Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert `
-TimestampServer 'http://timestamp.digicert.com' -HashAlgorithm SHA256
Comment on lines +75 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sign the scripts packaged in the release archives

When a signing certificate is configured, this only finds loose script files under out; the actual pc-tools-<version>.zip and PCTools-<version>.zip were already compressed by the build and are never unpacked or signed. Consequently, users of the primary archive still receive unsigned pc-tools.ps1, .psm1, and .psd1 files despite the signing-enabled release path. Sign the staging tree before compression, or repackage the archives after signing.

Useful? React with 👍 / 👎.

}

Remove-Item $pfxPath -Force

# Signing rewrites the files, so the checksums must be regenerated.
./build/Invoke-Build.ps1 -Task Checksum

# Publishing to the Gallery is opt-in: without an API key the release
# still ships as an archive. Gallery versions are immutable, so this runs
# after the suite has passed and only for a tagged build.
- name: Publish to the PowerShell Gallery
if: ${{ secrets.PSGALLERY_API_KEY != '' && startsWith(github.ref, 'refs/tags/v') }}
shell: powershell
env:
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
run: |
$module = './src/PCTools'

# Refuse to republish a version that already exists - the Gallery
# rejects it anyway, and failing here says why.
$version = (Import-PowerShellDataFile "$module/PCTools.psd1").ModuleVersion
$existing = Find-Module PCTools -RequiredVersion $version -ErrorAction SilentlyContinue
if ($existing) {
throw "PCTools $version is already on the PowerShell Gallery. Bump ModuleVersion before tagging."
}

Publish-Module -Path $module -NuGetApiKey $env:PSGALLERY_API_KEY -Verbose

- name: Publish release
uses: softprops/action-gh-release@v2
with:
files: |
out/*
Comment on lines +106 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Set the release tag for manual dispatches

When this workflow is run through workflow_dispatch, GITHUB_REF is the selected branch rather than v<version>, and the resolved version is used only for artifact names and the manifest check. Because the release action receives no tag_name, a manual run cannot publish the requested version tag (and may attempt to release the branch ref instead), leaving the documented pinned vX.Y.Z install path without a corresponding release. Pass v${{ steps.version.outputs.version }} as tag_name for this action.

Useful? React with 👍 / 👎.

generate_release_notes: true
body: |
## Verifying this release

Every file below is listed in `SHA256SUMS`. Verify before running:

```powershell
$file = 'pc-tools.ps1'
(Get-FileHash $file -Algorithm SHA256).Hash.ToLower()
# compare against the matching line in SHA256SUMS
```

Install commands are pinned to this tag in the
[README](https://github.com/likeBloodMoon/pc-powershelltools#install).
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ desktop.ini
# PowerShell
*.psproj
testResults*.xml
obj/
76 changes: 76 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- `PCTools` module (`src/PCTools`): 29 public functions covering cleanup,
repair, network, preferences and software installation. Every action returns a
structured `PCTools.ActionResult` and every mutating action supports `-WhatIf`
and `-Confirm`.
- `Invoke-PCMaintenance` and built-in maintenance profiles (Quick, Recommended,
Full, NetworkRepair), replacing the "Run selected"/"Run all" buttons.
- `Export-PCReport`: JSON and text export for any results, generalised from Net
Diag's network-only `Save-Report`.
- `Get-PCPreference`: reads the current state of every managed Windows
preference. `Set-PCPreference` applies **and reverts** them.
- `Clear-PCBrowserCache` for Chrome, Edge, Brave, Firefox and Vivaldi.
- PC Tools shell (`src/Shell`), replacing both bespoke GUIs. Runs every action
on a background runspace, adds a `-WhatIf` preview, a per-run results summary
and a network verdict banner naming the failing layer.
- `pc-tools.ps1` entry point, with `-NoGui` to load the module into a console
session.
- `MIGRATION.md` mapping every old function to its replacement.
- Release workflow: builds from a tag, verifies the manifest version matches,
runs the full suite, publishes checksummed artifacts, and Authenticode-signs
them when a signing certificate is configured.
- `Format-PCByteSize` is public, so hosts can format a total without reaching
into module internals.
- Network profiles: `Export-PCNetworkProfile`, `Import-PCNetworkProfile` and
`Get-PCNetworkProfile` save and restore an adapter's full configuration
(DHCP or static address, prefix, gateway, DNS) as JSON, giving the Home/Work
presets the roadmap asked for.
- `Import-PCConfiguration`: user-defined maintenance profiles from a JSON file.
Imported profiles appear in `Get-PCMaintenanceProfile` and run through
`Invoke-PCMaintenance` unchanged; one with the same name as a built-in
overrides it. Every action name is validated at import, so a typo fails there
rather than partway through a run.
- `Test-PCRoute`: per-hop traceroute, answering where connectivity stops rather
than only that it has.
- `Test-PCMtu`: path MTU probe, the diagnostic for the case where DNS resolves
and small requests work but large transfers and TLS handshakes stall.
- `Get-PCWirelessStatus`: parses and grades the Wi-Fi association instead of
dumping `netsh wlan show interfaces` as raw text.
- The shell's Network page gained Trace route, Check path MTU, Wi-Fi signal, and
save/apply adapter profile.

### Changed
- PowerShell Gallery publishing wired into the release workflow, gated on a
`PSGALLERY_API_KEY` secret and refusing to republish an existing version.
- Module manifest carries Gallery metadata: `CompatiblePSEditions`, edition
tags and inline release notes.
- Install instructions are pinned to a release tag and hash-verified. The
previous `irm .../main/... | iex` commands are documented with their trade-off
rather than recommended.
- `Repair-PCSystemImage` and `Repair-PCSystemFile` are separate commands; the
original `Run-SystemHealthChecks` ran DISM and SFC together and reported
neither.
- `Test-PCDisk` takes a drive letter instead of always scanning `C:`.
- Prefetch cleanup and the network stack reset are excluded from every
general-purpose profile.

### Fixed
- Every external command runs through a timeout wrapper. Previously only Net
Diag's full scan was protected; a stuck DISM could hang the Cleanup Tool
indefinitely.
- DISM, SFC, CHKDSK and winget exit codes are interpreted instead of discarded,
so a failed repair no longer looks identical to a successful one.
- `Set-PCNetworkAddress` removes the existing address and route first. The
original failed with an "instance already exists" error on any adapter that
already had an address.
- `Set-PCDhcp` also resets DNS servers, which the original left static.
- `Clear-PCWindowsUpdateCache` restarts `wuauserv` in a `finally` block, so a
mid-run failure no longer leaves Windows Update stopped, and waits for the
service to stop before deleting.
- `New-PCRestorePoint` detects Windows silently throttling the 24-hour limit
instead of reporting success when no checkpoint was created.
- Folder cleanup enumerates once instead of recursing twice, uses `-LiteralPath`
so paths containing brackets are not skipped, and reports bytes reclaimed.
- Subnet mask conversion rejects non-contiguous masks and handles `/0`.
- `Restart-PCExplorer` waits for the shell to return instead of sleeping a fixed
interval, which could leave the user with no taskbar.

- `ROADMAP.md` describing the phased plan for the project.
- MIT `LICENSE`.
- `.gitignore` for logs, reports and release output.
Expand Down
93 changes: 93 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Migrating from the single-file tools

`pc-cleanuptool.ps1` and `pc-netdiag.ps1` still work and are still shipped. They
are frozen: bug fixes only, no new features. Everything they do now lives in the
`PCTools` module, usually with more control and always with a way to preview it
first.

## Why bother

- `-WhatIf` on every action, so you can see the plan before committing to it.
- Actions return objects, so you can filter, total and export them.
- Failures are reported. The originals piped DISM, SFC, CHKDSK and winget to
`Out-Null`, so a failed repair looked exactly like a successful one.
- Preferences can be read and reverted, not just applied.
- Nothing blocks the UI thread.

## Command mapping

### Cleanup

| Was | Now |
|---|---|
| `Clear-TempFiles` | `Clear-PCTempFile` |
| `Clear-RecycleBinSafe` | `Clear-PCRecycleBin` |
| `Clear-WindowsUpdateCache` | `Clear-PCWindowsUpdateCache` |
| `Clear-PrefetchCache` | `Clear-PCPrefetchCache` |
| — | `Clear-PCBrowserCache` (new) |

### Repair

| Was | Now |
|---|---|
| `Run-SystemHealthChecks` | `Repair-PCSystemImage` **and** `Repair-PCSystemFile` (split; DISM and SFC report separately) |
| `Run-ChkDskScan` | `Test-PCDisk` (takes a `-DriveLetter`; was hard-coded to `C:`) |
| `Create-SystemRestorePoint` | `New-PCRestorePoint` |

### Network

| Was | Now |
|---|---|
| `Flush-DnsCache` | `Clear-PCDnsCache` |
| `Reset-NetworkStack` | `Reset-PCNetworkStack` (add `-SkipWinsock` to keep VPN LSPs) |
| `Get-ActiveAdapters` | `Get-PCNetworkAdapter -ConnectedOnly` |
| `Set-StaticIP` | `Set-PCNetworkAddress` (takes `-PrefixLength` or `-SubnetMask`) |
| `Set-DnsServers` | `Set-PCDnsServer` (or `-Preset Cloudflare\|Google\|Quad9`) |
| `Set-DhcpMode` | `Set-PCDhcp` (now also resets DNS) |
| `Run-QuickDiagnostics` | `Get-PCNetworkReport` |
| `Start-FullDiagnosticsWorker` | `Get-PCNetworkReport -Full` |
| `Save-Report` | `Export-PCReport` (works for any results, not just network) |

### Preferences

The six one-way setters (`Set-DarkTheme`, `Disable-BingSearch`,
`Show-HiddenFiles`, `Show-FileExtensions`, `Disable-MouseAcceleration`,
`Enable-NumLock`) are replaced by two commands over a table of definitions:

```powershell
Get-PCPreference # what is set right now
Set-PCPreference -Name DarkMode -Enabled $true # apply
Set-PCPreference -Name DarkMode -Enabled $false # restore the Windows default
```

Names: `DarkMode`, `DisableBingSearch`, `ShowHiddenFiles`, `ShowFileExtensions`,
`DisableMouseAcceleration`, `NumLockOnStartup`, `DisableStartMenuSuggestions`,
`ShowFullPathInTitleBar`, `LaunchExplorerToThisPC`.

### Software

| Was | Now |
|---|---|
| `Install-BasicApps` | `Install-PCApplication -Preset Essentials` (or `-Id <winget ids>`) |

## Behaviour changes worth knowing

- **`Run all` is now a profile.** `Invoke-PCMaintenance -ProfileName Full`. It
takes a restore point first and stops if it cannot.
- **Prefetch is not in any profile.** Clearing it costs launch performance for a
small disk saving. Call `Clear-PCPrefetchCache` explicitly if you want it.
- **The network stack reset is not in any general profile** and prompts by
default (`ConfirmImpact = 'High'`).
- **A failed action no longer stops the batch.** Every action reports its own
result; pass `-ContinueOnFailure $false` for the old behaviour.
- **Subnet masks are converted, not passed through.** `255.0.255.0` is rejected
as invalid rather than handed to `netsh`.

## Running the old tools

Nothing was removed:

```powershell
.\pc-cleanuptool.ps1
.\pc-netdiag.ps1
```
43 changes: 25 additions & 18 deletions PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
@{
# Target the lowest runtime the GUI supports. WinForms requires Windows
# PowerShell 5.1, so compatibility is checked against that.
Severity = @('Error', 'Warning')

ExcludeRules = @(
# The GUI shell intentionally holds module-scoped state ($sync, $App)
# that the analyzer reads as "assigned but never used".
'PSUseDeclaredVarsMoreThanAssignments',
# The GUI shell deliberately holds module-scoped state ($sync, $App,
# control variables) that the analyzer reads as "assigned but never
# used" because the reads happen inside event handlers.
'PSUseDeclaredVarsMoreThanAssignments'

# Write-Host is a deliberate choice in the console entry points, where
# coloured progress output is the whole point.
# Write-Host is a deliberate choice in the console entry points and the
# build script, where coloured progress output is the point.
'PSAvoidUsingWriteHost'

# False-positive here by construction. Nearly every public action wraps
# its work in `Invoke-PCAction -Body { ... }`, and the analyzer does not
# follow parameter use into that scriptblock, so it flags parameters
# that are plainly used two lines further down.
'PSReviewUnusedParameter'

# Purely cosmetic and extremely noisy against this codebase's existing
# style, including the original scripts. Enabling them buried the rules
# that catch real problems under a thousand indentation notes. Revisit
# as a one-off reformat, not as a gate.
'PSUseConsistentIndentation'
'PSUseConsistentWhitespace'
'PSAlignAssignmentStatement'
'PSPlaceOpenBrace'
'PSPlaceCloseBrace'
)

Rules = @{
# The one formatting-adjacent rule worth keeping: it catches syntax that
# parses in PowerShell 7 but not in 5.1, which is the runtime the GUI
# actually requires.
PSUseCompatibleSyntax = @{
Enable = $true
TargetVersions = @('5.1', '7.0')
}
PSPlaceOpenBrace = @{
Enable = $true
OnSameLine = $true
NewLineAfter = $true
IgnoreOneLineBlock = $true
}
PSUseConsistentIndentation = @{
Enable = $true
Kind = 'space'
IndentationSize = 4
}
}
}
Loading
Loading