-
Notifications
You must be signed in to change notification settings - Fork 0
Parameter And Secret Injection
How the framework gets values into your build class — three attribute families, each with a different role.
Non-sensitive values that vary per environment. Auto-bound from CLI args, environment variables, and the property's declared default.
[Parameter("Build configuration")]
Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
[Parameter("Target environment", EnvironmentVariable = "DEPLOY_ENVIRONMENT")]
string Environment = "development";
[Parameter] int Verbosity { get; set; } = 1;-
Command-line argument wins. By default the property name maps to a kebab-case flag —
Configuration→--configuration <value>. Both space-separated (--configuration Release) and=(--configuration=Release) forms work. Bare flags (--quiet) are treated astrue. -
Environment variable if the CLI didn't supply one. Default mapping is UPPER_SNAKE_CASE —
Configuration→CONFIGURATION. Override viaEnvironmentVariable = "...". - Property default — whatever you assigned in the declaration.
ParameterBinder handles string, bool, enum (case-insensitive), nullable wrappers, and any IConvertible scalar (int, long, double, decimal, DateTime, etc.).
For bool:
| Input | Result |
|---|---|
true, True, 1
|
true |
false, False, 0
|
false |
| anything else | error |
[Parameter(Name = "env", EnvironmentVariable = "DEPLOY_ENV")]
string Environment = "dev";
// CLI: --env prod
// Env: DEPLOY_ENV=prodThe decorated member's type must be Secret, not string. The Secret class:
-
ToString()always returns<Secret:Name>— never the value. This is what string interpolation, structured loggers, and most debugger displays call by default, so accidental "leaks" to those surfaces appear as<Secret:Name>rather than cleartext. - The cleartext is reachable via
Reveal(). As of Tamp.Core 1.6.0 this method ispublic, gated by the TAMP004 Roslyn analyzer which flagsReveal()calls outside an approved context (classes ending in*Settings/*SettingsBase, Tamp framework internals, test code). - Every secret in a
CommandPlan.Secretslist is auto-registered with the runner's redaction table; matching values are scrubbed from log output, dry-run rendering, and child-process stdout/stderr passed through Tamp's writer.
Pre-1.6.0 history:
Reveal()wasinternal, gated by per-satellite[InternalsVisibleTo]entries inTamp.Core/AssemblyInfo.cs. Every new satellite handling a service-principal secret / cert password / API key required a Tamp.Core minor bump and a nuget propagation wait before it could ship. The IVT gate was never load-bearing for security — the real masking lives inToString()+CommandPlan.Secrets. 1.6.0 madeReveal()public + replaced the IVT gate with TAMP004; net-new satellites no longer need any Tamp.Core change.
[Secret("Container registry password", EnvironmentVariable = "REGISTRY_PASSWORD")]
Secret RegistryPassword;- CI vendor's secret store (when running in a recognised CI and the vendor exposes secrets via env vars — they typically do)
- Local secret store (DPAPI on Windows, libsecret on Linux, Keychain on macOS) — planned, not yet implemented
- Environment variable
- Interactive prompt if attached to a TTY — planned, not yet implemented
Status: Current Tamp implements step 3 (env-var resolution). Steps 1, 2, 4 are scheduled. CI vendors that expose secrets via env vars (Azure DevOps, GitHub Actions, TeamCity) are already covered transparently by step 3.
Wrapper APIs that take secrets accept the typed Secret parameter directly:
Docker.Login(s => s
.SetServer("registry.example.com")
.SetUsername("ci")
.SetPassword(RegistryPassword)); // Secret, not stringWrappers that accept secrets should prefer stdin or file-based passing where the underlying tool supports it (docker login --password-stdin, gh auth login --with-token, etc.) — keeps the secret out of the OS process table. CommandPlan.StandardInput is the framework primitive for that.
Auto-compute a value from the build context and inject it into a property. Distinct from [Parameter] — these don't read from CLI/env, they construct from the repository state.
Loads the .slnx or .sln discovered at the repository root.
[Solution] readonly Solution Solution;
// Now you can read:
Solution.Path // AbsolutePath to the solution file
Solution.Name // Solution display name
Solution.Projects // IReadOnlyList<SolutionProject>
Solution.GetProject("MyApp")?.Path // case-insensitive lookupOverride the discovery with an explicit path:
[Solution(Path = "src/MySpecific.slnx")] readonly Solution Sln;Reads .git/HEAD, .git/refs/heads/*, and .git/config directly — no shell-out to git. Works on machines without git installed, as long as the working tree was cloned.
[GitRepository] readonly GitRepository Git;
Git.Branch // current branch name, null on detached HEAD
Git.Commit // 40-char SHA
Git.RemoteUrl // origin URL
Git.IsDetachedHead
Git.Root // working-tree root (AbsolutePath)Auto-resolves a .NET tool from NuGet. Decorated property must be Tool.
[NuGetPackage("dotnet-sonarscanner", Version = "9.0.0")] readonly Tool SonarScanner;
// later:
Target SonarBegin => _ => _.Executes(() =>
ProcessRunner.Execute(SonarScanner.Plan("begin", "/k:my-project")));First use installs into a Tamp-managed cache at TampBuild.TemporaryDirectory / "nuget-tools" / "{packageId}-{version}". Subsequent runs reuse the cache.
Three escape hatches for special cases:
| Property | When to use |
|---|---|
Version = "..." |
Pin the version. Default = latest stable (one network round-trip on first use). |
ExecutableName = "..." |
The installed binary name differs from the package id. |
LocalCachePath = "..." |
Skip install entirely; resolve from a pre-staged directory. For locked-down environments that can't reach nuget.org. |
UseSystemPath = true |
Legacy. Resolves against the OS PATH. Prefer [FromPath] (below) — it has the same intent with better naming and full Windows extension probing (.cmd / .exe / .bat / .ps1, not just .exe). |
Resolves a native executable from the OS PATH. The canonical way to inject non-NuGet tools (Docker, yarn, git, docker-compose, etc.).
[FromPath("docker")] readonly Tool Docker = null!;
[FromPath("yarn")] readonly Tool Yarn = null!;
[FromPath("gh", Optional = true)] readonly Tool? Gh = null;| Property | Default | Behavior |
|---|---|---|
Optional = false |
throws at injection time | Injects null when the executable can't be found; consumer can Requires(() => tool != null) or branch on it. |
On Windows, the resolver probes extensions in order: .cmd, .exe, .bat, .ps1, then the bare name. On POSIX, only the bare name. PATH-order is preserved (first hit wins). For inline use without an attribute: Tool.FromPath("yarn") (throws) or Tool.TryFromPath("yarn") (returns null).
Resolves a tool installed under <projectRoot>/node_modules/.bin/<name> — the standard place workspace-local Node tools live (turbo, vite, vitest, tsc, eslint, etc.).
[FromNodeModules("turbo")] readonly Tool Turbo = null!;
[FromNodeModules("vite")] readonly Tool Vite = null!;
[FromNodeModules("vitest", Optional = true)] readonly Tool? Vitest = null;| Property | Default | Behavior |
|---|---|---|
ProjectRoot |
TampBuild.RootDirectory |
Override for nested workspaces (e.g. ProjectRoot = "frontend"). Relative paths resolve against RootDirectory; absolute paths win as-is. |
Optional |
false |
Same semantics as [FromPath]. |
Important: the binary doesn't exist until yarn install (or npm install) runs. Targets that consume the injected Tool must DependsOn(nameof(YarnInstall)):
Target FrontendBuild => _ => _
.DependsOn(nameof(YarnInstall))
.Executes(() => Turbo.Run(Turbo, s => s.AddTask("build")));On Windows, the resolver probes .cmd (the standard yarn-installed shim) before .exe. For inline use: Tool.FromNodeModules("turbo", projectRoot) / Tool.TryFromNodeModules(...). The "not found" error message includes a yarn install hint.
Subclass ValueInjectionAttribute and override GetValue. The framework discovers it automatically — same code path as [Solution] and [GitRepository].
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class GitVersionAttribute : ValueInjectionAttribute
{
public override object? GetValue(MemberInfo member, Type memberType)
{
// … resolve and return your value …
}
}This is the extensibility point for ecosystem authors who want to add [NpmPackage], [LatestNuGetVersion], [GitVersion], [Pulumi], etc. — drop a new package into the consumer's Build.csproj and the attribute Just Works.
ParameterBinder runs once at the start of Execute<T>(args), in this order:
- ValueInjection attributes (Solution, GitRepository, NuGetPackage, FromPath, FromNodeModules, custom)
-
[Parameter]bindings
Targets are then collected, the graph is built, and the executor takes over. Properties are stable for the lifetime of the build invocation.
[Secret] resolution is intentionally lazy — secrets resolve only when a target that requires them is about to run, so unrelated targets don't fail because an unrelated secret isn't available. (v0 currently resolves all secrets at bind time too; the lazy variant lands when interactive prompting + local stores arrive.)
-
Failure Handling —
Requires(hard precondition) vsOnlyWhen(silent skip) -
Tooling Primitives —
AbsolutePath,Verify - Build Script Authoring — full target DSL surface
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