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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **`install.ps1` refused to run on every released PowerShell 7, saying it could not read
the version.** Reported from a user machine running 7.6.4. `pwsh.exe` reports
`ProductVersion` as `7.6.4 SHA: <hash>+<hash>` - the commit hash follows a **space**,
while the code stripped a `-suffix` (which only covers preview builds), so the `[version]`
cast threw and the version came out `$null`. The numeric `ProductMajorPart` /
`ProductMinorPart` / `ProductBuildPart` fields are read instead; there is no string to
misparse. The parse had been broken since the check was written - the pre-2.22 form
`if ($pwshVersion -and ...)` skipped the comparison instead of reporting it, so the
installer silently ran with **no version check at all** for three releases. Making the
check fail-closed in 2.22 is what made it visible. This fix is live as soon as it reaches
`main`: the one-liner fetches `install.ps1` from the branch, not from a release asset, so
no new release is required. `get.ps1` was never affected - it runs inside PowerShell and
reads `$PSVersionTable`.
- The only guard on that check was a **grep test**: it asserted the shape of the code and
never evaluated it, which is why 702 green tests did not notice. It is now joined by a
behavioural test that executes `install.ps1`'s own expression against the real `pwsh.exe`
on the test machine and asserts the result clears 7.1. Verified by mutation: with the old
expression restored, the behavioural test fails while the grep test beside it still
passes. 702 -> 704 tests.

