Skip to content

Add PreDeploy + PostDeploy custom-script hooks to IIS deploy (Phase 5)

Placeholder ppxd requested to merge feat/iis-deploy-custom-scripts into main

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
🟢 Unit 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
🟡 Pipeline E2E 1 Theory × 2 = 2 cases Variable substitution INTO script body — #{PoolName} resolves BEFORE the builder base64-encodes; helper DecodeBase64Body() for asserting decoded content
🟢 Real-host 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
  • _sentinelFilesToClean cleanup pass in Dispose()

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~IISDeploy on macOS — 56 unit tests green
  • dotnet test --filter Category=IISDeployE2E on macOS — 36 real-host tests skip cleanly
  • dotnet build 0 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.

Merge request reports

Loading