-
Notifications
You must be signed in to change notification settings - Fork 0
Failure Handling
Tamp distinguishes five distinct things that all sound similar but mean different things. Pick the right one.
| Need | Use |
|---|---|
| "If this target's predicate is false, skip silently and move on" | OnlyWhen(Func<bool>) |
| "If this target's predicate is false, abort the build" | Requires(Func<bool>) |
| "This target's failure shouldn't stop the build" | FailureMode(FailureMode.Continue) |
| "Run this target only when target X fails" | OnFailureOf(nameof(X)) |
| "Run this target whether the build succeeded or failed" | AssuredAfterFailure() |
| "Retry on transient failure" | Retry(count, Backoff, ...exitCodes) |
The rest of this page explains each one in depth.
The default condition. [CallerArgumentExpression] captures the predicate text for skip messages and dry-run output.
Target Push => _ => _
.OnlyWhen(() => Git.Branch == "main")
.DependsOn(nameof(Pack))
.Executes(() => DotNet.NuGetPush(...));Output:
==> Push (skipped: Git.Branch == "main")
Multiple OnlyWhen calls accumulate — all must hold.
A throwing predicate is treated as a skip with the exception type and message included in the skip reason.
Same shape as OnlyWhen, opposite outcome. Failing condition aborts the build with the captured expression text.
Target Deploy => _ => _
.Requires(() => DeploymentToken != null)
.Requires(() => Environment != "production" || ApprovedAt.HasValue)
.Executes(() => /* … */);Use Requires for "this target can't possibly run without X." Use OnlyWhen for "this target is irrelevant when X." Different intent → different signal in the build summary.
A failing Requires triggers OnFailureOf handlers the same way any other failure does.
Three modes, applied per-target:
| Mode | Behaviour |
|---|---|
Fatal (default) |
Target failure aborts the build. Other downstream targets become NotRun. |
Continue |
Target failure is logged but the build keeps going. |
Retry |
Retry per Backoff. Use the Retry(...) setter, which sets this mode automatically. |
Target SoftCheck => _ => _
.FailureMode(FailureMode.Continue)
.Executes(() => /* check that's nice-to-pass but not blocking */);
Target Flaky => _ => _
.Retry(count: 3, Backoff.Exponential(TimeSpan.FromSeconds(1)))
.Executes(() => /* network-dependent thing */);Backoff factories:
| Factory | Shape |
|---|---|
Backoff.Linear(step) |
step * attempt |
Backoff.Exponential(initial, factor=2.0) |
initial * factor^(attempt-1) |
Backoff.Constant(delay) |
always the same |
Backoff.Custom(Func<int, TimeSpan>) |
bring your own |
Status: Retry is recorded on the spec but not yet implemented in the v0 sequential executor. The setting carries forward to a v0.x executor.
Tamp-specific addition NUKE doesn't have. The handler runs only when one of the named targets fails. Distinct from anything else:
Target Deploy => _ => _
.Executes(() => /* the risky thing */);
Target NotifyOpsOnFailure => _ => _
.OnFailureOf(nameof(Deploy))
.Description("Slack the on-call channel if Deploy fails")
.Executes(() => Slack.Post("#oncall", $"Deploy failed at {Git.Commit[..7]}"));-
NotifyOpsOnFailureis not in the normal plan.tamp Deploydoes not pull it in; it runs only as a reaction toDeployfailing. - The handler's own dependency tree runs first — handler can declare deps like any other target.
- A handler's failure does not reverse the original failure.
Deployis still theFailedTargetin the result. - Multiple handlers for the same failed target all run; they're independent.
-
OnFailureOfis intentionally excluded from cycle detection — runtime-only. - The handler can read
Git,Solution,[Parameter]properties — same context as any target.
OnFailureOf(X) |
AssuredAfterFailure() |
|
|---|---|---|
| Runs when X succeeds? | ❌ | ✅ |
| Runs when X fails? | ✅ | ✅ |
| In the normal plan? | ❌ | ✅ |
| Conditional on a specific target? | ✅ | ❌ |
OnFailureOf is a catch handler. AssuredAfterFailure is unconditional cleanup.
Marks a target as cleanup that should run whether the build succeeded or failed. Used for things like:
- Container
docker logout - Tearing down a test database
- Notifying telemetry
- Uploading partial logs
Target Cleanup => _ => _
.AssuredAfterFailure()
.DependsOn(nameof(Compile)) // still in the plan; still has deps
.Executes(() => /* always run this */);When an earlier target fails, the executor switches into "post-failure mode": only assured-after-failure targets keep running. Everything else gets NotRun status.
A failing assured-after-failure target doesn't reverse the original failure. The original FailedTarget is preserved.
Target FlakyDownload => _ => _
.Retry(count: 5,
Backoff.Exponential(TimeSpan.FromSeconds(2)),
retryableExitCodes: new[] { 137, 139 }) // SIGKILL/SIGSEGV from busy runners
.Executes(() => DownloadArtifact(...));Setting Retry automatically switches the target's FailureMode to Retry. The retryableExitCodes filter is optional — without it, every non-zero exit retries.
Status: Recorded; not yet enforced by the v0 executor. A retry-aware executor is on the v0.x track.
The end-of-run table shows status per target:
─── Build Summary ───
Target Status Duration
Restore ✓ Done 1.2 s
Compile ✓ Done 14.5 s
Test ✓ Done 8.3 s
Pack ✗ Failed 4.1 s ← original failure
Notify ✓ Done 0.8 s ← OnFailureOf(Pack) ran
CleanArtifacts ✓ Done 0.1 s ← AssuredAfterFailure
─────
Total 29.0 s
BUILD FAILED — first failed target: Pack
The build's exit code reflects the original failure, not the cleanup or handler outcome.
- Build Script Authoring — full DSL surface
- Logging & Verbosity — controlling what the summary and per-target output looks like
Start here
Modules
- Module Catalog (canonical list, 50+ satellites)
- .NET toolchain
- Containers
- JS toolchain
- Supply-chain security
Analyzers
Tooling
Execution
CI integration
Editor integration
Migration
Reference
Project