From 6801537fe20431e94a07b5e8bc6fb84d0409bf85 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:22:25 +0100 Subject: [PATCH] fix(scripts): make PowerShell scripts parse under Windows PowerShell 5.1 The SDL scan reports 14 PSScriptAnalyzer findings across three scripts: MissingEndCurlyBrace, UnexpectedToken, MissingTypename, IncompleteHashLiteral, MissingCatchOrFinally, TerminatorExpectedAtEndOfString. They are parse failures, not style lints, and they have two unrelated causes. Parsing each file with both engines separates them: WinPS 5.1 PS 7.6 release-crate.ps1 6 0 test-anvil-container.ps1 4 0 test-anvil-dogfood.ps1 4 0 1. Encoding. The scripts are UTF-8 with no BOM, so Windows PowerShell decodes them as Windows-1252. The emoji then land as CP1252 bytes 0x91-0x94, which are the smart quotes, and PowerShell accepts those as string delimiters. release-crate.ps1 contains six such bytes, the first at the changelog header map, and the parser loses quote pairing from there on. Every reported line is a cascade: line 433 is plain ASCII and correct. Re-decoding the same bytes as UTF-8 yields zero errors, confirming the syntax was never wrong. run-examples.ps1 uses the check and cross marks, whose UTF-8 bytes also include 0x93 and 0x97, and parses today only because the resulting quotes happen to pair up. 2. `??`. test-anvil-{container,dogfood}.ps1 use the null-coalescing operator, which is PowerShell 7 only. This is genuine syntax the 5.1 tokenizer cannot accept, and no encoding change helps. Fix both. Replace non-ASCII with ASCII, and replace `?? ''` with a `[string]` cast: casting $null to string yields the empty string, so the behaviour is identical on both engines. The scripts remain valid PowerShell 7. A UTF-8 BOM also fixes cause 1 with no content change, and was verified to do so. ASCII is preferred here because it cannot regress through an editor that drops the BOM, and because the emoji are console output on a host whose default console encoding cannot render them. The one substantive change is the changelog header map, which loses its emoji prefixes. No committed CHANGELOG.md uses them today. After this change all eight scripts under scripts/ parse with zero errors under both Windows PowerShell 5.1 and PowerShell 7.6, and contain no non-ASCII bytes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/release-crate.ps1 | 42 ++++++++++++++++---------------- scripts/run-examples.ps1 | 16 ++++++------ scripts/test-anvil-container.ps1 | 12 ++++----- scripts/test-anvil-dogfood.ps1 | 4 +-- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/scripts/release-crate.ps1 b/scripts/release-crate.ps1 index c118c4fd1..e417c0bb7 100644 --- a/scripts/release-crate.ps1 +++ b/scripts/release-crate.ps1 @@ -69,17 +69,17 @@ $script:TypeGroupMapping = @{ # Maps the final group key to a user-friendly header in the changelog. $script:HeaderNameMapping = @{ - 'breaking' = '⚠️ Breaking'; - 'build' = 'πŸ—οΈ Build System'; - 'ci' = 'πŸ”„ Continuous Integration'; - 'docs' = 'πŸ“š Documentation'; - 'feat' = '✨ Features'; - 'fix' = 'πŸ› Bug Fixes'; - 'miscellaneous' = '🧩 Miscellaneous'; - 'perf' = '⚑ Performance'; - 'refactor' = '♻️ Code Refactoring'; - 'style' = '🎨 Styling'; - 'task' = 'βœ”οΈ Tasks'; + 'breaking' = 'Breaking'; + 'build' = 'Build System'; + 'ci' = 'Continuous Integration'; + 'docs' = 'Documentation'; + 'feat' = 'Features'; + 'fix' = 'Bug Fixes'; + 'miscellaneous' = 'Miscellaneous'; + 'perf' = 'Performance'; + 'refactor' = 'Code Refactoring'; + 'style' = 'Styling'; + 'task' = 'Tasks'; } # Defines the preferred order for commit type sections in the changelog. @@ -422,17 +422,17 @@ function Update-CrateVersion { } $newVersion = $versionParts -join '.' - Write-Host "βœ… Incrementing $bumpType version from $currentVersion to $newVersion." + Write-Host "Incrementing $bumpType version from $currentVersion to $newVersion." } else { $newVersion = $version - Write-Host "βœ… Using specified version: $newVersion." + Write-Host "Using specified version: $newVersion." } - Write-Host "πŸ“ Updating '$crateCargoToml'..." + Write-Host "Updating '$crateCargoToml'..." (Get-Content $crateCargoToml -Raw) -replace '(?<=version\s*=\s*")[^"]+', $newVersion | Set-Content $crateCargoToml -NoNewline - Write-Host "πŸ“ Updating '$rootCargoToml'..." + Write-Host "Updating '$rootCargoToml'..." function Get-EscapedRegexSpecialChars($str) { # Escape all regex metacharacters: . $ ^ { [ ( | ) * + ? \ / @@ -516,7 +516,7 @@ function Write-Changelog { ($newVersionSection -join "`n") + "`n" + $existingContent.Substring($insertPosition) $newContent | Set-Content $changelogFile -NoNewline - Write-Host "βœ… Changelog updated at '$changelogFile'." + Write-Host "Changelog updated at '$changelogFile'." return } } @@ -526,7 +526,7 @@ function Write-Changelog { $changelogContent = @("# Changelog", "") $changelogContent += $newVersionSection $changelogContent | Out-File -FilePath $changelogFile -Encoding utf8 - Write-Host "βœ… Changelog created at '$changelogFile'." + Write-Host "Changelog created at '$changelogFile'." } function Update-Readme { @@ -546,14 +546,14 @@ function Update-Readme { return } - Write-Host "πŸ“ Updating README.md..." + Write-Host "Updating README.md..." Push-Location $crateFolder try { $result = cargo doc2readme --lib --template ../README.j2 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to generate README: $result" } else { - Write-Host "βœ… README.md updated." + Write-Host "README.md updated." } } finally { @@ -575,7 +575,7 @@ function Show-DependentCratesWarning { ) Write-Host "" - Write-Host "⚠️ SEMVER-INCOMPATIBLE RELEASE DETECTED" -ForegroundColor Yellow + Write-Host "WARNING: SEMVER-INCOMPATIBLE RELEASE DETECTED" -ForegroundColor Yellow Write-Host "The version bump from $oldVersion to $newVersion is semver-incompatible." -ForegroundColor Yellow Write-Host "" Write-Host "The following workspace crates have a direct dependency on '$crateName'" -ForegroundColor Yellow @@ -601,7 +601,7 @@ function Show-FinalMessage { ) Write-Host "---" -ForegroundColor Green - Write-Host "πŸŽ‰ Success! Next steps:" -ForegroundColor Green + Write-Host "Success! Next steps:" -ForegroundColor Green Write-Host "1. Review the changes in the updated files." -ForegroundColor Green Write-Host "2. Commit the changes and push the changes:" -ForegroundColor Green Write-Host " git add ." -ForegroundColor DarkGray diff --git a/scripts/run-examples.ps1 b/scripts/run-examples.ps1 index 3824cf09e..7ea219a96 100644 --- a/scripts/run-examples.ps1 +++ b/scripts/run-examples.ps1 @@ -97,11 +97,11 @@ foreach ($pkg in $packages_to_process) { $output = $result[0..($result.Length - 2)] -join "`n" # All output except exit code if ($exit_code -eq 0) { - Write-Host "βœ“ Example '$example_name' in package '$pkg' completed successfully" -ForegroundColor Green + Write-Host "PASS: Example '$example_name' in package '$pkg' completed successfully" -ForegroundColor Green $success_count++ } else { - Write-Host "βœ— Example '$example_name' in package '$pkg' failed with exit code $exit_code" -ForegroundColor Red + Write-Host "FAIL: Example '$example_name' in package '$pkg' failed with exit code $exit_code" -ForegroundColor Red if ($output.Trim() -ne "") { Write-Host "Output:" -ForegroundColor Yellow Write-Host $output -ForegroundColor DarkYellow @@ -110,7 +110,7 @@ foreach ($pkg in $packages_to_process) { } } else { - Write-Host "βœ— Example '$example_name' in package '$pkg' timed out after $timeout_seconds seconds" -ForegroundColor Red + Write-Host "FAIL: Example '$example_name' in package '$pkg' timed out after $timeout_seconds seconds" -ForegroundColor Red $failures += "$pkg::$example_name (timeout)" Stop-Job -Job $job } @@ -119,7 +119,7 @@ foreach ($pkg in $packages_to_process) { } catch { - Write-Host "βœ— Example '$example_name' in package '$pkg' failed with exception: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "FAIL: Example '$example_name' in package '$pkg' failed with exception: $($_.Exception.Message)" -ForegroundColor Red $failures += "$pkg::$example_name (exception: $($_.Exception.Message))" } } @@ -155,11 +155,11 @@ foreach ($pkg in $packages_to_process) { $output = $result[0..($result.Length - 2)] -join "`n" # All output except exit code if ($exit_code -eq 0) { - Write-Host "βœ“ Example '$example_name' in package '$pkg' completed successfully" -ForegroundColor Green + Write-Host "PASS: Example '$example_name' in package '$pkg' completed successfully" -ForegroundColor Green $success_count++ } else { - Write-Host "βœ— Example '$example_name' in package '$pkg' failed with exit code $exit_code" -ForegroundColor Red + Write-Host "FAIL: Example '$example_name' in package '$pkg' failed with exit code $exit_code" -ForegroundColor Red if ($output.Trim() -ne "") { Write-Host "Output:" -ForegroundColor Yellow Write-Host $output -ForegroundColor DarkYellow @@ -168,7 +168,7 @@ foreach ($pkg in $packages_to_process) { } } else { - Write-Host "βœ— Example '$example_name' in package '$pkg' timed out after $timeout_seconds seconds" -ForegroundColor Red + Write-Host "FAIL: Example '$example_name' in package '$pkg' timed out after $timeout_seconds seconds" -ForegroundColor Red $failures += "$pkg::$example_name (timeout)" Stop-Job -Job $job } @@ -177,7 +177,7 @@ foreach ($pkg in $packages_to_process) { } catch { - Write-Host "βœ— Example '$example_name' in package '$pkg' failed with exception: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "FAIL: Example '$example_name' in package '$pkg' failed with exception: $($_.Exception.Message)" -ForegroundColor Red $failures += "$pkg::$example_name (exception: $($_.Exception.Message))" } } diff --git a/scripts/test-anvil-container.ps1 b/scripts/test-anvil-container.ps1 index 94d6a39c7..a2a4f5ecc 100644 --- a/scripts/test-anvil-container.ps1 +++ b/scripts/test-anvil-container.ps1 @@ -8,7 +8,7 @@ .DESCRIPTION Creates a throwaway repository in a temp directory, generates the anvil tree into it with the locally-built cargo-anvil, and then does only what a - developer would do: run `just anvil-container ` and observe what + developer would do: run `just anvil-container ` and observe what happens. The setup phase is held to that standard deliberately. If this script has to @@ -22,7 +22,7 @@ 2. The first run builds an image and runs the recipe inside it. 3. A second run reuses the image (the tag resolves, nothing is built), no cache volume masks the tools the image installed, and a host - GITHUB_TOKEN is forwarded β€” from the environment, or from the gh CLI + GITHUB_TOKEN is forwarded -- from the environment, or from the gh CLI when the environment has none. 3b. A recipe run from a linked worktree can still reach git history. 4. Changing a hashed input (the pinned toolchain) selects a new tag. @@ -123,8 +123,8 @@ function Invoke-Native { -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile $result = [pscustomobject]@{ ExitCode = $process.ExitCode - StdOut = (Get-Content -LiteralPath $stdoutFile -Raw -ErrorAction SilentlyContinue) ?? '' - StdErr = (Get-Content -LiteralPath $stderrFile -Raw -ErrorAction SilentlyContinue) ?? '' + StdOut = [string](Get-Content -LiteralPath $stdoutFile -Raw -ErrorAction SilentlyContinue) + StdErr = [string](Get-Content -LiteralPath $stderrFile -Raw -ErrorAction SilentlyContinue) } } finally { Remove-Item -LiteralPath $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue @@ -217,7 +217,7 @@ function Test-ImagePresent([string]$Reference) { function Remove-AnvilImages([string]$Prefix) { $images = Invoke-Engine -Arguments @('images', '--format', '{{.Repository}}:{{.Tag}}') -AllowFailure - # Podman reports images fully qualified (`localhost/anvil-…`), docker does + # Podman reports images fully qualified (`localhost/anvil-...`), docker does # not, so match anywhere in the reference rather than at the start. $matching = ($images.StdOut -split "`r?`n") | Where-Object { $_ -like "*$Prefix*" } foreach ($image in $matching) { @@ -579,7 +579,7 @@ Assert-That 'content in a gap survives regeneration' ($afterRegen -match 'a repo Assert-Equal 'the composed tag is unchanged by regeneration' $editedReference (Get-ImageReference -Repo $repo) # Anvil never overwrites repository content, and a region body is no exception: -# an edit inside one is preserved, exactly as `updates.md` Β§2 preserves an +# an edit inside one is preserved, exactly as `updates.md` section 2 preserves an # edited owned file. That is why the gaps matter -- editing inside a region # silently freezes the base digest and the tool pins at today's values while # the tag keeps resolving, so the layout has to make the gaps the obvious place diff --git a/scripts/test-anvil-dogfood.ps1 b/scripts/test-anvil-dogfood.ps1 index b677af323..e4ede012a 100644 --- a/scripts/test-anvil-dogfood.ps1 +++ b/scripts/test-anvil-dogfood.ps1 @@ -153,8 +153,8 @@ function Invoke-Native { -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile $result = [pscustomobject]@{ ExitCode = $process.ExitCode - StdOut = (Get-Content -LiteralPath $stdoutFile -Raw -ErrorAction SilentlyContinue) ?? '' - StdErr = (Get-Content -LiteralPath $stderrFile -Raw -ErrorAction SilentlyContinue) ?? '' + StdOut = [string](Get-Content -LiteralPath $stdoutFile -Raw -ErrorAction SilentlyContinue) + StdErr = [string](Get-Content -LiteralPath $stderrFile -Raw -ErrorAction SilentlyContinue) } } finally { Remove-Item -LiteralPath $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue