free-claude-code/scripts/ci.ps1
2026-06-18 18:17:31 -07:00

210 lines
5 KiB
PowerShell

param(
[string[]] $Only = @(),
[string[]] $Skip = @(),
[switch] $DryRun,
[switch] $Help,
[Parameter(ValueFromRemainingArguments = $true)]
[object[]] $RemainingArgs = @()
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$CheckOrder = @(
"suppressions",
"ruff-format",
"ruff-check",
"ty",
"pytest"
)
function Show-Usage {
@"
Usage: ci.ps1 [options]
Runs the local sequence for the same check IDs enforced by GitHub CI.
Requires uv on PATH when running ruff, ty, or pytest checks.
Local ruff checks repair formatting and autofixable lint before later checks.
Checks (in order):
suppressions Ban # type: ignore / # ty: ignore suppressions
ruff-format uv run ruff format
ruff-check uv run ruff check --fix
ty uv run ty check
pytest uv run pytest -v --tb=short
Options:
-Only ID Run only the given check (repeatable)
-Skip ID Skip the given check (repeatable)
-DryRun Print commands without running them.
-Help Show this help text.
"@
}
function Write-Step {
param([string] $Message)
Write-Host ""
Write-Host "==> $Message"
}
function Format-Argument {
param([string] $Value)
if ($Value -match '^[A-Za-z0-9_./:@%+=,\[\]-]+$') {
return $Value
}
return "'" + ($Value -replace "'", "''") + "'"
}
function Invoke-CiCommand {
param(
[string] $FilePath,
[string[]] $Arguments = @()
)
$parts = @($FilePath) + $Arguments
$commandText = ($parts | ForEach-Object { Format-Argument ([string] $_) }) -join " "
Write-Host "+ $commandText"
if (-not $DryRun) {
& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Command failed with exit code ${LASTEXITCODE}: $commandText"
}
}
}
function Test-ValidCheckId {
param([string] $CheckId)
return $CheckOrder -contains $CheckId
}
function Assert-ValidCheckId {
param([string] $CheckId)
if (-not (Test-ValidCheckId $CheckId)) {
throw "unknown check id: $CheckId (expected one of: $($CheckOrder -join ', '))"
}
}
function Test-ShouldRunCheck {
param([string] $CheckId)
if ($Only.Count -gt 0 -and ($Only -notcontains $CheckId)) {
return $false
}
if ($Skip -contains $CheckId) {
return $false
}
return $true
}
function Assert-UvAvailable {
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
throw "uv is required but was not found on PATH. Install uv first (see README or scripts/install.ps1)."
}
}
function Test-SelectedChecksNeedUv {
if ($DryRun) {
return $false
}
foreach ($checkId in $CheckOrder) {
if ((Test-ShouldRunCheck $checkId) -and $checkId -ne "suppressions") {
return $true
}
}
return $false
}
function Invoke-SuppressionsCheck {
Write-Step "Ban type ignore suppressions"
$pattern = '# type: ignore|# ty: ignore'
Write-Host "+ Get-ChildItem -Recurse -Filter *.py (excluding .venv, .git) | Select-String '$pattern'"
if (-not $DryRun) {
$matches = Get-ChildItem -Path . -Recurse -Filter *.py -File |
Where-Object {
$full = $_.FullName
$full -notmatch '[\\/]\.venv[\\/]' -and
$full -notmatch '[\\/]\.git[\\/]'
} |
Select-String -Pattern $pattern
if ($matches) {
$matches | ForEach-Object { Write-Host $_.Line }
throw "type: ignore / ty: ignore comments are not allowed. Fix the underlying type errors instead."
}
}
}
function Invoke-RuffFormatCheck {
Write-Step "ruff format"
Invoke-CiCommand -FilePath "uv" -Arguments @("run", "ruff", "format")
}
function Invoke-RuffLintCheck {
Write-Step "ruff check --fix"
Invoke-CiCommand -FilePath "uv" -Arguments @("run", "ruff", "check", "--fix")
}
function Invoke-TyCheck {
Write-Step "ty check"
Invoke-CiCommand -FilePath "uv" -Arguments @("run", "ty", "check")
}
function Invoke-PytestCheck {
Write-Step "pytest"
Invoke-CiCommand -FilePath "uv" -Arguments @("run", "pytest", "-v", "--tb=short")
}
function Invoke-Check {
param([string] $CheckId)
switch ($CheckId) {
"suppressions" { Invoke-SuppressionsCheck }
"ruff-format" { Invoke-RuffFormatCheck }
"ruff-check" { Invoke-RuffLintCheck }
"ty" { Invoke-TyCheck }
"pytest" { Invoke-PytestCheck }
default { throw "unknown check id: $CheckId" }
}
}
if ($Help) {
Show-Usage
return
}
if ($RemainingArgs.Count -gt 0) {
Show-Usage
throw "Unknown option: $($RemainingArgs -join ' ')"
}
foreach ($checkId in $Only) {
Assert-ValidCheckId $checkId
}
foreach ($checkId in $Skip) {
Assert-ValidCheckId $checkId
}
if (Test-SelectedChecksNeedUv) {
Assert-UvAvailable
}
foreach ($checkId in $CheckOrder) {
if (Test-ShouldRunCheck $checkId) {
Invoke-Check $checkId
}
}
Write-Host ""
Write-Host "All selected CI checks passed."