From 51a56d8fbc1148e92f72796e6443352a2ad962f1 Mon Sep 17 00:00:00 2001 From: xsyetopz <187086553+xsyetopz@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:04:44 +0300 Subject: [PATCH 1/5] Resolve BraveDebloater issue batch Fixes #10 Fixes #11 Fixes #13 Fixes #15 Fixes #16 Fixes #17 Fixes #18 Fixes #19 --- .github/workflows/ci.yml | 15 ++++++++++ Invoke-BraveDebloat.ps1 | 21 ++++++++++++- README.md | 49 ++++++++++++++++++++++++++++++ ROADMAP.md | 33 +++++++++++++++++++++ scripts/New-ReleaseChecksums.ps1 | 19 ++++++++++++ scripts/Test-Behavior.ps1 | 24 +++++++++++++++ src/Backup.ps1 | 51 ++++++++++++++++++++++++++++++++ src/PlatformPolicy.ps1 | 19 +++++++++--- tests/PresetResolution.Tests.ps1 | 31 +++++++++++++++++++ 9 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 ROADMAP.md create mode 100644 scripts/New-ReleaseChecksums.ps1 create mode 100644 tests/PresetResolution.Tests.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb54e60..93296bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,21 @@ jobs: shell: pwsh run: ./scripts/Test-Behavior.ps1 + - name: Install PowerShell test tools + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module Pester -Scope CurrentUser -Force -SkipPublisherCheck + Install-Module PSScriptAnalyzer -Scope CurrentUser -Force + + - name: Run Pester tests + shell: pwsh + run: Invoke-Pester -Path ./tests -CI + + - name: Run PSScriptAnalyzer + shell: pwsh + run: Invoke-ScriptAnalyzer -Path ./Invoke-BraveDebloat.ps1,./scripts,./src -Recurse -Severity Error + - name: Dry-run extreme preset shell: pwsh run: ./Invoke-BraveDebloat.ps1 -Preset Extreme -LockShields diff --git a/Invoke-BraveDebloat.ps1 b/Invoke-BraveDebloat.ps1 index 9ea1a53..84982bf 100755 --- a/Invoke-BraveDebloat.ps1 +++ b/Invoke-BraveDebloat.ps1 @@ -8,6 +8,9 @@ param( [ValidateSet('Auto', 'Windows', 'macOS', 'Linux', 'Android', 'iOS')] [string]$Platform = 'Auto', + [ValidateSet('Stable', 'Beta', 'Nightly')] + [string]$Channel = 'Stable', + [ValidateSet('CurrentUser', 'LocalMachine')] [string]$Scope = 'CurrentUser', @@ -37,6 +40,14 @@ param( [string]$UndoFromBackup, + [switch]$ListBackups, + + [ValidateRange(-1, 36500)] + [int]$PruneBackupsOlderThanDays = -1, + + [ValidateRange(-1, 100000)] + [int]$KeepLatestBackups = -1, + [switch]$List, [switch]$ListFeatures, @@ -66,11 +77,16 @@ foreach ($moduleName in @('Common.ps1', 'Manifest.ps1', 'PlatformPolicy.ps1', 'B $manifest = Get-Manifest $platformName = Resolve-PlatformName -Name $Platform if ([string]::IsNullOrWhiteSpace($ProfileRoot)) { - $ProfileRoot = Get-DefaultProfileRoot -PlatformName $platformName + $ProfileRoot = Get-DefaultProfileRoot -PlatformName $platformName -Channel $Channel } $applyChanges = $Apply -and -not $WhatIfPreference $isWhatIf = $Apply -and $WhatIfPreference +if ($ListBackups -or $PruneBackupsOlderThanDays -ge 0 -or $KeepLatestBackups -ge 0) { + Invoke-BackupRetention -Directory $BackupDirectory -OlderThanDays $PruneBackupsOlderThanDays -KeepLatest $KeepLatestBackups -DoApply:$applyChanges + return +} + if ($UndoFromBackup) { Restore-RegistryBackup -BackupPath $UndoFromBackup -Manifest $manifest -ProfileRoot $ProfileRoot -AllowedPolicyPath $PolicyPath -DoApply:$applyChanges if (-not $applyChanges) { @@ -197,6 +213,9 @@ else { Write-Step "Preset: $Preset" } Write-Step "Platform: $platformName" +if ($Channel -ne 'Stable') { + Write-Step "Channel: $Channel" +} Write-Step "Scope: $Scope ($($policyTarget.Path))" if ($LockShields) { Write-Step 'Shield baseline: enabled. Brave will keep ad blocking, standard fingerprinting protection, HTTPS upgrades, and referrer capping on by policy.' diff --git a/README.md b/README.md index 4072aa0..369cb3c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,22 @@ Extra UI in the `Extreme` preset: Optional profile preference cleanup can also hide some new tab, sponsored background, and toolbar surfaces. That part edits per-profile `Preferences` JSON, so close Brave before applying it. +## Install + +Download the latest release from the repository's Releases page, then extract the zip to a folder you control, such as `Downloads\BraveDebloater`. + +Open PowerShell in that folder and run the default dry-run: + +```powershell +.\Invoke-BraveDebloat.ps1 +``` + +Review the output before applying changes. To verify a release archive first, download `SHA256SUMS.txt` from the same release and compare it with: + +```powershell +Get-FileHash .\BraveDebloater-vX.Y.Z.zip -Algorithm SHA256 +``` + ## Start Here Preview the default cleanup first: @@ -83,6 +99,16 @@ Run a read-only health check: .\Invoke-BraveDebloat.ps1 -Doctor ``` +List backups or preview retention cleanup: + +```powershell +.\Invoke-BraveDebloat.ps1 -ListBackups +.\Invoke-BraveDebloat.ps1 -PruneBackupsOlderThanDays 30 +.\Invoke-BraveDebloat.ps1 -KeepLatestBackups 10 +``` + +Add `-Apply` only after the preview lists the backups you expect to delete. + Apply the default cleanup and lock a safe Shields baseline: ```powershell @@ -131,6 +157,17 @@ Examples: Use `-PolicyPath` when testing, or when your managed Linux/macOS policy file lives somewhere custom. +## Brave Channels + +Profile preference cleanup targets Brave Stable by default. Use `-Channel` when you want the default profile path for Beta or Nightly: + +```powershell +.\Invoke-BraveDebloat.ps1 -Channel Beta -IncludeProfilePreferences +.\Invoke-BraveDebloat.ps1 -Channel Nightly -IncludeProfilePreferences +``` + +Stable policy behavior is unchanged. `-ProfileRoot` still overrides the detected profile path. + ## Presets `Standard` removes Brave-specific bloat and Brave telemetry. @@ -214,6 +251,18 @@ Policy names and values come from Brave's official Group Policy documentation an See `docs/debloatable-validation.md` for the source version, the policy choices, and the validation commands. +See `ROADMAP.md` for planned safety, testing, release trust, user experience, and maintainability work. + +## Release Checksums + +Generate SHA256 checksums for release artifacts before publishing: + +```powershell +.\scripts\New-ReleaseChecksums.ps1 -Path .\BraveDebloater-vX.Y.Z.zip -OutputPath .\SHA256SUMS.txt +``` + +Upload `SHA256SUMS.txt` beside the release archive. + ## Project Checks Run the local checks: diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..b2e88dc --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,33 @@ +# Roadmap + +BraveDebloater stays safety-first: dry-run by default, native policy writes, strict restore validation, and no policies that weaken Brave Shields, Safe Browsing, updates, or installed extensions. + +## Safety + +- Keep backup and restore validation narrow. +- Keep profile preference cleanup opt-in and skipped while Brave is running. +- Add policy changes only when they are documented by Brave or Chromium policy sources. + +## Testing + +- Keep manifest, behavior, Pester, and PowerShell syntax checks in CI. +- Expand tests when new CLI switches change write behavior. +- Validate policy names against Brave's latest templates before releases. + +## Release Trust + +- Publish SHA256 checksum files beside release archives. +- Keep release notes short and focused on user-visible changes. +- Document how to verify downloads before applying changes. + +## User Experience + +- Improve examples for Brave Stable, Beta, and Nightly profiles. +- Keep output concise: what would change, what changed, and what to do next. +- Accept user-provided screenshots for common dry-run and Doctor workflows. + +## Maintainability + +- Keep `Invoke-BraveDebloat.ps1` as a thin entrypoint. +- Put shared behavior in `src/*.ps1`. +- Prefer small issue-sized changes over broad rewrites. diff --git a/scripts/New-ReleaseChecksums.ps1 b/scripts/New-ReleaseChecksums.ps1 new file mode 100644 index 0000000..69c398d --- /dev/null +++ b/scripts/New-ReleaseChecksums.ps1 @@ -0,0 +1,19 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string[]]$Path, + + [string]$OutputPath = 'SHA256SUMS.txt' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$lines = foreach ($item in $Path) { + $hash = Get-FileHash -LiteralPath $item -Algorithm SHA256 + '{0} {1}' -f $hash.Hash.ToLowerInvariant(), (Split-Path -Leaf $hash.Path) +} + +$lines | Set-Content -LiteralPath $OutputPath -Encoding ASCII +Write-Host "Wrote $OutputPath" diff --git a/scripts/Test-Behavior.ps1 b/scripts/Test-Behavior.ps1 index 9c617fb..7110b5e 100644 --- a/scripts/Test-Behavior.ps1 +++ b/scripts/Test-Behavior.ps1 @@ -147,6 +147,30 @@ try { throw '-WhatIf created a backup directory.' } + $channelOutput = (& $scriptPath -Preset Core -Channel Beta -ProfileRoot '' *>&1 | Out-String) + Assert-TextContains -Text $channelOutput -Expected 'Channel: Beta' -Context '-Channel Beta output' + + $retentionDirectory = Join-Path $tempRoot 'RetentionBackups' + New-Item -ItemType Directory -Path $retentionDirectory -Force | Out-Null + $oldBackup = Join-Path $retentionDirectory 'BraveDebloater-20240101-010101-001.json' + $newBackup = Join-Path $retentionDirectory 'BraveDebloater-20260101-010101-001.json' + Set-Content -LiteralPath $oldBackup -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath $newBackup -Value '{}' -Encoding UTF8 + (Get-Item -LiteralPath $oldBackup).LastWriteTime = (Get-Date).AddDays(-60) + (Get-Item -LiteralPath $newBackup).LastWriteTime = Get-Date + + $retentionPreview = (& $scriptPath -BackupDirectory $retentionDirectory -PruneBackupsOlderThanDays 30 *>&1 | Out-String) + Assert-TextContains -Text $retentionPreview -Expected 'Would remove backup BraveDebloater-20240101-010101-001.json' -Context 'backup retention preview' + if (-not (Test-Path -LiteralPath $oldBackup)) { + throw 'Backup retention preview deleted a backup.' + } + + $retentionApply = (& $scriptPath -BackupDirectory $retentionDirectory -KeepLatestBackups 1 -Apply *>&1 | Out-String) + Assert-TextContains -Text $retentionApply -Expected 'Removed backup BraveDebloater-20240101-010101-001.json.' -Context 'backup retention apply' + if (Test-Path -LiteralPath $oldBackup) { + throw 'Backup retention apply did not remove the old backup.' + } + $tamperedBackup = Join-Path $tempRoot 'tampered-backup.json' [ordered]@{ schemaVersion = 1 diff --git a/src/Backup.ps1 b/src/Backup.ps1 index d81700f..b65ef30 100644 --- a/src/Backup.ps1 +++ b/src/Backup.ps1 @@ -147,6 +147,57 @@ function Get-BackupSummary { } } +function Get-BackupFiles { + param([string]$Directory) + + $fullDirectory = Get-FullFileSystemPath -Path $Directory + if (-not (Test-Path -LiteralPath $fullDirectory)) { + return @() + } + + return @(Get-ChildItem -LiteralPath $fullDirectory -Filter 'BraveDebloater-*.json' | Where-Object { -not $_.PSIsContainer } | Sort-Object LastWriteTime -Descending) +} + +function Invoke-BackupRetention { + param( + [string]$Directory, + [int]$OlderThanDays = -1, + [int]$KeepLatest = -1, + [switch]$DoApply + ) + + $files = @(Get-BackupFiles -Directory $Directory) + Write-Step "Backups: $($files.Count) found in $(Get-FullFileSystemPath -Path $Directory)" + foreach ($file in $files) { + Write-Step ("Backup: {0} ({1:yyyy-MM-dd HH:mm:ss})" -f $file.Name, $file.LastWriteTime) + } + + $remove = @() + if ($OlderThanDays -ge 0) { + $cutoff = (Get-Date).AddDays(-$OlderThanDays) + $remove += @($files | Where-Object { $_.LastWriteTime -lt $cutoff }) + } + if ($KeepLatest -ge 0 -and $files.Count -gt $KeepLatest) { + $remove += @($files | Select-Object -Skip $KeepLatest) + } + $remove = @($remove | Sort-Object FullName -Unique) + + if ($remove.Count -eq 0) { + Write-Step 'Backup cleanup: nothing to remove.' + return + } + + foreach ($file in $remove) { + if ($DoApply) { + Remove-Item -LiteralPath $file.FullName -Force + Write-Step "Removed backup $($file.Name)." + } + else { + Write-DryRun "Would remove backup $($file.Name). Add -Apply to delete it." + } + } +} + function New-BackupPath { param([string]$Directory) diff --git a/src/PlatformPolicy.ps1 b/src/PlatformPolicy.ps1 index db396ba..eb3b1ae 100644 --- a/src/PlatformPolicy.ps1 +++ b/src/PlatformPolicy.ps1 @@ -27,12 +27,23 @@ function Resolve-PlatformName { } function Get-DefaultProfileRoot { - param([string]$PlatformName) + param( + [string]$PlatformName, + [ValidateSet('Stable', 'Beta', 'Nightly')] + [string]$Channel = 'Stable' + ) + + $suffix = switch ($Channel) { + 'Beta' { '-Beta' } + 'Nightly' { '-Nightly' } + default { '' } + } + $browserDirectory = "Brave-Browser$suffix" switch ($PlatformName) { - 'Windows' { return (Join-Path $env:LOCALAPPDATA 'BraveSoftware\Brave-Browser\User Data') } - 'macOS' { return (Join-Path $HOME 'Library/Application Support/BraveSoftware/Brave-Browser') } - 'Linux' { return (Join-Path $HOME '.config/BraveSoftware/Brave-Browser') } + 'Windows' { return (Join-Path $env:LOCALAPPDATA "BraveSoftware\$browserDirectory\User Data") } + 'macOS' { return (Join-Path $HOME "Library/Application Support/BraveSoftware/$browserDirectory") } + 'Linux' { return (Join-Path $HOME ".config/BraveSoftware/$browserDirectory") } default { return '' } } } diff --git a/tests/PresetResolution.Tests.ps1 b/tests/PresetResolution.Tests.ps1 new file mode 100644 index 0000000..971af7c --- /dev/null +++ b/tests/PresetResolution.Tests.ps1 @@ -0,0 +1,31 @@ +#requires -Version 5.1 + +$root = Split-Path -Parent $PSScriptRoot +. (Join-Path $root 'src/Common.ps1') +. (Join-Path $root 'src/Manifest.ps1') + +Describe 'Preset resolution' { + BeforeAll { + $manifest = Get-Manifest + $presets = Get-ManifestMap -Object $manifest.presets + } + + It 'resolves preset aliases to the same policies' { + (Resolve-PresetPolicies -Name 'Standard' -Presets $presets) -join ',' | Should -Be ((Resolve-PresetPolicies -Name 'Core' -Presets $presets) -join ',') + (Resolve-PresetPolicies -Name 'High' -Presets $presets) -join ',' | Should -Be ((Resolve-PresetPolicies -Name 'Privacy' -Presets $presets) -join ',') + (Resolve-PresetPolicies -Name 'Extreme' -Presets $presets) -join ',' | Should -Be ((Resolve-PresetPolicies -Name 'Aggressive' -Presets $presets) -join ',') + } + + It 'rejects unknown presets clearly' { + { Resolve-PresetPolicies -Name 'Missing' -Presets $presets } | Should -Throw "Unknown preset 'Missing'." + } + + It 'rejects preset cycles' { + $cycle = @{ + A = @('@B') + B = @('@A') + } + + { Resolve-PresetPolicies -Name 'A' -Presets $cycle } | Should -Throw "Preset cycle detected at 'A'." + } +} From 46e3e17315361f047dc0a97c87e244aad1f282ca Mon Sep 17 00:00:00 2001 From: xsyetopz <187086553+xsyetopz@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:09:45 +0300 Subject: [PATCH 2/5] Fix issue batch CI --- README.md | 6 ++ config/policies.json | 2 +- docs/debloatable-validation.md | 5 +- scripts/Update-PolicyTemplateVersion.ps1 | 81 ++++++++++++++++++++++++ tests/PresetResolution.Tests.ps1 | 8 +-- 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 scripts/Update-PolicyTemplateVersion.ps1 diff --git a/README.md b/README.md index 369cb3c..65fe94a 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,12 @@ Validate against a downloaded Brave policy template zip: .\scripts\Test-LatestPolicyTemplates.ps1 -TemplateZipPath .\policy_templates.zip ``` +Update the recorded template version after downloading a newer official zip: + +```powershell +.\scripts\Update-PolicyTemplateVersion.ps1 -TemplateZipPath .\policy_templates.zip +``` + ## Pull Request Review Greptile review guidance lives in `greptile.json`. It covers PowerShell compatibility, policy writes, registry writes, profile JSON writes, and feature-toggle behavior. diff --git a/config/policies.json b/config/policies.json index 6c5a84d..2a0268d 100644 --- a/config/policies.json +++ b/config/policies.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "policyTemplateVersion": "150.1.93.96", + "policyTemplateVersion": "150.1.94.4", "sources": [ "https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy", "https://brave-browser-downloads.s3.brave.com/latest/policy_templates.zip" diff --git a/docs/debloatable-validation.md b/docs/debloatable-validation.md index 36b9960..98d6ffa 100644 --- a/docs/debloatable-validation.md +++ b/docs/debloatable-validation.md @@ -11,7 +11,7 @@ Official sources used for this pass: Downloaded template evidence: -- Template version: `150.1.93.96` +- Template version: `150.1.94.4` - Archive timestamp: June 23, 2026 - Checked files: `VERSION` and `windows/admx/brave.admx` @@ -19,7 +19,7 @@ Targeted Reddit, Brave Community, and GitHub searches did not produce a newer or ## What Changed -The manifest version in `config/policies.json` changed from `148.1.91.121` to `150.1.93.96`. +The manifest version in `config/policies.json` changed from `148.1.91.121` to `150.1.94.4`. These official-template policies were added because they match BraveDebloater's scope: @@ -82,3 +82,4 @@ pwsh -NoProfile -File ./scripts/Test-Behavior.ps1 ``` The template validator uses a local zip file on purpose. CI can download the current zip before running it, but normal offline checks do not need network access. + diff --git a/scripts/Update-PolicyTemplateVersion.ps1 b/scripts/Update-PolicyTemplateVersion.ps1 new file mode 100644 index 0000000..2224c74 --- /dev/null +++ b/scripts/Update-PolicyTemplateVersion.ps1 @@ -0,0 +1,81 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$TemplateZipPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$root = Split-Path -Parent $PSScriptRoot +$manifestPath = Join-Path (Join-Path $root 'config') 'policies.json' +$validationDocPath = Join-Path (Join-Path $root 'docs') 'debloatable-validation.md' + +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function Read-ZipEntryText { + param( + [Parameter(Mandatory = $true)]$Zip, + [Parameter(Mandatory = $true)][string]$EntryName + ) + + $entry = $Zip.GetEntry($EntryName) + if ($null -eq $entry) { + throw "Template zip is missing '$EntryName'." + } + + $stream = $entry.Open() + try { + $reader = New-Object System.IO.StreamReader($stream, $true) + try { + return $reader.ReadToEnd() + } + finally { + $reader.Dispose() + } + } + finally { + $stream.Dispose() + } +} + +function Get-TemplateVersion { + param([Parameter(Mandatory = $true)]$Zip) + + $versionText = Read-ZipEntryText -Zip $Zip -EntryName 'VERSION' + $parts = (($versionText -split "`n") | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(MAJOR|MINOR|BUILD|PATCH)=' }) -replace '^[^=]+=' + if ($parts.Count -ne 4) { + throw 'Template VERSION file did not contain MAJOR, MINOR, BUILD, and PATCH.' + } + + return ($parts -join '.') +} + +if (-not (Test-Path -LiteralPath $TemplateZipPath)) { + throw "Missing template zip: $TemplateZipPath" +} + +$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $TemplateZipPath)) +try { + $templateVersion = Get-TemplateVersion -Zip $zip +} +finally { + $zip.Dispose() +} + +$manifestText = Get-Content -LiteralPath $manifestPath -Raw +$oldVersion = [regex]::Match($manifestText, '"policyTemplateVersion":\s*"([^"]+)"').Groups[1].Value +if ([string]::IsNullOrWhiteSpace($oldVersion)) { + throw "Could not find policyTemplateVersion in $manifestPath." +} + +$manifestText = [regex]::Replace($manifestText, '"policyTemplateVersion":\s*"[^"]+"', ('"policyTemplateVersion": "{0}"' -f $templateVersion), 1) +Set-Content -LiteralPath $manifestPath -Value $manifestText -Encoding UTF8 + +$docText = Get-Content -LiteralPath $validationDocPath -Raw +$docText = [regex]::Replace($docText, 'Template version: `[^`]+`', ('Template version: `{0}`' -f $templateVersion), 1) +$docText = [regex]::Replace($docText, 'changed from `([^`]+)` to `[^`]+`', ('changed from `$1` to `{0}`' -f $templateVersion), 1) +Set-Content -LiteralPath $validationDocPath -Value $docText -Encoding UTF8 + +Write-Host "Updated policy template version from $oldVersion to $templateVersion." diff --git a/tests/PresetResolution.Tests.ps1 b/tests/PresetResolution.Tests.ps1 index 971af7c..81dcf42 100644 --- a/tests/PresetResolution.Tests.ps1 +++ b/tests/PresetResolution.Tests.ps1 @@ -1,11 +1,11 @@ #requires -Version 5.1 -$root = Split-Path -Parent $PSScriptRoot -. (Join-Path $root 'src/Common.ps1') -. (Join-Path $root 'src/Manifest.ps1') - Describe 'Preset resolution' { BeforeAll { + $root = Split-Path -Parent $PSScriptRoot + . (Join-Path $root 'src/Common.ps1') + . (Join-Path $root 'src/Manifest.ps1') + $manifest = Get-Manifest $presets = Get-ManifestMap -Object $manifest.presets } From 9736b183201792453b71321892bef6d8f20e0b4b Mon Sep 17 00:00:00 2001 From: xsyetopz <187086553+xsyetopz@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:11:48 +0300 Subject: [PATCH 3/5] Fix preset Pester manifest load --- tests/PresetResolution.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PresetResolution.Tests.ps1 b/tests/PresetResolution.Tests.ps1 index 81dcf42..9c07f2a 100644 --- a/tests/PresetResolution.Tests.ps1 +++ b/tests/PresetResolution.Tests.ps1 @@ -6,7 +6,7 @@ Describe 'Preset resolution' { . (Join-Path $root 'src/Common.ps1') . (Join-Path $root 'src/Manifest.ps1') - $manifest = Get-Manifest + $manifest = Get-Content -LiteralPath (Join-Path (Join-Path $root 'config') 'policies.json') -Raw | ConvertFrom-Json $presets = Get-ManifestMap -Object $manifest.presets } From 2529883fed19b58d1cce03481d81864e9be2c094 Mon Sep 17 00:00:00 2001 From: xsyetopz <187086553+xsyetopz@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:13:35 +0300 Subject: [PATCH 4/5] Fix analyzer CI invocation --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93296bb..5fbd057 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,14 @@ jobs: - name: Run PSScriptAnalyzer shell: pwsh - run: Invoke-ScriptAnalyzer -Path ./Invoke-BraveDebloat.ps1,./scripts,./src -Recurse -Severity Error + run: | + $results = foreach ($path in @('./Invoke-BraveDebloat.ps1', './scripts', './src')) { + Invoke-ScriptAnalyzer -Path $path -Recurse -Severity Error + } + if ($results) { + $results | Format-Table -AutoSize | Out-String | Write-Error + exit 1 + } - name: Dry-run extreme preset shell: pwsh From cbb8f3f9f43bc09786212e0847d003432243bd63 Mon Sep 17 00:00:00 2001 From: xsyetopz <187086553+xsyetopz@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:37:58 +0300 Subject: [PATCH 5/5] Address backup and CI review comments --- .github/workflows/ci.yml | 4 +-- scripts/PolicyTemplateVersion.ps1 | 21 +++++++++++++++ scripts/Test-Behavior.ps1 | 4 +++ scripts/Test-LatestPolicyTemplates.ps1 | 5 ++-- scripts/Test-PolicyManifest.ps1 | 33 ++++++++++++++++++++++++ scripts/Update-PolicyTemplateVersion.ps1 | 9 +++---- src/Backup.ps1 | 14 +++++++--- tests/PresetResolution.Tests.ps1 | 33 ++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 scripts/PolicyTemplateVersion.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fbd057..d51caa5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,8 @@ jobs: shell: pwsh run: | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - Install-Module Pester -Scope CurrentUser -Force -SkipPublisherCheck - Install-Module PSScriptAnalyzer -Scope CurrentUser -Force + Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force -SkipPublisherCheck + Install-Module PSScriptAnalyzer -RequiredVersion 1.24.0 -Scope CurrentUser -Force - name: Run Pester tests shell: pwsh diff --git a/scripts/PolicyTemplateVersion.ps1 b/scripts/PolicyTemplateVersion.ps1 new file mode 100644 index 0000000..094039f --- /dev/null +++ b/scripts/PolicyTemplateVersion.ps1 @@ -0,0 +1,21 @@ +#requires -Version 5.1 + +function Get-PolicyTemplateVersionFromText { + param([Parameter(Mandatory = $true)][string]$VersionText) + + $values = @{} + foreach ($line in ($VersionText -split "`n")) { + if ($line.Trim() -match '^(MAJOR|MINOR|BUILD|PATCH)=(.+)$') { + $values[$Matches[1]] = $Matches[2].Trim() + } + } + + $keys = @('MAJOR', 'MINOR', 'BUILD', 'PATCH') + foreach ($key in $keys) { + if (-not $values.ContainsKey($key)) { + throw 'Template VERSION file did not contain MAJOR, MINOR, BUILD, and PATCH.' + } + } + + return (($keys | ForEach-Object { $values[$_] }) -join '.') +} diff --git a/scripts/Test-Behavior.ps1 b/scripts/Test-Behavior.ps1 index 7110b5e..80f0c77 100644 --- a/scripts/Test-Behavior.ps1 +++ b/scripts/Test-Behavior.ps1 @@ -159,6 +159,10 @@ try { (Get-Item -LiteralPath $oldBackup).LastWriteTime = (Get-Date).AddDays(-60) (Get-Item -LiteralPath $newBackup).LastWriteTime = Get-Date + $listBackupsOutput = (& $scriptPath -BackupDirectory $retentionDirectory -ListBackups *>&1 | Out-String) + Assert-TextContains -Text $listBackupsOutput -Expected 'Backups: 2 found' -Context '-ListBackups output' + Assert-TextDoesNotContain -Text $listBackupsOutput -Unexpected 'Backup cleanup: nothing to remove.' -Context '-ListBackups output' + $retentionPreview = (& $scriptPath -BackupDirectory $retentionDirectory -PruneBackupsOlderThanDays 30 *>&1 | Out-String) Assert-TextContains -Text $retentionPreview -Expected 'Would remove backup BraveDebloater-20240101-010101-001.json' -Context 'backup retention preview' if (-not (Test-Path -LiteralPath $oldBackup)) { diff --git a/scripts/Test-LatestPolicyTemplates.ps1 b/scripts/Test-LatestPolicyTemplates.ps1 index 4633798..cad26e7 100644 --- a/scripts/Test-LatestPolicyTemplates.ps1 +++ b/scripts/Test-LatestPolicyTemplates.ps1 @@ -11,6 +11,8 @@ $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $manifestPath = Join-Path (Join-Path $root 'config') 'policies.json' +. (Join-Path $PSScriptRoot 'PolicyTemplateVersion.ps1') + Add-Type -AssemblyName System.IO.Compression.FileSystem function Read-ZipEntryText { @@ -47,8 +49,7 @@ $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json $zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $TemplateZipPath)) try { $versionText = Read-ZipEntryText -Zip $zip -EntryName 'VERSION' - $templateVersion = (($versionText -split "`n") | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(MAJOR|MINOR|BUILD|PATCH)=' }) -replace '^[^=]+=' - $templateVersion = $templateVersion -join '.' + $templateVersion = Get-PolicyTemplateVersionFromText -VersionText $versionText if ($templateVersion -ne [string]$manifest.policyTemplateVersion) { throw "Manifest policyTemplateVersion '$($manifest.policyTemplateVersion)' does not match template '$templateVersion'." } diff --git a/scripts/Test-PolicyManifest.ps1 b/scripts/Test-PolicyManifest.ps1 index 779bd05..a11d4dd 100644 --- a/scripts/Test-PolicyManifest.ps1 +++ b/scripts/Test-PolicyManifest.ps1 @@ -55,6 +55,37 @@ function Resolve-Preset { return $items.ToArray() } +function Test-PolicyTemplateVersionUpdater { + $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('BraveDebloaterTemplateVersion-{0}' -f [guid]::NewGuid().ToString('N')) + try { + New-Item -ItemType Directory -Path (Join-Path $tempRoot 'scripts') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $tempRoot 'config') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $tempRoot 'docs') -Force | Out-Null + Copy-Item -LiteralPath (Join-Path $root 'scripts/Update-PolicyTemplateVersion.ps1') -Destination (Join-Path $tempRoot 'scripts/Update-PolicyTemplateVersion.ps1') + Copy-Item -LiteralPath (Join-Path $root 'scripts/PolicyTemplateVersion.ps1') -Destination (Join-Path $tempRoot 'scripts/PolicyTemplateVersion.ps1') + Set-Content -LiteralPath (Join-Path $tempRoot 'config/policies.json') -Value '{"policyTemplateVersion": "0.0.0.0"}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $tempRoot 'docs/debloatable-validation.md') -Value 'Template version: `0.0.0.0`' -Encoding UTF8 + + $zipRoot = Join-Path $tempRoot 'zip' + New-Item -ItemType Directory -Path $zipRoot -Force | Out-Null + Set-Content -LiteralPath (Join-Path $zipRoot 'VERSION') -Value "PATCH=4`nBUILD=3`nMAJOR=1`nMINOR=2" -Encoding UTF8 + $zipPath = Join-Path $tempRoot 'template.zip' + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::CreateFromDirectory($zipRoot, $zipPath) + + & (Join-Path $tempRoot 'scripts/Update-PolicyTemplateVersion.ps1') -TemplateZipPath $zipPath *> $null + $updated = Get-Content -LiteralPath (Join-Path $tempRoot 'config/policies.json') -Raw | ConvertFrom-Json + if ([string]$updated.policyTemplateVersion -ne '1.2.3.4') { + throw 'Update-PolicyTemplateVersion.ps1 did not assemble shuffled VERSION keys by name.' + } + } + finally { + if (Test-Path -LiteralPath $tempRoot) { + Remove-Item -LiteralPath $tempRoot -Recurse -Force + } + } +} + if (-not (Test-Path -LiteralPath $manifestPath)) { throw "Missing manifest: $manifestPath" } @@ -187,4 +218,6 @@ if ($parseErrors.Count -gt 0) { throw "PowerShell parse errors in Invoke-BraveDebloat.ps1: $($messages -join '; ')" } +Test-PolicyTemplateVersionUpdater + Write-Host 'Policy manifest and PowerShell syntax checks passed.' diff --git a/scripts/Update-PolicyTemplateVersion.ps1 b/scripts/Update-PolicyTemplateVersion.ps1 index 2224c74..0e9f787 100644 --- a/scripts/Update-PolicyTemplateVersion.ps1 +++ b/scripts/Update-PolicyTemplateVersion.ps1 @@ -12,6 +12,8 @@ $root = Split-Path -Parent $PSScriptRoot $manifestPath = Join-Path (Join-Path $root 'config') 'policies.json' $validationDocPath = Join-Path (Join-Path $root 'docs') 'debloatable-validation.md' +. (Join-Path $PSScriptRoot 'PolicyTemplateVersion.ps1') + Add-Type -AssemblyName System.IO.Compression.FileSystem function Read-ZipEntryText { @@ -44,12 +46,7 @@ function Get-TemplateVersion { param([Parameter(Mandatory = $true)]$Zip) $versionText = Read-ZipEntryText -Zip $Zip -EntryName 'VERSION' - $parts = (($versionText -split "`n") | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(MAJOR|MINOR|BUILD|PATCH)=' }) -replace '^[^=]+=' - if ($parts.Count -ne 4) { - throw 'Template VERSION file did not contain MAJOR, MINOR, BUILD, and PATCH.' - } - - return ($parts -join '.') + return Get-PolicyTemplateVersionFromText -VersionText $versionText } if (-not (Test-Path -LiteralPath $TemplateZipPath)) { diff --git a/src/Backup.ps1 b/src/Backup.ps1 index b65ef30..951a388 100644 --- a/src/Backup.ps1 +++ b/src/Backup.ps1 @@ -172,6 +172,7 @@ function Invoke-BackupRetention { Write-Step ("Backup: {0} ({1:yyyy-MM-dd HH:mm:ss})" -f $file.Name, $file.LastWriteTime) } + $pruneRequested = $OlderThanDays -ge 0 -or $KeepLatest -ge 0 $remove = @() if ($OlderThanDays -ge 0) { $cutoff = (Get-Date).AddDays(-$OlderThanDays) @@ -183,14 +184,21 @@ function Invoke-BackupRetention { $remove = @($remove | Sort-Object FullName -Unique) if ($remove.Count -eq 0) { - Write-Step 'Backup cleanup: nothing to remove.' + if ($pruneRequested) { + Write-Step 'Backup cleanup: nothing to remove.' + } return } foreach ($file in $remove) { if ($DoApply) { - Remove-Item -LiteralPath $file.FullName -Force - Write-Step "Removed backup $($file.Name)." + try { + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop + Write-Step "Removed backup $($file.Name)." + } + catch { + Write-Warning ("Failed to remove backup {0}: {1}" -f $file.Name, $_.Exception.Message) + } } else { Write-DryRun "Would remove backup $($file.Name). Add -Apply to delete it." diff --git a/tests/PresetResolution.Tests.ps1 b/tests/PresetResolution.Tests.ps1 index 9c07f2a..24a9280 100644 --- a/tests/PresetResolution.Tests.ps1 +++ b/tests/PresetResolution.Tests.ps1 @@ -29,3 +29,36 @@ Describe 'Preset resolution' { { Resolve-PresetPolicies -Name 'A' -Presets $cycle } | Should -Throw "Preset cycle detected at 'A'." } } + +Describe 'Backup retention' { + BeforeAll { + $root = Split-Path -Parent $PSScriptRoot + . (Join-Path $root 'src/Common.ps1') + . (Join-Path $root 'src/Backup.ps1') + } + + It 'keeps deleting backups after one removal fails' { + $directory = Join-Path ([System.IO.Path]::GetTempPath()) ('BraveDebloaterPester-{0}' -f [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $directory -Force | Out-Null + try { + foreach ($name in @('BraveDebloater-pass1.json', 'BraveDebloater-fail.json', 'BraveDebloater-pass2.json')) { + Set-Content -LiteralPath (Join-Path $directory $name) -Value '{}' -Encoding UTF8 + } + + Mock Remove-Item { + if ($LiteralPath -like '*fail.json') { + throw 'locked' + } + } + Mock Write-Warning {} + + Invoke-BackupRetention -Directory $directory -KeepLatest 0 -DoApply + + Should -Invoke Remove-Item -Times 3 -Exactly + Should -Invoke Write-Warning -Times 1 -Exactly + } + finally { + Microsoft.PowerShell.Management\Remove-Item -LiteralPath $directory -Recurse -Force -ErrorAction SilentlyContinue + } + } +}