ci(tools): remove the temporary cross-platform generator check

It did its job: it caught that the script did not parse under Windows
PowerShell 5.1, and confirmed identical behavior under pwsh on Linux, macOS and
Windows plus a clean #requires refusal on 5.1.

The fixes it drove stay -- #requires -Version 7.0, ASCII-only source, portable
path joins, culture-invariant parsing and the single-rename atomic swap. Only
the scaffolding goes, so the tools/ path does not carry a networked workflow
into main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-07-25 01:53:02 -07:00
parent 737d3ec4ce
commit cb2a33113a
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A

View file

@ -1,252 +0,0 @@
# TEMPORARY -- delete this file before merging PR #2542.
#
# Verifies tools/Export-IpBlocklist.ps1 runs correctly under pwsh on every OS, and that it refuses to
# run on Windows PowerShell 5.1 with a clear message rather than a wall of parse errors. Most of the
# checks are offline: the script's feed-name filter throws AFTER the Add-Type
# compile and the cooldown gate but BEFORE any download, so "did it throw 'No feeds matched'?" is a
# deterministic signal that it got that far without touching the network.
#
# That gives us discriminating assertions for the two bugs this exists to catch:
# * path join -- a fresh list is written via Join-Path, then the script is pointed at the folder.
# If its own path join is wrong for this OS it will not find the file and will run
# instead of skipping.
# * duration -- a 3h-old list with -MinInterval 2.5h must RUN. A culture-sensitive parse reads
# "2.5" as 25 under a comma-decimal locale, which would skip instead.
#
# One step does hit the network, using the smallest feed in the set (~200KB), because the file write
# and the atomic swap cannot be exercised without content to write.
name: Tools PS1 Check
on:
workflow_dispatch:
pull_request:
paths:
- 'tools/Export-IpBlocklist.ps1'
- '.github/workflows/tools-ps1-check.yml'
jobs:
check:
runs-on: ${{ matrix.os }}
timeout-minutes: 10
name: ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
name: Linux (pwsh)
shell: pwsh
- os: macos-latest
name: macOS (pwsh)
shell: pwsh
- os: windows-latest
name: Windows (pwsh)
shell: pwsh
defaults:
run:
shell: ${{ matrix.shell }}
steps:
- uses: actions/checkout@v7
- name: Environment
run: |
"PSVersion : $($PSVersionTable.PSVersion)"
"PSEdition : $($PSVersionTable.PSEdition)"
"Platform : $([System.Environment]::OSVersion.Platform)"
"Separator : $([IO.Path]::DirectorySeparatorChar)"
"Culture : $((Get-Culture).Name) (decimal '$((Get-Culture).NumberFormat.NumberDecimalSeparator)')"
# ASCII-only is a hard requirement, not style. Windows PowerShell reads a BOM-less .ps1 as ANSI, and
# a UTF-8 em dash decodes to a trailing U+201D -- a smart quote PowerShell accepts as a string
# delimiter. One of those inside a string ends it early and the file dies in parse errors BEFORE
# #requires is honored, so the "needs PowerShell 7" message never gets a chance to print.
- name: Source is ASCII-only
run: |
$bytes = [IO.File]::ReadAllBytes("$PWD/tools/Export-IpBlocklist.ps1")
$bad = @()
for ($i = 0; $i -lt $bytes.Length; $i++) { if ($bytes[$i] -gt 127) { $bad += $i } }
if ($bad.Count -gt 0) {
"::error::Non-ASCII bytes at offsets: $($bad[0..([Math]::Min(9,$bad.Count-1))] -join ', ')" +
" ($($bad.Count) total). Windows PowerShell would fail to parse this before #requires runs."
exit 1
}
"OK (ASCII-only, $($bytes.Length) bytes)"
- name: Syntax parses
run: |
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile(
"$PWD/tools/Export-IpBlocklist.ps1", [ref]$null, [ref]$errs)
if ($errs) {
$errs | ForEach-Object { "::error::$($_.Extent.StartLineNumber): $($_.Message)" }
exit 1
}
"OK"
# Proves the script's own Join-Path is right for this OS: it must locate the list we wrote at
# <Dist>/Configuration/ip-blocklist.txt and skip. A literal 'Configuration\ip-blocklist.txt' would
# miss it on Linux/macOS and fall through to the feed filter instead.
# Also proves Add-Type compiled, since that runs before the feed filter.
- name: Offline - default path resolution and cooldown skip
run: |
$dist = Join-Path $env:RUNNER_TEMP 'Distribution'
$cfg = Join-Path $dist 'Configuration'
New-Item -ItemType Directory -Path $cfg -Force | Out-Null
$stamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ', [Globalization.CultureInfo]::InvariantCulture)
Set-Content -LiteralPath (Join-Path $cfg 'ip-blocklist.txt') -Value @(
"# modernuo-blocklist generated=$stamp count=1", "9.9.9.9")
# *>&1 (not 2>&1): the script reports through Write-Host, which is the information stream.
# Progress records are silenced so the captured text stays readable.
$ProgressPreference = 'SilentlyContinue'
$out = ''
try { $out = (& "$PWD/tools/Export-IpBlocklist.ps1" -DistributionPath $dist -Feeds '__none__' *>&1 | Out-String) }
catch { $out = "EXCEPTION: $($_.Exception.Message)" }
$out
if ($out -notmatch 'Nothing downloaded') {
"::error::Expected the cooldown gate to skip. The script did not find the list at the path it " +
"resolved, which means its path join is wrong for this OS."
exit 1
}
"OK"
# Discriminates an invariant duration parse from a culture-sensitive one. The list is 3h old, so
# -MinInterval 2.5h must NOT skip; a parse that reads "2.5" as 25 would skip. Reaching the feed
# filter (which throws) is the proof it passed the gate -- still no download.
- name: Offline - duration parsing is culture-invariant
run: |
$dist = Join-Path $env:RUNNER_TEMP 'DistributionAged'
$cfg = Join-Path $dist 'Configuration'
New-Item -ItemType Directory -Path $cfg -Force | Out-Null
$stamp = [DateTime]::UtcNow.AddHours(-3).ToString('yyyy-MM-ddTHH:mm:ssZ', [Globalization.CultureInfo]::InvariantCulture)
Set-Content -LiteralPath (Join-Path $cfg 'ip-blocklist.txt') -Value @(
"# modernuo-blocklist generated=$stamp count=1", "9.9.9.9")
$cultures = @([Globalization.CultureInfo]::InvariantCulture)
$comma = [Globalization.CultureInfo]::GetCultureInfo('de-DE')
if ($comma.NumberFormat.NumberDecimalSeparator -eq ',') {
$cultures += $comma
} else {
"::warning::de-DE resolved without a comma decimal separator (globalization-invariant mode?); " +
"running the invariant culture only."
}
# Passing the gate is signalled by the feed filter throwing, so the throw is an expected
# outcome to capture rather than a failure to propagate.
$ProgressPreference = 'SilentlyContinue'
$original = [Threading.Thread]::CurrentThread.CurrentCulture
foreach ($c in $cultures) {
[Threading.Thread]::CurrentThread.CurrentCulture = $c
$out = ''
try { $out = (& "$PWD/tools/Export-IpBlocklist.ps1" -DistributionPath $dist -MinInterval '2.5h' -Feeds '__none__' *>&1 | Out-String) }
catch { $out = "EXCEPTION: $($_.Exception.Message)" }
finally { [Threading.Thread]::CurrentThread.CurrentCulture = $original }
"--- culture '$($c.Name)' ---"
$out
if ($out -match 'Nothing downloaded') {
"::error::Under culture '$($c.Name)' a 3h-old list was treated as newer than -MinInterval 2.5h. " +
"The duration parse is culture-sensitive ('2.5' read as 25)."
exit 1
}
if ($out -notmatch 'No feeds matched') {
"::error::Under culture '$($c.Name)' the script neither skipped nor reached the feed filter. Unexpected."
exit 1
}
}
"OK"
# The only networked step. Uses the smallest feed in the set so CI stays cheap and polite, and is
# the only way to cover the file write, the atomic swap (File.Move 3-arg vs File.Replace) and the
# on-disk format.
- name: Online - end to end write, format and idempotent re-run
run: |
$ProgressPreference = 'SilentlyContinue'
$dist = Join-Path $env:RUNNER_TEMP 'DistributionLive'
New-Item -ItemType Directory -Path $dist -Force | Out-Null
$file = Join-Path (Join-Path $dist 'Configuration') 'ip-blocklist.txt'
# A throw inside the script surfaces as a terminating error and fails the step on its own.
& "$PWD/tools/Export-IpBlocklist.ps1" -DistributionPath $dist -Feeds 'sentinel-turris'
if (-not (Test-Path -LiteralPath $file)) {
"::error::No blocklist written to $file"; exit 1
}
if (Test-Path -LiteralPath "$file.tmp") {
"::error::Left a .tmp behind next to the list"; exit 1
}
# Header must match what UOContent/Misc/Blocklist/BlocklistFile.cs reads, with an invariant
# timestamp -- the shard compares generated= verbatim to decide whether to reload.
$header = Get-Content -LiteralPath $file -TotalCount 1
"header: $header"
if ($header -notmatch '^# modernuo-blocklist generated=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z count=(\d+)') {
"::error::Header does not match the format the shard parses"; exit 1
}
$declared = [int]$Matches[1]
if ($declared -lt 1) { "::error::Header declares $declared entries"; exit 1 }
# LF-only: the C# writer emits '\n' explicitly and the reader trims, but CRLF here would mean
# something re-encoded the file.
$bytes = [IO.File]::ReadAllBytes($file)
if ([Array]::IndexOf($bytes, [byte]13) -ge 0) {
"::error::Output contains CR; expected LF-only line endings"; exit 1
}
# Every body line must be an address or CIDR the shard can parse.
$lines = [IO.File]::ReadAllLines($file)
if (($lines.Count - 1) -ne $declared) {
"::error::Header says $declared entries, file has $($lines.Count - 1)"; exit 1
}
$checked = 0
foreach ($line in $lines[1..[Math]::Min(500, $lines.Count - 1)]) {
$addr = ($line -split '/')[0]
$parsed = [Net.IPAddress]::Any
if (-not [Net.IPAddress]::TryParse($addr, [ref]$parsed)) {
"::error::Unparseable entry: '$line'"; exit 1
}
$checked++
}
"validated $checked of $declared entries"
# Re-running must skip: proves the header this OS wrote is readable by the gate on this OS.
$again = & "$PWD/tools/Export-IpBlocklist.ps1" -DistributionPath $dist -Feeds 'sentinel-turris' *>&1 | Out-String
if ($again -notmatch 'Nothing downloaded') {
"::error::Re-run did not skip; the generated header is not round-tripping"; exit 1
}
# ...and -Force must override the cooldown and swap the file in place.
$before = (Get-Item -LiteralPath $file).Length
& "$PWD/tools/Export-IpBlocklist.ps1" -DistributionPath $dist -Feeds 'sentinel-turris' -Force *> $null
if (Test-Path -LiteralPath "$file.tmp") { "::error::-Force left a .tmp behind"; exit 1 }
if ((Get-Item -LiteralPath $file).Length -lt 1) { "::error::-Force produced an empty list"; exit 1 }
"OK (rewrote $before-byte list in place)"
# The script targets pwsh only. Windows PowerShell must refuse it via #requires with a readable
# message -- which is only possible because the source is ASCII and therefore still parses there.
refuses-windows-powershell:
runs-on: windows-latest
timeout-minutes: 5
name: Windows PowerShell 5.1 refuses cleanly
steps:
- uses: actions/checkout@v7
- name: Fails with the #requires message, not parse errors
shell: powershell
run: |
"PSVersion: $($PSVersionTable.PSVersion)"
$out = ''
try { $out = (& "$PWD/tools/Export-IpBlocklist.ps1" -DryRun *>&1 | Out-String) }
catch { $out = "EXCEPTION: $($_.Exception.Message)" }
$out
if ($out -match 'Unexpected token|Missing closing') {
"::error::Windows PowerShell hit parse errors instead of the #requires message. The source is " +
"probably no longer ASCII-only."
exit 1
}
if ($out -notmatch 'requires') {
"::error::Expected a #requires refusal naming PowerShell 7."
exit 1
}
"OK - refused cleanly"