Run conditions
A run condition is an IRunCondition: one asynchronous predicate that receives the pipeline context and
a cancellation token.
Run conditions may be evaluated both during execution and by PlanAsync() or --dry-run.
Keep them side-effect-free: they must not mutate external state or rely on being evaluated
exactly once.
Fluent conditions that await module results remain unresolved during planning because
planning never executes dependencies to produce those results. The plan reports an unknown skip
decision unless another condition determines the outcome.
public class ServiceIsAvailable : IRunCondition
{
public async Task<bool> EvaluateAsync(IPipelineContext context, CancellationToken cancellationToken)
{
using var response = await context.Network.Http.SendAsync(
new HttpOptions(new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/ping")),
cancellationToken);
return response.StatusCode == HttpStatusCode.OK;
}
}
Pass the token to asynchronous work: it is canceled when the pipeline stops evaluating conditions.
Applying conditions
Apply the condition with an attribute that states its intent:
[RunIf<ServiceIsAvailable>]
public class DeployModule : Module
[RunIf<T1, ..., T4>]runs only when all of its one to four conditions aretrue.[RunIfAny<T1, T2, ..., T4>]runs when at least one condition istrue.[SkipIf<T1, ..., T4>]skips when any condition istrue.
Or register the condition in Configure, with a type or an instance:
protected override void Configure(ModuleConfigurationBuilder module) => module
.WithRunIf<ServiceIsAvailable>()
.WithSkipIf(new IsDependabot());
WithRunIf and WithSkipIf compose with .WithSkipWhen(...) using OR-to-skip semantics: the module is
skipped when any configured condition says so.
Reusable groups
Combine conditions once with ConditionGroup. Logic is ConditionLogic.All (AND) or ConditionLogic.Any (OR):
public sealed class OnUnixPlatforms : ConditionGroup, IPlanningSafe
{
public override IReadOnlyList<IRunCondition> Conditions => [new OnLinux(), new OnMacOS()];
public override ConditionLogic Logic => ConditionLogic.Any;
}
Stateful condition attributes
When a condition needs constructor state, derive an attribute from RunConditionAttribute. The base
constructor takes the intent: ConditionIntent.Run runs the module only when the condition is satisfied,
and ConditionIntent.Skip skips it when the condition is satisfied.
public sealed class RunIfRegionAttribute(string region)
: RunConditionAttribute(ConditionIntent.Run), IPlanningSafe
{
public override string ConditionNames => $"RunIfRegion({region})";
public override Task<bool> EvaluateAsync(IPipelineContext context, CancellationToken cancellationToken) =>
Task.FromResult(context.Environment.Variables.Get("REGION") == region);
}
[RunIfRegion("eu-west-2")]
public class RegionalDeployModule : Module
Each run attribute is a requirement of its own. To make several run attributes alternatives, give them the
same GroupKey: the module runs when any attribute in the group is satisfied, and every group must still hold.
public sealed class RunOnBranchAttribute(string branch) : RunConditionAttribute(ConditionIntent.Run)
{
public override Type? GroupKey => typeof(RunOnBranchAttribute);
public override async Task<bool> EvaluateAsync(IPipelineContext context, CancellationToken cancellationToken)
{
var info = await context.Tools.Git.Information.GetInfoAsync(cancellationToken);
return info?.BranchName == branch;
}
}
// Runs on main or on release.
[RunOnBranch("main")]
[RunOnBranch("release")]
public class ReleaseModule : Module
The group key is ignored for skip attributes, because any satisfied skip attribute already skips the module.
Evaluation order
Skip attributes are evaluated first, then required run attributes, then alternative run attributes
(RunIfAny and grouped attributes). Attribute conditions and fluent conditions run in the same
execution pipeline after dependency waiting. Both invoke skipped hooks and lifecycle notifications.
Fluent dependencies are validated before execution conditions are evaluated. Every dependency
declared with DependsOn<T>() must therefore be registered, even when an attribute condition
will skip the consuming module on the current platform or environment.
Planning-safe conditions
PlanAsync(), --dry-run, and dependency-graph export evaluate typed conditions registered through
attributes, WithRunIf, or WithSkipIf only when they are marked with IPlanningSafe. Implement the
marker on conditions, condition attributes, and condition groups that are free of side effects,
blocking work, and remote I/O. Unmarked typed conditions stay unresolved until execution. The same
marker applies to dependency selectors derived from DependsOnBaseAttribute and to module registration
handlers.
Fluent delegates passed to WithSkipWhen or WithSkipWhenAll are different: planning can invoke them
without an IPlanningSafe marker, including asynchronous delegates. Do not put execution-only HTTP,
Git, or other remote checks in these delegates; use an unmarked IRunCondition with WithRunIf or
WithSkipIf instead. A dry-run does not prevent I/O performed by a delegate.
Not every delegate runs on every plan. Module selection and earlier skip decisions can bypass it, and composed conditions short-circuit. If a delegate accesses an unavailable module result, that condition remains unresolved; another condition can still determine the skip decision. Work performed before that access can still run. Keep delegates side-effect-free and safe to evaluate more than once.
Built-in conditions
Built-in conditions include OnCI, OnLocal, OnLinux, OnWindows, OnMacOS, OnFreeBSD, and OnUnix.
OnCI and OnLocal use context.Environment.BuildSystem.IsCI: a detected build system
(GitHub Actions, Azure Pipelines, TeamCity, and others) counts as CI, and otherwise a CI
environment variable set to anything other than false or 0 does. When only the CI variable makes
the run count as CI, the pipeline logs this once at startup.
All built-in conditions are planning-safe:
[RunIf<OnLinux>]
public class LinuxModule : Module
Parameterized built-ins cover environment variables. Combine platform conditions when a module can run on alternative operating systems:
[RunIfEnvironmentVariable("NUGET_API_KEY")]
[SkipIfEnvironmentVariable("CI", "true")]
[RunIfAny<OnLinux, OnMacOS>]
public class PublishModule : Module
Use RunIfEnvironmentVariableUnset or SkipIfEnvironmentVariableUnset for the inverse
environment-variable check. The ModularPipelines.Git package also provides
RunIfBranch, RunIfBranchStartsWith, RunIfChanged, and SkipIfBranch; these stateful attributes
derive from RunConditionAttribute, and RunIfBranch and RunIfBranchStartsWith share a group key.
One-off conditions can use Configure(ModuleConfigurationBuilder).WithSkipWhen(...).