Skip to content

Build Script Authoring

Scott Singleton edited this page May 15, 2026 · 6 revisions

Build Script Authoring

A Tamp build is a regular .NET console project. There's no DSL, no manifest format, no tamp.json — just C# with a fluent target API. This page is the reference for that API.

Anatomy

using Tamp;
using Tamp.NetCli.V10;

class Build : TampBuild
{
    public static int Main(string[] args) => Execute<Build>(args);

    [Parameter] Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
    [Solution] readonly Solution Solution = null!;

    AbsolutePath Artifacts => RootDirectory / "artifacts";

    Target Clean => _ => _
        .Executes(() => CleanArtifacts());        // 1.1.0+ helper: bin/obj/Artifacts wipe with self-evict guard

    Target Restore => _ => _
        .Internal()                               // 1.1.0+: hidden from --list / `tamp` no-args
        .Executes(() => DotNet.Restore(s => s.SetProject(Solution.Path)));

    Target Compile => _ => _
        .DependsOn(nameof(Restore))
        .Executes(() => DotNet.Build(s => s.SetConfiguration(Configuration)));

    Target Pack => _ => _
        .DependsOn(nameof(Compile))
        .Executes(() => DotNet.Pack(s => s.SetOutput(Artifacts)));   // top-level by default — no marker needed

    Target Default => _ => _                     // 1.1.0+: explicit default target marker
        .Default()
        .DependsOn(nameof(Pack));
}

