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
99 changes: 99 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,105 @@ Windows Update driver listing, run-to-run delta and HTML report. See CLAUDE.md.

---

## [2.21] - 2026-07-23

A release about telling the truth on the way out. The self-update could change a file
other than the one being run and still print "Update complete", and the exit code called
a perfectly good maintenance run a failure whenever the machine simply lacked an optional
tool or a network. Both are the same underlying habit: reporting a state the run did not
actually verify.

### Fixed

- **The self-update updated a file that was not being run, and reported success.** The
check asked whether a PowerShell Gallery copy existed *anywhere on the machine*, and the
answer was acted on as if it had been *"is the file I am running that copy"*. Both are
routinely true at once: `install.ps1` (2.15) installs into `%ProgramFiles%\WinClean`,
which is what the desktop shortcut starts, while an older `Install-Script` copy can
still sit in `Documents\PowerShell\Scripts`. `Update-Script` then updated the copy in
Documents, printed "Update complete! Please run WinClean again to use the new version",
and exited 0 - and the shortcut kept starting the untouched old file, run after run,
with no error anywhere. The running file is now compared against the Gallery install
location, and a self-update is offered only when they are the same file. Every other
copy is told the method that actually applies to it. Not a 2.20 regression: the branch
dates from 2.10, and two parallel installations became an ordinary state in 2.15
- **The advice shown to non-Gallery copies created the very problem above.** It read
`Install-Script -Name WinClean -Scope CurrentUser -Force`, which installs a *second*
copy in Documents and leaves the running one untouched - building the two-installation
state that the update logic then misread
- **An update that reported success is now verified against the file on disk.** "The
cmdlet did not throw" is not "the running file is now the new version"; the version is
read back from the executing script afterwards, and anything short of the expected
version is reported as a warning instead of being announced as complete. The check
states the final version of that file, which is what the next run will use; it does not
attempt to prove which actor put it there. Measured on 2026-07-22 with each provider in
turn: either reports the other's install, so detection does not depend on which one
performed it
- **The self-update now works on a machine that has only PSResourceGet.** Every step used
to be PowerShellGet: `Find-Script` for discovery and `Update-Script` for the update.
Where PowerShellGet is absent, discovery threw and the surrounding catch turned that into
"no update available" - it logged a warning, but no update was ever offered, and the
manual instruction named a command that machine cannot run. Discovery, the update and the
printed advice now each use whichever provider is present, and a discovery failure is
reported as a counted warning instead of resembling "you are up to date"
- **Several Gallery installations now disable the automatic update instead of guessing.**
`AllUsers` and `CurrentUser` copies can coexist; `Update-Script` has no `-Scope` at all,
and while `Update-PSResource` does have one, WinClean does not map a matched install
location back to a scope, so nothing currently directs the update at the copy being
executed. Verifying afterwards would report the miss honestly, but only after the unused
copy had already been modified. WinClean now says several installations exist, prints the
path it is running from, and leaves them alone - the same rule the Storage Sense lookup
follows: an ambiguous target is not acted on and the reason is stated
- **An `AllUsers` copy is now visible to PSResourceGet detection.** `Get-PSResource`
defaults to `CurrentUser` and searches only the Documents paths, so on a machine with
PSResourceGet but no PowerShellGet an `AllUsers` install - the natural scope for a script
that requires administrator - was invisible, and the running copy was told it did not
come from the Gallery and pointed at the installer, adding a second installation

### Changed

- **A failed self-update is a warning, not an error.** It was logged at `ERROR` without
incrementing the error counter, so the run printed "Update failed" and still exited 0 -
a contradiction in the one place that has to be believable. Failing to update the script
is not a failure of the maintenance that was actually requested, so it is now a counted
warning and the exit code agrees with the log
- **No internet connection is a warning, not an error.** Same rule as the missing `winget`
below, and for the same measurable reason: the exit code comes from the error count
alone, so a machine with no connectivity ended **every** run with code 1 no matter how
completely the cleanup succeeded - a laptop that runs maintenance away from the network
reported failure forever. Both update functions read the same connectivity check, so
both are warnings now, and the state stays visible where it belongs: in the log and in
`AppUpdatesStatus: "skipped-offline"`, which covers the Windows half too
- **A missing `winget` is a warning, not an error.** The exit code is computed from the
error count alone, so every run on a machine without App Installer ended with code 1
while all nine phases completed - which any scheduler, CI job or test harness reads as a
failed run. The absence of an optional third-party tool is a property of the machine,
not a failure of the run, by the same rule that makes a machine without Docker a normal
machine here. A `winget` that is present and then fails is still reported, at a severity
that follows whether the run can carry on: the upgrade check failing outright and an
unhandled error remain errors, while a stale source, a timeout or a partly failed batch
are warnings. Behaviour unchanged since 1.2; found by a full end-to-end run of the
published release, because ordinary stand runs pass `-SkipUpdates` and never reach it

