PowerShell-on-Linux: multi-shell Calamari pipeline
Summary
Calamari can now execute PowerShell scripts natively. Operator's .ps1 main script flows through the same RunScriptCommand pipeline as .sh — same variable preamble, extract step, rewriters, convention hooks. PowerShell-on-Linux works through pwsh; Windows falls back to powershell.exe if pwsh isn't installed.
Zero new NuGet dependencies. pwsh / powershell.exe resolved at runtime via PATH probe (same pattern as 1.7.x #352 Tentacle fallback).
Operator UX
Name your file by extension — the right shell runs it:
| Extension | Syntax | Executor |
|---|---|---|
.ps1, .psm1
|
PowerShell |
pwsh → fallback powershell.exe on Windows |
.sh, no extension, anything else |
Bash |
bash (existing) |
No new wire literal. No operator opt-in. Same Squid.Calamari run-script invocation handles both.
What's in the box
| Layer | File |
|---|---|
| Syntax detector |
src/Squid.Calamari/Scripting/ScriptSyntaxDetector.cs (new) |
| PS executor |
src/Squid.Calamari/Execution/PowerShellScriptExecutor.cs (new) |
| PS executor adapter |
src/Squid.Calamari/Scripting/PowerShellScriptExecutorAdapter.cs (new) |
| PS bootstrapper |
src/Squid.Calamari/Variables/PowerShellVariableBootstrapper.cs (new) |
| Syntax-aware bootstrap step |
src/Squid.Calamari/Commands/RunScriptCommandPipeline.cs — WriteBootstrappedBashScriptStep → WriteBootstrappedScriptStep
|
| Engine registers both adapters | src/Squid.Calamari/Scripting/ScriptEngine.cs |
| Tests (27 new) |
ScriptSyntaxDetectorTests + PowerShellVariableBootstrapperTests + PowerShellScriptExecutorTests + WriteBootstrappedScriptStepTests
|
Key parity decisions
-
Same variable-name sanitiser as bash bootstrapper (
./-//→_). Operator'sMy.Varresolves under either shell without renaming. -
-NoProfile -NonInteractive -Fileargument shape matches established Squid pattern inSshExecutionStrategy+LocalProcessExecutionStrategy. -
UTF-8 stdout pin before first
$env:line — matches Tentacle'sLocalScriptService.PowerShellUtf8Preamble. Without it Windows hosts mangle Chinese/emoji in captured logs.
What's NOT in this PR (scoped out, separate follow-up)
-
Tentacle routing is unchanged.
LocalScriptService.IsCalamariCompatiblestill only returns true for Bash; PowerShell scripts dispatched through Tentacle still route via the direct launcher (PR #352 path). This PR adds Calamari's capability to handle PS; expanding Tentacle dispatch to USE it is a separate small PR. -
PreDeploy.ps1 / PostDeploy.ps1 convention scripts — convention scripts are still bash-only (file extension hard-coded in
ConventionScriptStep). Multi-shell conventions are a separate follow-up.
Test plan
-
Squid.Calamari.Tests: 449/449 (was 422; +27) -
Squid.UnitTests: 5625/5625 (unchanged) -
Solution build: 0 errors -
Pre-merge audit GREEN on 8/8 invariants -
Tentacle.Tests env-dependent failures unchanged from main baseline (Python/F#/K8s tests need installs locally) -
Staging: .ps1deploy with non-ASCII variable values (CJK + emoji) round-tripping through captured logs
Non-breaking guarantee
| Surface | Status |
|---|---|
Existing .sh deploys |
Byte-identical behaviour — WriteBootstrappedScriptStep picks Bash for everything without .ps1/.psm1
|
| Existing scripts without extension | Default Bash (existing behaviour preserved) |
| Wire literals | Unchanged (zero new literals) |
| Tentacle dispatch routing | Unchanged — PS still goes through launcher, not Calamari |
| IIS PS1 deploy path | Unchanged — separate from Calamari |
WriteBootstrappedBashScriptStep class name |
Renamed to WriteBootstrappedScriptStep (internal class — only Calamari + 2 doc-strings in Tentacle updated) |
| SquidWeb | Untouched |