Five rules:

  1. The class derives from TampBuild.
  2. Main calls Execute<T>(args) — the framework's reflective entry.
  3. Targets are Target-typed properties whose value is a _ => _.X().Y().Z() lambda.
  4. [Parameter] properties get auto-bound from CLI args, environment variables, or their declared default. Member-name → flag-name mapping is PascalCasekebab-case (e.g. Configuration--configuration, MaxParallelism--max-parallelism). The CLI parser is case-sensitive — only the kebab-case form binds (--Registry won't match Registry). Override via [Parameter(Name = "...")]. Full rules: Parameter & Secret Injection — name → flag → env mapping.
  5. Computed paths and helpers are normal C# members. [Parameter] works on both properties and fields (including readonly fields — the canonical Tamp build script idiom is [Parameter] readonly string Configuration;).

That's the whole programming model. Everything below is the surface you can configure on each target.

Top-level is the default (1.1.0+)

Every target you declare is top-level by default — visible in --list, runnable by name, surfaced in IDE runner menus. Mark internal helpers (Restore, intermediate fan-outs, glue targets) with .Internal(). The old .TopLevel() marker is obsolete; it's a no-op kept for source compatibility and removed in 2.0.

Decorator Effect
(none) Default — target is top-level, surfaces in --list.
.Internal() Hide from default --list; still invokable by name and via --list --all.
.Default() Mark as the build's default target — runs when tamp is invoked with no args. Exactly one allowed per build class (otherwise the executor falls back to a target literally named Default, then Ci).

CleanArtifacts() helper (1.1.0+)

The bin/obj/Artifacts wipe used to be five lines of GlobDirectories boilerplate, and the build script's own output directory had to be excluded by hand or the running process self-evicted. CleanArtifacts() ships that pattern with the self-deletion guard built in — pass it an AbsolutePath to scope to a project, or call it with no args to clean every solution project.

Target Clean => _ => _.Executes(() => CleanArtifacts());                   // every solution project + Artifacts
Target Clean => _ => _.Executes(() => CleanArtifacts(RootDirectory / "src" / "MyLib"));  // single project

Two authoring styles for wrapper calls (1.2.0+)

Every wrapper verb exposes both a fluent configurer and an object-init overload. Both produce identical CommandPlans — pick the one you prefer.

// fluent — chains read top-to-bottom, easy to grep for "SetX"
DotNet.Build(s => s
    .SetProject(Solution.Path)
    .SetConfiguration(Configuration)
    .SetNoRestore(true));

// object-init — flatter, integrates with target-typed `new()` in C# 9+
DotNet.Build(new()
{
    Project = Solution.Path,
    Configuration = Configuration,
    NoRestore = true,
});

Fluent stays canonical in docs and the tamp init template; object-init is available across every first-party wrapper and any sub-facade. To scaffold a new build with the object-init shape, pass the flag at init time:

dotnet tamp init --settings-style=init

Fluent remains the default — pass --settings-style=fluent explicitly or omit the flag for the canonical shape.

The fluent surface

Each method returns ITargetDefinition for chaining.

Lifecycle / metadata

Method Purpose
Phase(Phase) Group target by lifecycle: Restore / Build / Test / Pack / Publish / Deploy / Custom.
Description(string) Shown in --list output.
Tag(params string[]) Free-form labels for grouping in summaries.
Internal() Hide from default --list and the runner's IDE menus. Target stays invokable by name and via --list --all. Replaces .TopLevel() — every target is top-level by default in 1.1.0+.
Default() Mark as the build's default target (runs when invoked with no args). Mutually exclusive with Internal().
TopLevel() Obsolete in 1.1.0+, removed in 2.0. No-op kept for source compatibility — top-level is the default.

Dependencies (four flavours)

Method Semantics
DependsOn(...names) Hard prerequisite: pulls the named targets into the plan and orders before this one.
After(...names) Order-only: if both targets happen to be in the plan, the named ones come first. Does not pull them in.
Before(...names) Mirror of After.
Triggers(...names) Outgoing fan-out: if this target runs, also pull in the named targets.
TriggeredBy(...names) Incoming fan-out. Equivalent to declaring Triggers(this) on each named target.

nameof(OtherTarget) is the recommended idiom for referring to other targets — type-safe and refactor-safe.

Cycles across DependsOn ∪ After ∪ Before ∪ TriggeredBy are detected at construction time and fail-fast with a path trace. OnFailureOf is intentionally excluded from cycle detection — it's a runtime conditional, not a planning edge.

Conditional execution

Method Behaviour on false
OnlyWhen(Func<bool>) Skip silently. The [CallerArgumentExpression] capture surfaces the predicate text in dry-run output and skip messages.
Requires(Func<bool>) Hard fail: aborts the build with the predicate text in the failure message.

Use OnlyWhen for "this target is irrelevant in this context" (e.g., Push only on main); use Requires for "this target can't run without X" (e.g., Deploy requires credentials set).

Failure handling

Method Purpose
FailureMode(Mode) Default Fatal (abort build). Continue (this target's failure shouldn't stop the build). Retry (retry per Backoff).
Retry(count, Backoff, ...exitCodes) Retry policy. Backoff factories: Backoff.Linear, Backoff.Exponential, Backoff.Constant, Backoff.Custom.
OnFailureOf(...names) Catch handler. This target runs only when one of the named targets fails. Distinct from FailureMode.Continue and from AssuredAfterFailure. See Failure Handling for the full decision matrix.
AssuredAfterFailure() Cleanup pattern. This target runs whether the build succeeded or failed, as long as it's in the plan. Failing assured cleanups don't reverse the original failure.

Capability preflight

Method Effect
RequiresNetwork() Fails fast if offline mode is set.
RequiresDocker() Fails fast if Docker daemon is unreachable.
RequiresAdmin() Fails fast if not elevated.
RequiresTool(name, minVersion?) Fails fast if the tool isn't on PATH (or below the minimum version).

Note: Capability preflight is recorded on the spec but the v0 executor doesn't yet enforce it. Expected to land in v0.x.

Resource consumption

Method Purpose
Consumes(Resource, ConsumeMode) Declarative resource usage. ConsumeMode.Shared (parallel-safe) or ConsumeMode.Exclusive (serializes).

Built-in resource kinds: Resource.BuildCache.Dotnet, Resource.BuildCache.Yarn, Resource.BuildCache.Nuget, Resource.Filesystem(path), Resource.Network.Internet, Resource.Network.Registry(host), Resource.Process.Docker. Modules can extend.

Note: Resource scheduling is recorded but not yet honoured by the v0 sequential executor. The declarative surface is in place for the eventual scheduler.

Scheduling hints

Method Purpose
Timeout(TimeSpan) Hard wall-clock kill at expiry.
ExpectedDuration(TimeSpan) Soft hint; powers "this is taking longer than usual" telemetry.
MemoryBudget(int megabytes) Expected peak RSS.
MemoryHardLimit(int megabytes) Optional ceiling enforced via cgroup if available.
MaxParallelism(int) Copies of this target in one build invocation.
MaxHostParallelism(int) Copies across the whole host.

Idempotency / caching

Method Purpose
Idempotent() Same inputs → same result; safe to skip if cached.
InputHash(Func<string>) Hash function over inputs (file globs, env vars, parameters).
Produces(globPattern) Declarative outputs.
RunMode(RunMode) Always (default), WhenInputsChanged, or Manual.

The work

Three overloads of Executes:

.Executes(() => { /* arbitrary code */ })
.Executes(() => DotNet.Build(...))                  // single CommandPlan
.Executes(() => new[] { DotNet.Restore(), DotNet.Build(...) })   // sequence of plans

Multiple Executes calls accumulate; they run in declaration order.

Default and Ci shortcuts

The executor resolves the default target in this order: explicit .Default() decorator → property literally named Default → property literally named Ci. A common shape:

Target Ci => _ => _.DependsOn(nameof(Pack));                  // top-level by default
Target Default => _ => _.Default().DependsOn(nameof(Compile)); // local-dev shortcut, explicit marker

Naming caveat: Ci is a property name on user build classes. Tamp's static TampBuild.CiHost (the typed CI vendor adapter) deliberately uses a different name to avoid collision.

Common idioms

Patterns that come up often enough to memorize but aren't load-bearing framework API — write inline, don't reach for a helper.

Image tag from SemVer + git sha

[GitRepository] readonly GitRepository Git = null!;
// + a Tool/Parameter source of GitVersion's SemVer per your project

string ImageTag => $"{GitVersion.SemVer}-{Git.Commit[..7]}";

Target DockerBuildBackend => _ => _
    .Executes(() => Docker.Build(s => s
        .SetContext(RootDirectory)
        .AddTag($"holdfast-backend:{ImageTag}")));

One line of string interpolation. Don't reach for a TampBuild.ImageTag(...) helper — different projects want different shapes (v{semver}+{sha}, {branch}-{sha}-{date}, etc.) and the inline form is already self-documenting. (HoldFast asked for a blessed helper during the 1.2.0 trial; we kept it as a doc pattern instead — recorded in this section for the record.)

Fan-out target

Target Ci => _ => _
    .Default()
    .DependsOn(Test, Publish, FrontendBuild, DockerBuildBackend);

The varargs DependsOn(Target t1, Target t2, params Target[] more) overload (1.3.0+) takes bare identifiers. Names are resolved by the framework via the same property reflection that registers the targets.

For 1 dep: the single-arg DependsOn(Restore) shape (CallerArgumentExpression captures the name). Both forms compose with chained .DependsOn(X).DependsOn(Y) if you prefer that style. All three produce identical specs.

Post-deploy smoke probe

using Tamp.Http;

Target SmokeQa => _ => _
    .DependsOn(DeployQa)
    .Executes(async () =>
        await HttpProbe.WaitForHealthy(
            url: "https://qa.example.com/health/live",
            timeout: TimeSpan.FromMinutes(2)));

HttpProbe.WaitForHealthy (in Tamp.Http 0.1.1+) polls a URL on a 2-second cadence until IsSuccessStatusCode or your custom predicate returns true. Treats connection errors and per-request timeouts as transient. Throws TimeoutException on budget exhaustion with the last observed status, attempt count, and last transport error in the message.

Override defaults via optional params: interval, headers (for auth'd health endpoints), isHealthy (async predicate for body-content checks like rejecting 200 OK with "status":"degraded"), HttpClient (for self-signed certs or shared client reuse), CancellationToken. See the Tamp.Http README for the parameterized example.

Multi-target invocation

tamp Test Pack         # both run; deduped union of dependency closures

Targets share dependencies — e.g., if both Test and Pack depend on Compile, Compile runs once.

Dry-run and plan modes

tamp Pack --dry-run    # print every CommandPlan that would run
tamp Pack --plan       # render the target dependency graph
tamp --list            # list top-level targets (or all if none marked)
tamp --list-tree       # list targets with their dependencies
tamp --list --all      # include internal targets too

Skipping targets (1.9.0+)

tamp Pack --skip StampVersion           # skip a single dep; dependents still run
tamp Pack --skip StampVersion --skip Lint   # repeat for multiple skips
tamp Pack --skip-deps                   # run only Pack's Executes; skip all upstream

Skipped targets land in the build summary as Skipped with the reason skipped by --skip or skipped by --skip-deps — visible distinct from OnlyWhen-driven skips. Their Executes block is a no-op; downstream targets that DependsOn them still run normally (treat the skip as "already satisfied").

Use cases:

  • Debug loopstamp Pack --skip-deps after a successful upstream run to re-test just the pack step.
  • Bypass a broken upstream tool — e.g., --skip StampVersion when cargo-edit isn't installed yet on a fresh machine.
  • Selective replay — combine --skip with dotnet tamp Ci to skip the long-running step you've already validated.

Typo protection: --skip XYZ where XYZ isn't a known target name errors with exit code 2 rather than silently no-op'ing.

Flags reference

Flag Purpose
--dry-run Print plans, execute nothing.
--plan Render target DAG, exit.
--list List top-level targets.
--list-tree List + show dependencies.
--all Used with --list* — include internal targets.
--skip <target> Treat the named target as already-satisfied. Repeatable. Dependents still run. (1.9.0+)
--skip-deps Treat every non-root target as skipped. Run only the explicitly-named target's Executes. (1.9.0+)
--format=<text|json> Output format for --list / --list-tree. JSON emits the full target + parameter catalog. (1.9.0+)
--reporter=<text|json> Build event reporter. json emits NDJSON (one event per line) to stdout, suppresses banner + ==> decorations. For IDE-extension consumption. (1.9.0+)
--verbosity <q|m|n|v|d> quiet / minimal / normal / verbose / diagnostic.
--quiet Shortcut for --verbosity quiet.
--verbose Shortcut for --verbosity verbose.
--diagnostic Shortcut for --verbosity diagnostic.

Anything else in --name value form is treated as a [Parameter] binding.

Where next

Clone this wiki locally