Atom is an opinionated, type-safe build automation framework for .NET. It enables you to define your build logic in C#, debug it like standard code, and automatically generate CI/CD configuration files for GitHub Actions and Azure DevOps.
Write build logic in C# alongside your application code.
Step through your build process using your IDE.
Define logic once; Atom generates the YAML for GitHub and Azure DevOps.
Pull in capabilities via NuGet packages (GitVersion, Azure KeyVault, etc.).
Reduces boilerplate by automatically discovering targets and parameters.
Note
It is recommended to use the atom dotnet tool to invoke atom projects:
dotnet tool install -g Invex.Atom.Tool
atom ...
However, the dotnet cli can also be used directly:
dotnet run -- ...
-
Create a .NET 10 project
dotnet new console -n _atom -
Update the
_atom.csprojfile:<Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <RootNamespace>Atom</RootNamespace> </PropertyGroup> <ItemGroup> <PackageReference Include="Invex.Atom.Build" Version="3.*" /> </ItemGroup> </Project>
-
Replace the
Program.csfile withIBuild.cs:using Invex.Atom.Build.Definition; using Invex.Atom.Build.Hosting; namespace Atom; [BuildDefinition] [GenerateEntryPoint] internal interface IBuild : IBuildDefinition { Target HelloWorld => t => t .DescribedAs("Prints a hello world message to the console") .Executes(() => Logger.LogInformation("Hello, World!")); }
-
Execute
atom HelloWorld26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: 01:16:47.552 INF Executing build HelloWorld Prints a hello world message to the console 26-06-06 +10:00 HelloWorld | Atom.Build: 01:16:47.616 INF Hello, World! Build Summary HelloWorld │ Succeeded │ <0.01s
-
Add a parameter to the
IBuildinterface:[!NOTE]
.RequiresParam(nameof(...))is used to ensure the parameter is provided before the target is executed..UsesParam(nameof(...))is used to use the parameter if it is provided, but not fail the build if it is not.using Invex.Atom.Build.Definition; using Invex.Atom.Build.Hosting; using Invex.Atom.Build.Params; namespace Atom; [BuildDefinition] [GenerateEntryPoint] internal interface IBuild : IBuildDefinition { [ParamDefinition("my-name", "My name")] string? MyName => GetParam(() => MyName); Target HelloWorld => t => t .DescribedAs("Prints a hello world message to the console") .RequiresParam(nameof(MyName)) .Executes(() => Logger.LogInformation("Hello, World! I am {MyName}.", MyName)); }
-
Execute `atom HelloWorld --my-name Frodo
26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: 01:23:25.405 INF Executing build HelloWorld Prints a hello world message to the console 26-06-06 +10:00 HelloWorld | Atom.Build: 01:23:25.467 INF Hello, World! I am Frodo. Build Summary HelloWorld │ Succeeded │ <0.01s
-
Add a secret parameter to the
IBuildinterface:using Invex.Atom.Build.Definition; using Invex.Atom.Build.Hosting; using Invex.Atom.Build.Params; namespace Atom; [BuildDefinition] [GenerateEntryPoint] internal interface IBuild : IBuildDefinition { [ParamDefinition("my-name", "My name")] string? MyName => GetParam(() => MyName); [SecretDefinition("my-secret", "My secret")] string? MySecret => GetParam(() => MySecret); Target HelloWorld => t => t .DescribedAs("Prints a hello world message to the console") .RequiresParam(nameof(MyName)) .RequiresParam(nameof(MySecret)) .Executes(() => Logger.LogInformation("Hello, World! I am {MyName} and my secret is {MySecret}.", MyName, MySecret)); }
-
Execute
atom HelloWorld --my-name Frodo --my-secret TheOneRing[!NOTE] The secret is masked in the output.
26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: 01:27:08.482 INF Executing build HelloWorld Prints a hello world message to the console 26-06-06 +10:00 HelloWorld | Atom.Build: 01:27:08.544 INF Hello, World! I am Frodo and my secret is *****. Build Summary HelloWorld │ Succeeded │ <0.01s
-
Update the
IBuildinterface:using Invex.Atom.Build.Definition; using Invex.Atom.Build.Hosting; using Invex.Atom.Build.Params; namespace Atom; [BuildDefinition] [GenerateEntryPoint] internal interface IBuild : IBuildDefinition { [ParamDefinition("my-name", "My name")] string? MyName => GetParam(() => MyName); [SecretDefinition("my-secret", "My secret")] string? MySecret => GetParam(() => MySecret); Target HelloWorld => t => t .DescribedAs("Prints a hello world message to the console") .RequiresParam(nameof(MyName)) .RequiresParam(nameof(MySecret)) .Executes(() => Logger.LogInformation("Hello, World! I am {MyName} and my secret is {MySecret}.", MyName, MySecret)); Target Goodbye => t => t .DescribedAs("Prints a goodbye message to the console") .DependsOn(nameof(HelloWorld)) .Executes(() => Logger.LogInformation("Goodbye!")); }
-
Execute
atom Goodbye --my-name Frodo --my-secret TheOneRing[!NOTE] The
Goodbyetarget depends on theHelloWorldtarget, so both will be executed in the correct order, and the parameters only need to be provided once.26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: 01:30:12.175 INF Executing build HelloWorld Prints a hello world message to the console 26-06-06 +10:00 HelloWorld | Atom.Build: 01:30:12.235 INF Hello, World! I am Frodo and my secret is *****. Goodbye Prints a goodbye message to the console 26-06-06 +10:00 Goodbye | Atom.Build: 01:30:12.240 INF Goodbye! Build Summary HelloWorld │ Succeeded │ <0.01s Goodbye │ Succeeded │ <0.01s
-
Update the
_atom.csprojfile:<Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <RootNamespace>Atom</RootNamespace> </PropertyGroup> <ItemGroup> <PackageReference Include="Invex.Atom.Module.GithubWorkflows" Version="3.*" /> </ItemGroup> </Project>
-
Update the
IBuildinterface:using Invex.Atom.Build.BuildOptions; using Invex.Atom.Build.Definition; using Invex.Atom.Build.Hosting; using Invex.Atom.Build.Params; using Invex.Atom.Module.GithubWorkflows.Extensions; using Invex.Atom.Module.GithubWorkflows.Helpers; using Invex.Atom.Workflows; using Invex.Atom.Workflows.Definition; using Invex.Atom.Workflows.Definition.Triggers; using Invex.Atom.Workflows.Options; using Invex.StructuredText.Expressions; namespace Atom; [BuildDefinition] [GenerateEntryPoint] internal interface IBuild : IWorkflowBuildDefinition, IGithubWorkflows { [ParamDefinition("my-name", "My name")] string? MyName => GetParam(() => MyName); [SecretDefinition("my-secret", "My secret")] string? MySecret => GetParam(() => MySecret); Target HelloWorld => t => t .DescribedAs("Prints a hello world message to the console") .RequiresParam(nameof(MyName)) .RequiresParam(nameof(MySecret)) .Executes(() => Logger.LogInformation("Hello, World! I am {MyName} and my secret is {MySecret}.", MyName, MySecret)); Target Goodbye => t => t .DescribedAs("Prints a goodbye message to the console") .DependsOn(nameof(HelloWorld)) .Executes(() => Logger.LogInformation("Goodbye!")); IReadOnlyList<WorkflowDefinition> IWorkflowBuildDefinition.Workflows => [ new("Hello") { Triggers = [WorkflowTriggers.PushToMain], Targets = [ new(nameof(HelloWorld)) { Options = [ BuildOptions.Inject.Param(nameof(MyName), TextExpressions.Github.GithubRepositoryOwner), BuildOptions.Inject.Secret(nameof(MySecret)), ], }, new(nameof(Goodbye)), ], Types = [WorkflowTypes.Github.Action], }, ]; }
-
Execute
atom gen26-06-06 +10:00 Invex.Atom.Module.GithubWorkflows.GithubActions.GithubWorkflowFileWriter: 01:38:57.388 INF Writing new workflow file: L:\Repos\Invex-Games\atom\.github\workflows\Hello.yml 26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: 01:38:57.472 INF Executing build Gen Generates workflow files Build Summary Gen │ Succeeded │ <0.01s.github/workflows/Hello.ymlname: Hello on: push: branches: [ main ] permissions: { } jobs: HelloWorld: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: HelloWorld id: HelloWorld run: dotnet run --project _atom/_atom.csproj -- HelloWorld --skip --headless env: my-secret: ${{ secrets.MY_SECRET }} my-name: ${{ github.repository_owner }} Goodbye: needs: [ HelloWorld ] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Goodbye id: Goodbye run: dotnet run --project _atom/_atom.csproj -- Goodbye --skip --headless
Atom can also be used as a file-based app:
Atom.cs
#:sdk Microsoft.NET.Sdk.Worker
#:package Invex.Atom.Build@3.*
using Invex.Atom.Build.Definition;
using Invex.Atom.Build.Hosting;
namespace Atom;
[BuildDefinition]
[GenerateEntryPoint]
internal interface IBuild : IBuildDefinition
{
Target HelloWorld =>
t => t
.DescribedAs("Prints a hello world message to the console")
.Executes(() => Logger.LogInformation("Hello, World!"));
}- Introduction — What Atom is and why you'd use it
- Your First Build — Create a minimal build and run it
- Base vs Workflow Build —
BuildDefinitionvsWorkflowBuildDefinition
- Build Definitions — The
[BuildDefinition]attribute and source generators - Targets — Defining targets with the fluent
TargetDefinitionAPI - Parameters — Declaring, requiring, and resolving parameters
- Secrets — Secure parameter handling with
ISecretsProvider - Artifacts — Producing and consuming build artifacts
- Variables — Sharing data between targets with workflow variables
- File System —
IRootedFileSystem,RootedPath, and path providers - Process Runner — Executing external processes
- Build Info — Build ID, version, and timestamp providers
- Build Options — Configuring build behaviour with
IBuildOption - Hosting —
AtomHost,[GenerateEntryPoint], and host configuration - Lifecycle Hooks —
IAtomLifecycleHookfor pre/post-build logic - Logging & Reports — Spectre console output and report data
- File Transformations — Temporary, reversible file edits with
TransformFileScope
- Overview — What workflows add on top of a base build
- Workflow Definitions —
WorkflowDefinition, targets, and types - Triggers — Push, pull request, and manual triggers
- Workflow Options — Checkout, deployment, conditions, and more
- Variables in Workflows — Cross-job data sharing
- Debugging Workflows — Local workflow simulation
- Overview — What a module is and how to add one
- .NET —
Invex.Atom.Module.Dotnet - GitHub Workflows —
Invex.Atom.Module.GithubWorkflows - DevOps Workflows —
Invex.Atom.Module.DevopsWorkflows - Azure Key Vault —
Invex.Atom.Module.AzureKeyVault - Azure Storage —
Invex.Atom.Module.AzureStorage - GitVersion —
Invex.Atom.Module.GitVersion
- SetupBuildInfo — Initialises build ID, version, and timestamp
- ValidateBuild — Checks the build for common issues
- GenerateWorkflowFiles — Generates CI/CD YAML files
- Writing a Module — Creating a reusable Atom module NuGet package
- Custom Providers — Implementing
IArtifactProvider,ISecretsProvider, and more - Source Generators — How the Atom analysers and source generators work
- Testing — Using
Invex.Atom.TestUtils
- CLI — Command-line arguments and the Atom global tool
- API Reference — Auto-generated API documentation (via DocFX)
The Atom libraries are human-made, however GitHub Copilot completions have been used on a superficial level.
Generative AI was also used to assist in writing tests and documentation.
Atom is released under the MIT License.