fix: a rebuild no longer deletes the installers it is about to copy (… #126
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: checks | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| # Read-only token: nothing here writes to the repo. | |
| permissions: | |
| contents: read | |
| # A new push to the same branch makes the previous run pointless. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| powershell: | |
| runs-on: windows-latest | |
| # A hung job is not a failed job: without this it holds a runner until the | |
| # six-hour default. Pester 6 hangs this suite (see the Pester step below), | |
| # which is exactly the shape of failure a timeout has to cover. | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # `shell: powershell` is Windows PowerShell 5.1; `pwsh` would be 7. | |
| # DiscWright requires 5.1, so 5.1 is the only parser whose verdict means | |
| # anything here - 7 accepts syntax that 5.1 rejects. | |
| - name: Parse every script under Windows PowerShell 5.1 | |
| shell: powershell | |
| run: | | |
| $failed = $false | |
| foreach ($f in Get-ChildItem -Recurse -Filter *.ps1) { | |
| $errors = $null | |
| [void][System.Management.Automation.Language.Parser]::ParseFile( | |
| $f.FullName, [ref]$null, [ref]$errors) | |
| if ($errors -and $errors.Count) { | |
| $failed = $true | |
| Write-Host "::error file=$($f.Name)::$($errors.Count) parse error(s)" | |
| foreach ($e in $errors) { | |
| Write-Host " line $($e.Extent.StartLineNumber): $($e.Message)" | |
| } | |
| } else { | |
| Write-Host "ok $($f.Name)" | |
| } | |
| } | |
| if ($failed) { exit 1 } | |
| # The ISO build tests skip themselves if the runner has no IMAPI2FS, and the | |
| # ISO content tests skip if it has no 7-Zip, so the job still means something | |
| # on a machine that cannot do either. | |
| - name: Pester | |
| shell: powershell | |
| run: | | |
| # Pester 5, deliberately, and not merely "5 or later" - the same | |
| # pin tests/Invoke-Tests.ps1 makes, for the reason written out there: | |
| # under 6.1.0 every file in this suite hangs in BeforeAll. Hosted | |
| # runners ship 5.x today, so -MinimumVersion 5.0 happens to import 5.x | |
| # today. It would import 6.x the day that changes, and a hung job just | |
| # runs until the timeout above rather than telling anybody why. | |
| $p5 = Get-Module -ListAvailable Pester | | |
| Where-Object { $_.Version.Major -eq 5 } | | |
| Sort-Object Version -Descending | Select-Object -First 1 | |
| if (-not $p5) { | |
| Set-PSRepository PSGallery -InstallationPolicy Trusted | |
| Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser -MinimumVersion 5.0 -MaximumVersion 5.999.999 | |
| $p5 = Get-Module -ListAvailable Pester | | |
| Where-Object { $_.Version.Major -eq 5 } | | |
| Sort-Object Version -Descending | Select-Object -First 1 | |
| } | |
| Import-Module $p5.Path -Force | |
| Write-Host "Pester $((Get-Module Pester).Version)" | |
| $cfg = New-PesterConfiguration | |
| $cfg.Run.Path = 'tests' | |
| # tests/ui drives the real window: it launches the app, moves the | |
| # pointer and needs a desktop that behaves like a person's. A hosted | |
| # runner has a virtual screen, so the suite's own "is there a desktop" | |
| # guard says yes and the tests then fail on a display nothing can | |
| # really use. Excluded by path AND by tag, because Run.Path is | |
| # recursive and would otherwise pick up the file again the moment | |
| # anything is renamed. | |
| $cfg.Run.ExcludePath = @((Join-Path $PWD 'tests\ui')) | |
| $cfg.Filter.ExcludeTag = @('UI') | |
| $cfg.Run.Exit = $true | |
| $cfg.Output.Verbosity = 'Detailed' | |
| $cfg.TestResult.Enabled = $true | |
| $cfg.TestResult.OutputPath = 'pester-results.xml' | |
| Invoke-Pester -Configuration $cfg | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pester-results | |
| path: pester-results.xml | |
| if-no-files-found: ignore | |
| # Fails the build on Errors only. Warnings are printed but tolerated: the | |
| # default rule set has strong opinions about WinForms code that are not | |
| # worth rewriting a working app over, and a check that always fails is a | |
| # check everybody learns to ignore. Which rules run is in | |
| # PSScriptAnalyzerSettings.psd1, with the reasoning for each exclusion. | |
| - name: PSScriptAnalyzer | |
| shell: powershell | |
| run: | | |
| if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) { | |
| Set-PSRepository PSGallery -InstallationPolicy Trusted | |
| Install-Module PSScriptAnalyzer -Force -Scope CurrentUser | |
| } | |
| # Invoke-ScriptAnalyzer does find the settings file on its own when it | |
| # sits in the analyzed path, but naming it means the result does not | |
| # depend on where the job happens to be standing. | |
| # docs/research holds throwaway harnesses preserved exactly as they were | |
| # run, as the evidence behind a decision recorded in ROADMAP.md. They are | |
| # not part of DiscWright and will never be edited - tidying them would | |
| # make them no longer the scripts that produced the numbers. Linting them | |
| # buys nothing and costs two dozen warnings on every run, and a check | |
| # that always shouts is a check everybody learns to ignore. | |
| $results = @(Invoke-ScriptAnalyzer -Path . -Recurse -Settings .\PSScriptAnalyzerSettings.psd1 | | |
| Where-Object { $_.ScriptPath -notlike '*\docs\research\*' }) | |
| $errors = @($results | Where-Object { $_.Severity -eq 'Error' }) | |
| $warnings = @($results | Where-Object { $_.Severity -eq 'Warning' }) | |
| foreach ($w in $warnings) { | |
| Write-Host "::warning file=$($w.ScriptName),line=$($w.Line)::$($w.RuleName): $($w.Message)" | |
| } | |
| foreach ($e in $errors) { | |
| Write-Host "::error file=$($e.ScriptName),line=$($e.Line)::$($e.RuleName): $($e.Message)" | |
| } | |
| Write-Host "" | |
| Write-Host "$($errors.Count) error(s), $($warnings.Count) warning(s)" | |
| if ($errors.Count) { exit 1 } | |
| # GitHub serves repository files without a BOM. Windows PowerShell 5.1 | |
| # decodes a BOM-less file as the system ANSI codepage rather than UTF-8, so | |
| # one stray non-ASCII byte in a script silently mangles on any machine whose | |
| # codepage is not the author's. Everything executable is ASCII today; this | |
| # keeps it that way. Documentation is exempt - GitHub renders it as UTF-8. | |
| - name: Executable files must stay pure ASCII | |
| shell: powershell | |
| run: | | |
| $failed = $false | |
| foreach ($f in Get-ChildItem -Recurse -Include *.ps1,*.vbs,*.cmd) { | |
| $bytes = [IO.File]::ReadAllBytes($f.FullName) | |
| $bad = @() | |
| for ($i = 0; $i -lt $bytes.Length; $i++) { | |
| if ($bytes[$i] -gt 127) { $bad += $i } | |
| } | |
| if ($bad.Count) { | |
| $failed = $true | |
| Write-Host "::error file=$($f.Name)::$($bad.Count) non-ASCII byte(s), first at offset $($bad[0])" | |
| } else { | |
| Write-Host "ok $($f.Name)" | |
| } | |
| } | |
| if ($failed) { exit 1 } | |
| # There is deliberately no job here for tests/ui. | |
| # | |
| # Those tests drive the real window, and a hosted runner's desktop is 1024x768 | |
| # with nobody sitting at it. The window needs 1032px of height, so it opens | |
| # scrolled, controls sit off-screen, and a synthesized click lands on nothing. | |
| # Measured: every window test that only READS state passes there, and every one | |
| # that has to CLICK something below the fold fails. | |
| # | |
| # Running it anyway behind continue-on-error was worse than not running it. The | |
| # check still reported red, and a red check that everyone is told to ignore | |
| # teaches people to ignore red checks. | |
| # | |
| # They run locally, where the desktop is real and they are trustworthy: | |
| # | |
| # .\tests\Invoke-Tests.ps1 both suites | |
| # Invoke-Pester tests/ui the window alone | |
| # | |
| # A version constant maintained by hand drifts, because bumping it is a step | |
| # that gets forgotten. This runs only when a tag is pushed and fails the tag if | |
| # the app would report a version it is not. | |
| version-matches-tag: | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: App version must match the tag | |
| shell: powershell | |
| run: | | |
| # GITHUB_REF_NAME rather than an expression, so the tag arrives as data | |
| # instead of being pasted into the script before it runs. | |
| $tag = $env:GITHUB_REF_NAME | |
| $expected = $tag -replace '^v','' | |
| $m = [regex]::Match((Get-Content DiscWright.ps1 -Raw), '\$APP_VERSION\s*=\s*''([^'']+)''') | |
| if (-not $m.Success) { | |
| Write-Host "::error::no `$APP_VERSION assignment found in DiscWright.ps1" | |
| exit 1 | |
| } | |
| $actual = $m.Groups[1].Value | |
| Write-Host "tag: $tag expects: $expected DiscWright.ps1 says: $actual" | |
| if ($actual -ne $expected) { | |
| Write-Host "::error::`$APP_VERSION is '$actual' but the tag is '$tag' - bump the constant before tagging" | |
| exit 1 | |
| } | |
| Write-Host "ok" |