Skip to content

G1.5: PreDeploy / PostDeploy convention hooks

Placeholder ppxd requested to merge feat/calamari-g1-5-convention-hooks into main

Summary

Adds two new pipeline steps that look for PreDeploy.sh and PostDeploy.sh in the working directory (typically extracted by G1.4 from the package) and run them via the same script engine + variable preamble as the operator's main script.

Stacks on #369 (G1.4 ExtractPackageStep). Together they make a typical operator workflow end-to-end: package arrives → extract → rewriter pipeline → PreDeploy hook → main script → PostDeploy hook → cleanup.

Pipeline shape after this PR

ResolveWorkingDirectory → LoadVariablesFromFiles →
  ExtractPackage (G1.4) →
  SubstituteInFiles → ConfigurationTransforms → JsonConfigVariables (G1.1-3) →
  **PreDeploy (G1.5)** →
  WriteBootstrappedBashScript → ExecuteScriptWithEngine [operator's main] →
  **PostDeploy (G1.5)** →
  BuildRunScriptCommandResult → CleanupTemporaryFiles

Behaviour

Concern Behaviour
Script absent from working dir Step is a no-op (most packages won't ship conventions)
Variable scope Same export-VAR preamble as the main script — identical scope
Non-zero exit Throws → halts deploy. PreDeploy fails before main runs; PostDeploy fails after main runs but before cleanup
Output variables emitted by the hook Merge into context.Variables — PreDeploy can compute values for main; PostDeploy can harvest values main set
Temp file tracking Bootstrapped temp added to context.TemporaryFiles so cleanup gets it

Wire-contract surface

Const Value Purpose
ConventionScriptNames.PreDeploy "PreDeploy" Operators name files in their packages this exact string
ConventionScriptNames.PostDeploy "PostDeploy" Same

Pinned by tests (Rule 8 drift detectors).

What's deferred

DeployFailed.sh (Octopus has it as a fourth convention — runs only on execution failure) is intentionally not in this PR. Modelling it cleanly needs the pipeline context to expose a "execution failed" flag for an IAlwaysRunExecutionStep variant to read. Will land separately if operator demand surfaces.

Test plan

  • Squid.Calamari.Tests: 314/314 (was 300; +14)
  • Squid.UnitTests: 5625/5625 (cross-project — unchanged)
  • Solution build: 0 errors
  • Wire literals (PreDeploy, PostDeploy) pinned (Rule 8)
  • Gating: present-runs / absent-skips / sibling-convention-doesn't-match / WorkingDir-null-doesn't-crash
  • Bootstrapping: temp file fed to engine (not original source) + preamble correctly composed + tracked for cleanup
  • Failure: non-zero exit code throws with operator-actionable message
  • Output variables flow back into the shared variable set
  • Defensive: empty convention name + null engine + null WorkingDir all throw cleanly
  • Staging: real .nupkg containing PreDeploy.sh + PostDeploy.sh end-to-end

Non-breaking guarantee

Surface Status
Existing wire literals Unchanged
Standalone-script deploys (no PreDeploy.sh / PostDeploy.sh) Zero impact — both steps' IsEnabled returns false
Operator's main script execution Unchanged — same ExecuteScriptWithEngineStep between the two new hook positions
Frontend Untouched — convention scripts are file-system-driven, no UI wire literals needed

Merge request reports

Loading