Add PreDeploy + PostDeploy custom-script hooks to IIS deploy (Phase 5)
Summary
Mirrors Octopus's Octopus.Action.CustomScripts.{Stage}.ps1 taxonomy (KnownVariables.cs:27-35) by embedding two operator-extensibility hooks directly inside the IIS deploy script. PreDeploy runs BEFORE the IIS configure dispatch (canonical use: Stop-WebAppPool to release file locks). PostDeploy runs AFTER successful IIS configure (canonical use: smoke-test, Start-WebAppPool, Invoke-WebRequest /health).
This closes the operator-experience gap with Octopus for the most-used custom-script stages. Operators get the standard "stop pool → reconfigure → start pool → smoke" pattern in a single Squid action instead of composing 3 separate Squid.Script+IIS steps.
What ships
Production (+97 LOC across 3 files)
| File | Δ | What |
|---|---|---|
IISDeployProperties.cs |
+30 |
CustomScriptsPreDeploy / CustomScriptsPostDeploy constants — keys match Octopus exactly (Squid.Action.CustomScripts.PreDeploy.ps1 etc.) for portable operator specs |
IISDeployScriptBuilder.cs |
+65 |
ScriptContentProperties hashset + dispatched emission. Data values → single-quote escape (unchanged). Script content → base64 round-trip emission so newlines/apostrophes survive byte-for-byte |
DeployToIISWebSite.ps1 |
+32 | Two hook blocks: PreDeploy fires before Invoke-Command -ScriptBlock $DeployIISScriptBlock, PostDeploy fires after. Both isolated via [scriptblock]::Create(). WebAdministration auto-imported |
Test code (+498 LOC, 12 new tests across 3 tiers)
| Tier | New | What |
|---|---|---|
|
|
5 | base64 emission shape; empty-script optimization (skips base64 for empty values); adversarial round-trip preservation (apostrophes + newlines + dollar signs); drift-detector invariant pinning the new hook blocks + their ordering relative to Invoke-Command
|
|
|
1 Theory × 2 = 2 cases | Variable substitution INTO script body — #{PoolName} resolves BEFORE the builder base64-encodes; helper DecodeBase64Body() for asserting decoded content |
|
|
5 | PreDeploy sentinel write → file exists after deploy; PostDeploy sentinel queries Get-Website.State → proves PostDeploy sees configured site; PreDeploy throw → aborts before configure, no site created, PostDeploy doesn't fire; multi-line PreDeploy → both statements execute (proves newlines survive base64); realistic Stop-WebAppPool/Start-WebAppPool workflow → pool ends Started
|
Test infrastructure
-
IISTestContext.RegisterSentinelPath(suffix)— unique sentinel file path + cleanup registration -
_sentinelFilesToCleancleanup pass inDispose()
Why base64 instead of single-quote escape for script content
Operator scripts contain newlines (statement separators in PowerShell). Single-quote single-line escape collapses \n → space, which turns Stop-WebAppPool MyPool\nStart-Sleep 2 into one undefined command. base64 round-trip preserves every byte. Verified via Build_PreDeployScriptWithApostrophesAndNewlines_RoundTripsExactlyViaBase64.
Cumulative coverage after Phase 5
| Tier | Phase 4 | Phase 5 |
|---|---|---|
| Unit | 51 | 56 (+5) |
| Pipeline E2E | 8 | 9 (+1) |
| Real-host | 31 | 36 (+5) |
Windows tentacle suite expected ~131 → ~136 in ~12-13 min.
Octopus alignment notes
Octopus's ConfiguredScriptBehaviour runs custom scripts at a higher orchestration layer (across all package-deploy types). Squid Phase 5 embeds the equivalent hooks INSIDE the IIS deploy script — same operator-facing contract (variable names, script body semantics), different runtime layer. The drift-detector test EmbeddedScript_HasPreDeployAndPostDeployHooks_ForOctopusCustomScriptsParity is explicitly Squid-only and not part of KeyOperations.
Test plan
-
dotnet test --filter FullyQualifiedName~IISDeployon macOS — 56 unit tests green -
dotnet test --filter Category=IISDeployE2Eon macOS — 36 real-host tests skip cleanly -
dotnet build0 errors -
CI run on windows-latest: 5 new real-host tests against real IIS
Out of scope
- Phase 6: Variable substitution INTO files (
SubstituteInFiles) - Phase 7: XML config transforms (XDT)
- Phase 8: AppSettings/ConnectionStrings rewriter
- Phase 9: JSON/YAML structured config
- Phase 10: Package extraction (NuGet/zip)
- Phase 11: Custom installation directory + purge
- Phase 12: IIS certificate auto-import + private-key ACL
Breaking-change risk
None. Pure additive: new property names, new builder dispatch path for script properties, new PS1 hook blocks that no-op when slots are empty. All existing tests unchanged.