Planned for a later release: quick system health section (SMART, image integrity, WinRE),
Windows Update driver listing, run-to-run delta and HTML report. See CLAUDE.md.

Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ CleanScript/
│ └── logo.svg # Логотип проекта
├── get.ps1 # Bootstrap: разовый запуск одной командой (irm | iex)
├── install.ps1 # Bootstrap: установка/обновление + ярлык (RunAs admin)
├── tests/ # Pester тесты (702 всего; счётчик - прогоном, не грепом)
├── tests/ # Pester тесты (704 всего; счётчик - прогоном, не грепом)
│ ├── Helpers.Tests.ps1 # Unit-тесты helper-функций (378, дот-сорсят продукт - нужны права админа)
│ ├── Fixes.Tests.ps1 # Валидационные тесты исправлений (206)
│ ├── Fixes.Tests.ps1 # Валидационные тесты исправлений (208)
│ ├── Integration.Tests.ps1 # Интеграционные тесты в песочнице ФС (67, требуют admin)
│ ├── StandHelpers.Tests.ps1 # Чистые хелперы стенда: dead-man + версионный гейт + таблица истинности report-гварда (23, без admin)
│ └── Docs.Tests.ps1 # Гварды документации: нет тире + нет битых ссылок (28, без 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 тесты (702; интеграционные требуют admin - на GitHub runners это выполняется)
3. **test** - Pester тесты (704; интеграционные требуют 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` - 378 unit-тестов (дот-сорсят WinClean.ps1), `tests/Fixes.Tests.ps1` - 206, `tests/Integration.Tests.ps1` - 67 (песочница ФС, требуют admin), `tests/StandHelpers.Tests.ps1` - 23 (без admin), `tests/Docs.Tests.ps1` - 28 (гварды доков, без admin)
- `tests/Helpers.Tests.ps1` - 378 unit-тестов (дот-сорсят WinClean.ps1), `tests/Fixes.Tests.ps1` - 208, `tests/Integration.Tests.ps1` - 67 (песочница ФС, требуют admin), `tests/StandHelpers.Tests.ps1` - 23 (без admin), `tests/Docs.Tests.ps1` - 28 (гварды доков, без admin)
- Особенности: функции в BeforeAll (не AST), regex для locale-независимости, отдельные It блоки

---
Expand Down Expand Up @@ -447,7 +447,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 # 702 Pester тестов (считать прогоном, не грепом)
Invoke-Pester ./tests -Output Detailed # 704 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 (702 tests)
- Pester tests (704 tests)

### Release-impacting changes

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

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

Expand Down
18 changes: 17 additions & 1 deletion install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,23 @@ if (-not (Test-Path $pwshPath)) {
# had established. That is the same fail-open shape as the SHA256 verification that hid
# inside `if ($hashAsset)` until v2.17: a check that cannot run becomes a check that
# passes. Absence of evidence is not evidence of compatibility.
$pwshVersion = try { [version](((Get-Item $pwshPath).VersionInfo.ProductVersion -split '-')[0]) } catch { $null }
#
# Read the NUMERIC version fields rather than parsing the display string. pwsh.exe reports
# ProductVersion as "7.6.4 SHA: 929d27f4...+929d27f4..." - the commit hash is appended after
# a SPACE, so stripping a "-suffix" (which only covers preview builds like "7.7.0-preview.2")
# left the hash in place and the [version] cast threw on every released PowerShell 7. Making
# the check fail-closed above is what surfaced it: the parse had never worked, and the old
# form skipped the comparison instead of reporting it, so the failure was invisible for three
# releases. ProductMajorPart/MinorPart/BuildPart are integers taken straight from the version
# resource - there is no string to misparse.
$pwshVersion = try {
$vi = (Get-Item $pwshPath).VersionInfo
# A genuine PowerShell 7 binary never reports major 0; that value means the version
# resource is missing or unreadable, which must stay fail-closed.
if ($vi.ProductMajorPart -gt 0) {
[version]::new($vi.ProductMajorPart, $vi.ProductMinorPart, $vi.ProductBuildPart)
} else { $null }
} catch { $null }
if (-not $pwshVersion) {
# Deliberately a different message from "your version is too old": the user needs to
# know the file is there but unreadable, which points at a damaged or substituted
Expand Down
40 changes: 40 additions & 0 deletions tests/Fixes.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,46 @@ Describe "v2.17: bootstrap verification is mandatory" -Tag "Fix", "V217" {
$code | Should -Match 'version could not be read'
$code | Should -Not -Match '\$pwshVersion -and \$pwshVersion -lt'
}

It "install.ps1 actually reads the version of the real pwsh.exe (v2.23)" {
# Reported from a user machine running PowerShell 7.6.4: the installer refused to
# run, saying the version could not be read. It could not - pwsh reports
# ProductVersion as "7.6.4 SHA: <hash>+<hash>", the hash separated by a SPACE, so
# stripping a "-suffix" left it in place and the [version] cast threw on EVERY
# released PowerShell 7. The parse had been broken since it was written; the
# fail-open form above it skipped the comparison instead of reporting it, which is
# exactly why nobody could see it for three releases.
#
# The grep test above passed throughout, because it checks the SHAPE of the code and
# never evaluates it. This one runs install.ps1's own expression against the real
# binary on this machine - the only way the defect could have been caught.
$installPath = (Resolve-Path (Join-Path $PSScriptRoot '..' 'install.ps1')).Path
$ast = [System.Management.Automation.Language.Parser]::ParseFile($installPath, [ref]$null, [ref]$null)
$assign = @($ast.FindAll({
param($n)
$n -is [System.Management.Automation.Language.AssignmentStatementAst] -and
$n.Left.Extent.Text -eq '$pwshVersion'
}, $true))[0]
$assign | Should -Not -BeNullOrEmpty -Because 'install.ps1 must still compute $pwshVersion'

$pwshPath = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles)) 'PowerShell\7\pwsh.exe'
$pwshPath | Should -Exist -Because 'these tests run under PowerShell 7 from the canonical location'

# Run the product's line, then hand its result back out of the scriptblock.
$result = & ([scriptblock]::Create($assign.Extent.Text + "`n`$pwshVersion"))

$result | Should -Not -BeNullOrEmpty -Because 'a working PowerShell 7 must yield a version'
$result | Should -BeOfType ([version])
($result -ge [version]'7.1') | Should -BeTrue -Because "the parsed version ($result) must clear the 7.1 gate"
}

It "install.ps1 does not parse the pwsh version out of the display string (v2.23)" {
# ProductVersion is a display string that carries a commit hash; the numeric
# ProductMajorPart/MinorPart/BuildPart fields cannot be misparsed.
$code = ($installScript -split "`n" | Where-Object { $_ -notmatch '^\s*#' }) -join "`n"
$code | Should -Not -Match 'ProductVersion\s+-split'
$code | Should -Match 'ProductMajorPart'
}
}

Describe "v2.18: bootstrap host allowlist is exact, not a broad suffix" -Tag "Fix", "V218" -ForEach @(
Expand Down
Loading