### Documentation

- README (EN and RU) gained an **Updating an existing installation** table: update the
copy you actually run, with the method you installed it with
- `docs/troubleshooting.md` explains the "update complete but the version never changes"
symptom, how to inspect and clean up a two-installation machine, and states plainly that
a missing `winget` no longer makes the run exit non-zero
- `docs/result-json.md` documents `AppUpdatesStatus`, the new `UpdatedAndExited` value of
`Aborted`, and what each status means for reading `AppUpdatesOffered`

### Tests

- 452 to 573. New coverage for update-channel classification, provider discovery and
selection across both package providers, on-disk verification of an applied update, and
the severity of every skip in the Updates phase. Twenty-one mutations were run against
the new guards and each one failed a test

---

## [2.20] - 2026-07-22

A correctness and honesty round driven by a full audit of the code base, a third-party
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ CleanScript/
│ └── logo.svg # Логотип проекта
├── get.ps1 # Bootstrap: разовый запуск одной командой (irm | iex)
├── install.ps1 # Bootstrap: установка/обновление + ярлык (RunAs admin)
├── tests/ # Pester тесты (452 всего; счётчик - прогоном, не грепом)
│ ├── Helpers.Tests.ps1 # Unit-тесты helper-функций (159, дот-сорсят продукт - нужны права админа)
├── tests/ # Pester тесты (573 всего; счётчик - прогоном, не грепом)
│ ├── Helpers.Tests.ps1 # Unit-тесты helper-функций (280, дот-сорсят продукт - нужны права админа)
│ ├── Fixes.Tests.ps1 # Валидационные тесты исправлений (183)
│ ├── Integration.Tests.ps1 # Интеграционные тесты в песочнице ФС (67, требуют admin)
│ ├── StandHelpers.Tests.ps1 # Чистые хелперы стенда: dead-man + версионный гейт ассертов (17, без admin)
Expand Down Expand Up @@ -198,7 +198,7 @@ Publish-PSResource -Path .\WinClean.ps1 -Repository PSGallery -ApiKey $env:PSGAL
**Проверки (4 job'а, счёт сверять gh run view --json jobs, а не этой строкой):**
1. **lint** - PSScriptAnalyzer (Error+Warning) через общий ools/Invoke-Lint.ps1 - тот же, что зовёт релиз-гейт
2. **syntax** - Проверка синтаксиса PowerShell
3. **test** - Pester тесты (452; интеграционные требуют admin - на GitHub runners это выполняется)
3. **test** - Pester тесты (573; интеграционные требуют admin - на GitHub runners это выполняется)
4. **smoke** - прогон -ReportOnly + геометрия рамок + result JSON

**Исключения PSScriptAnalyzer** (допустимые для CLI):
Expand All @@ -208,7 +208,7 @@ Publish-PSResource -Path .\WinClean.ps1 -Repository PSGallery -ApiKey $env:PSGAL

### Pester тесты (v2.13+)

- `tests/Helpers.Tests.ps1` - 159 unit-тестов (дот-сорсят WinClean.ps1), `tests/Fixes.Tests.ps1` - 183, `tests/Integration.Tests.ps1` - 67 (песочница ФС, требуют admin), `tests/StandHelpers.Tests.ps1` - 17 (без admin), `tests/Docs.Tests.ps1` - 26 (гварды доков, без admin)
- `tests/Helpers.Tests.ps1` - 280 unit-тестов (дот-сорсят WinClean.ps1), `tests/Fixes.Tests.ps1` - 183, `tests/Integration.Tests.ps1` - 67 (песочница ФС, требуют admin), `tests/StandHelpers.Tests.ps1` - 17 (без admin), `tests/Docs.Tests.ps1` - 26 (гварды доков, без admin)
- Особенности: функции в BeforeAll (не AST), regex для locale-независимости, отдельные It блоки

---
Expand Down Expand Up @@ -379,7 +379,7 @@ pwsh tools/Invoke-ReleaseCheck.ps1 # версия во всех
pwsh tools/Invoke-ReleaseCheck.ps1 -IncludeStand # + боевой прогон на VM (минуты)
pwsh tools/Invoke-ReleaseCheck.ps1 -VerifyPublished # ПОСЛЕ выпуска: ассеты релиза и SHA256

Invoke-Pester ./tests -Output Detailed # 452 Pester тестов (считать прогоном, не грепом)
Invoke-Pester ./tests -Output Detailed # 573 Pester тестов (считать прогоном, не грепом)
pwsh tools/Invoke-SmokeTest.ps1 # Смоук: ReportOnly + геометрия UI
pwsh tools/proxmox/Invoke-StandTest.ps1 -Mode Report # Стенд на Proxmox (RU=VM 190, EN: -ConfigPath ...en.json = VM 191)
# Ночная матрица: cron 03:30 на proxmos (/opt/winclean-stand, /etc/cron.d/winclean-stand), отчёт в Telegram
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Invoke-Pester ./tests/Integration.Tests.ps1
All PRs automatically run:
- PSScriptAnalyzer (linting)
- Syntax check
- Pester tests (452 tests)
- Pester tests (573 tests)

### Release-impacting changes

Expand Down Expand Up @@ -251,7 +251,7 @@ Invoke-Pester ./tests/Integration.Tests.ps1
Все PR автоматически проходят:
- PSScriptAnalyzer (линтинг)
- Проверка синтаксиса
- Pester тесты (452 тестов)
- Pester тесты (573 тестов)

### Изменения, влияющие на релиз

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,24 @@ cd WinClean
.\WinClean.ps1
```

### 🔄 Updating an existing installation

Update the copy you actually run, with the method you installed it with:

| Installed with | Update with |
|----------------|-------------|
| `install.ps1` (desktop shortcut) | re-run `irm https://raw.githubusercontent.com/bivlked/WinClean/main/install.ps1 \| iex` from an elevated terminal |
| `get.ps1` one-liner | nothing to do - every run downloads the latest release |
| PowerShell Gallery (`Install-Script`) | `Update-Script -Name WinClean` |
| PowerShell Gallery (`Install-PSResource`) | `Update-PSResource -Name WinClean` |
| Manual download or clone | download the latest release again |

When a newer version exists, WinClean names the option that applies to the copy you
launched (the check runs at startup unless `-SkipUpdates` is passed, and says nothing when
you are already up to date). Only a copy installed from the Gallery can update itself; the others cannot,
and are told what to do instead rather than being offered an update that would leave the
running file untouched.

</details>

---
Expand Down Expand Up @@ -303,7 +321,7 @@ WinClean is built to be safe to run on a working machine. The short version:

```
┌────────────────────────────────────────────────────────────────┐
│ WinClean v2.20
│ WinClean v2.21
├────────────────────────────────────────────────────────────────┤
│ PREPARATION │
│ ├─ ✓ Check Administrator Rights │
Expand Down
20 changes: 19 additions & 1 deletion README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,24 @@ cd WinClean
.\WinClean.ps1
```

### 🔄 Обновление установленной копии

Обновляйте ту копию, которую реально запускаете, тем же способом, каким ставили:

| Как установлено | Как обновить |
|-----------------|--------------|
| `install.ps1` (ярлык на рабочем столе) | повторно выполнить `irm https://raw.githubusercontent.com/bivlked/WinClean/main/install.ps1 \| iex` в окне с правами администратора |
| One-liner `get.ps1` | ничего не нужно: каждый запуск скачивает последний релиз |
| PowerShell Gallery (`Install-Script`) | `Update-Script -Name WinClean` |
| PowerShell Gallery (`Install-PSResource`) | `Update-PSResource -Name WinClean` |
| Ручная загрузка или клон | скачать последний релиз заново |

Когда в галерее есть версия новее, WinClean называет тот вариант, который подходит именно
запущенной копии (проверка идёт при запуске, если не передан `-SkipUpdates`, и молчит,
когда у вас уже последняя версия). Обновить себя может только копия, установленная из галереи; остальным
скрипт говорит, что делать, вместо предложения обновления, которое оставило бы
выполняемый файл нетронутым.

</details>

---
Expand Down Expand Up @@ -303,7 +321,7 @@ WinClean создан так, чтобы его можно было безопа

```
┌────────────────────────────────────────────────────────────────┐
│ WinClean v2.20
│ WinClean v2.21
├────────────────────────────────────────────────────────────────┤
│ ПОДГОТОВКА │
│ ├─ ✓ Проверка прав администратора │
Expand Down
Loading
Loading