diff --git a/ADotNet/ADotNet.csproj b/ADotNet/ADotNet.csproj
index 254ea25..4fd789c 100644
--- a/ADotNet/ADotNet.csproj
+++ b/ADotNet/ADotNet.csproj
@@ -86,4 +86,8 @@
+
+
+
+
diff --git a/ADotNet/Clients/ADotNetClient.cs b/ADotNet/Clients/ADotNetClient.cs
index a19fb24..8692500 100644
--- a/ADotNet/Clients/ADotNetClient.cs
+++ b/ADotNet/Clients/ADotNetClient.cs
@@ -10,7 +10,7 @@
namespace ADotNet.Clients
{
- public class ADotNetClient
+ public class ADotNetClient : IADotNetClient
{
private readonly IBuildService buildService;
diff --git a/ADotNet/Clients/Builders/GitHubPipelineBuilder.cs b/ADotNet/Clients/Builders/GitHubPipelineBuilder.cs
new file mode 100644
index 0000000..0d6d99c
--- /dev/null
+++ b/ADotNet/Clients/Builders/GitHubPipelineBuilder.cs
@@ -0,0 +1,108 @@
+// ---------------------------------------------------------------------------
+// Copyright (c) Hassan Habib & Shri Humrudha Jagathisun All rights reserved.
+// Licensed under the MIT License.
+// See License.txt in the project root for license information.
+// ---------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets;
+
+namespace ADotNet.Clients.Builders
+{
+ ///
+ /// Builder for creating a GitHub pipeline.
+ ///
+ public class GitHubPipelineBuilder
+ {
+ private readonly GithubPipeline githubPipeline;
+ private readonly IADotNetClient aDotNetClient;
+
+ internal GitHubPipelineBuilder(IADotNetClient aDotNetClient)
+ {
+ this.githubPipeline = new GithubPipeline
+ {
+ OnEvents = new Events(),
+ Jobs = new Dictionary()
+ };
+
+ this.aDotNetClient = aDotNetClient;
+ }
+
+ ///
+ /// Creates a new instance of the class
+ /// with a default .
+ ///
+ /// A new instance of .
+ public static GitHubPipelineBuilder CreateNewPipeline()
+ {
+ var aDotNetClient = new ADotNetClient();
+
+ return new GitHubPipelineBuilder(aDotNetClient);
+ }
+
+ ///
+ /// Sets the name of the GitHub pipeline.
+ ///
+ /// The name of the pipeline.
+ /// The current instance of .
+ public GitHubPipelineBuilder SetName(string name)
+ {
+ this.githubPipeline.Name = name;
+
+ return this;
+ }
+
+ ///
+ /// Configures the pipeline to trigger on push events for specified branches.
+ ///
+ /// The branches to trigger on push events.
+ /// The current instance of .
+ public GitHubPipelineBuilder OnPush(params string[] branches)
+ {
+ this.githubPipeline.OnEvents.Push = new PushEvent
+ {
+ Branches = branches
+ };
+
+ return this;
+ }
+
+ ///
+ /// Configures the pipeline to trigger on pull request events for specified branches.
+ ///
+ /// The branches to trigger on pull request events.
+ /// The current instance of .
+ public GitHubPipelineBuilder OnPullRequest(params string[] branches)
+ {
+ this.githubPipeline.OnEvents.PullRequest = new PullRequestEvent
+ {
+ Branches = branches
+ };
+
+ return this;
+ }
+
+ ///
+ /// Adds a job to the GitHub pipeline.
+ ///
+ /// The unique identifier for the job.
+ /// The action to configure the job.
+ /// The current instance of .
+ public GitHubPipelineBuilder AddJob(string jobIdentifier, Action configureJob)
+ {
+ var jobBuilder = new JobBuilder();
+ configureJob(jobBuilder);
+ this.githubPipeline.Jobs[jobIdentifier] = jobBuilder.Build();
+
+ return this;
+ }
+
+ ///
+ /// Saves the configured pipeline (yml) to the specified file path.
+ ///
+ /// The file path where the pipeline will be saved.
+ public void SaveToFile(string path) =>
+ this.aDotNetClient.SerializeAndWriteToFile(this.githubPipeline, path);
+ }
+}
diff --git a/ADotNet/Clients/Builders/JobBuilder.cs b/ADotNet/Clients/Builders/JobBuilder.cs
new file mode 100644
index 0000000..8584e0e
--- /dev/null
+++ b/ADotNet/Clients/Builders/JobBuilder.cs
@@ -0,0 +1,188 @@
+// ---------------------------------------------------------------------------
+// Copyright (c) Hassan Habib & Shri Humrudha Jagathisun All rights reserved.
+// Licensed under the MIT License.
+// See License.txt in the project root for license information.
+// ---------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks.SetupDotNetTaskV1s;
+
+namespace ADotNet.Clients.Builders
+{
+ ///
+ /// A builder to create a job for a GitHub Actions workflow.
+ ///
+ public class JobBuilder
+ {
+ private readonly Job job;
+
+ internal JobBuilder()
+ {
+ this.job = new Job
+ {
+ Steps = new List(),
+ EnvironmentVariables = null
+ };
+ }
+
+ ///
+ /// Sets the name of the job.
+ ///
+ /// The name of the job.
+ /// The current instance of .
+ public JobBuilder WithName(string name)
+ {
+ this.job.Name = name;
+
+ return this;
+ }
+
+ ///
+ /// Specifies the machine on which the job will run.
+ ///
+ /// The machine or environment to run the job on.
+ /// The current instance of .
+ public JobBuilder RunsOn(string machine)
+ {
+ this.job.RunsOn = machine;
+
+ return this;
+ }
+
+ ///
+ /// Adds an environment variable to the job.
+ ///
+ /// The key of the environment variable.
+ /// The value of the environment variable.
+ /// The current instance of .
+ public JobBuilder AddEnvironmentVariable(string key, string value)
+ {
+ this.job.EnvironmentVariables ??= new Dictionary();
+
+ this.job.EnvironmentVariables[key] = value;
+
+ return this;
+ }
+
+ ///
+ /// Adds multiple environment variables to the job.
+ ///
+ /// A dictionary of environment variables to add.
+ /// The current instance of .
+ public JobBuilder AddEnvironmentVariables(Dictionary variables)
+ {
+ this.job.EnvironmentVariables ??= new Dictionary();
+
+ foreach (var variable in variables)
+ {
+ this.job.EnvironmentVariables[variable.Key] = variable.Value;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Adds a checkout step to the job.
+ ///
+ /// The name of the checkout step (default: "Check out").
+ /// The current instance of .
+ public JobBuilder AddCheckoutStep(string name = "Check out")
+ {
+ this.job.Steps.Add(new CheckoutTaskV2 { Name = name });
+
+ return this;
+ }
+
+ ///
+ /// Adds a setup step for a specific .NET version to the job.
+ ///
+ /// The version of .NET to set up.
+ /// The name of the setup step (default: "Setup Dot Net Version").
+ /// Specifies whether to include prerelease versions.
+ /// The current instance of .
+ public JobBuilder AddSetupDotNetStep(
+ string version,
+ string stepName = "Setup Dot Net Version",
+ bool includePrerelease = false)
+ {
+ this.job.Steps.Add(new SetupDotNetTaskV1
+ {
+ Name = stepName,
+ TargetDotNetVersion = new TargetDotNetVersion
+ {
+ DotNetVersion = version,
+ IncludePrerelease = includePrerelease
+ }
+ });
+
+ return this;
+ }
+
+ ///
+ /// Adds a restore step to the job.
+ ///
+ /// The name of the restore step (default: "Restore").
+ /// The current instance of .
+ public JobBuilder AddRestoreStep(string name = "Restore")
+ {
+ this.job.Steps.Add(new RestoreTask { Name = name });
+
+ return this;
+ }
+
+ ///
+ /// Adds a build step to the job.
+ ///
+ /// The name of the build step (default: "Build").
+ /// The current instance of .
+ public JobBuilder AddBuildStep(string name = "Build")
+ {
+ this.job.Steps.Add(new DotNetBuildTask { Name = name });
+
+ return this;
+ }
+
+ ///
+ /// Adds a test step to the job.
+ ///
+ /// The name of the test step (default: "Test").
+ /// The command to execute the test
+ /// (default: "dotnet test --no-build --verbosity normal").
+ /// The current instance of .
+ public JobBuilder AddTestStep(string name = "Test", string command = null)
+ {
+ this.job.Steps.Add(new TestTask
+ {
+ Name = name,
+ Run = command ?? "dotnet test --no-build --verbosity normal"
+ });
+
+ return this;
+ }
+
+ ///
+ /// Adds a generic step to the job with a custom command.
+ ///
+ /// The name of the step.
+ /// The command to execute for this step.
+ /// The current instance of .
+ public JobBuilder AddGenericStep(string name, string runCommand)
+ {
+ this.job.Steps.Add(new GithubTask
+ {
+ Name = name,
+ Run = runCommand
+ });
+
+ return this;
+ }
+
+ ///
+ /// Builds and returns the configured job.
+ ///
+ /// The configured instance.
+ public Job Build() => this.job;
+ }
+}
\ No newline at end of file
diff --git a/AdoNet.Tests.Console/Program.cs b/AdoNet.Tests.Console/Program.cs
index 06d673f..c581dc1 100644
--- a/AdoNet.Tests.Console/Program.cs
+++ b/AdoNet.Tests.Console/Program.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using ADotNet.Clients;
+using ADotNet.Clients.Builders;
using ADotNet.Models.Pipelines.AdoPipelines.AspNets;
using ADotNet.Models.Pipelines.AdoPipelines.AspNets.Tasks.DotNetExecutionTasks;
using ADotNet.Models.Pipelines.AdoPipelines.AspNets.Tasks.PublishBuildArtifactTasks;
@@ -185,6 +186,42 @@ static void Main(string[] args)
};
adoClient.SerializeAndWriteToFile(githubPipeline, "github-pipelines.yaml");
+
+ string projectName = "OtripleS.Api.Infrastructure.Provision";
+
+ GitHubPipelineBuilder.CreateNewPipeline()
+ .SetName("Github")
+ .OnPush("master")
+ .OnPullRequest("master")
+
+ .AddJob("build", job => job
+ .WithName("Build")
+ .RunsOn(BuildMachines.WindowsLatest)
+ .AddEnvironmentVariable("AzureClientId", "${{ secrets.AZURECLIENTID }}")
+
+ .AddEnvironmentVariables(new Dictionary
+ {
+ { "AzureTenantId", "${{ secrets.AZURETENANTID }}" },
+ { "AzureClientSecret", "${{ secrets.AZURECLIENTSECRET }}" },
+ { "AzureAdminName", "${{ secrets.AZUREADMINNAME }}" },
+ { "AzureAdminAccess", "${{ secrets.AZUREADMINACCESS }}" }
+ })
+
+ .AddCheckoutStep("Check Out")
+
+ .AddSetupDotNetStep(
+ version: "6.0.101",
+ includePrerelease: true)
+
+ .AddRestoreStep()
+ .AddBuildStep()
+
+ .AddGenericStep(
+ name: "Provision",
+ runCommand:
+ "dotnet run --project .\\{projectName}\\{projectName}.csproj"))
+
+ .SaveToFile("github-pipelines-fluent.yaml");
}
}
}
diff --git a/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.Logic.cs b/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.Logic.cs
new file mode 100644
index 0000000..65cd9bb
--- /dev/null
+++ b/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.Logic.cs
@@ -0,0 +1,144 @@
+// ---------------------------------------------------------------------------
+// Copyright (c) Hassan Habib & Shri Humrudha Jagathisun All rights reserved.
+// Licensed under the MIT License.
+// See License.txt in the project root for license information.
+// ---------------------------------------------------------------------------
+
+using ADotNet.Clients.Builders;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks;
+using FluentAssertions;
+using Moq;
+using Xunit;
+
+namespace ADotNet.Tests.Unit.Clients.Builders
+{
+ public partial class GitHubPipelineBuilderTests
+ {
+ [Fact]
+ public void ShouldCreateNewPipeline()
+ {
+ // given..when
+ var builder = GitHubPipelineBuilder.CreateNewPipeline();
+
+ // then
+ builder.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ShouldSetPipelineName()
+ {
+ // given
+ string inputName = "My GitHub Pipeline";
+ string expectedName = inputName;
+
+ // when
+ var pipelineBuilder = GitHubPipelineBuilder.CreateNewPipeline()
+ .SetName(inputName);
+
+ var actualPipeline = GetPipeline(pipelineBuilder);
+
+ // then
+ actualPipeline.Should().NotBeNull();
+ actualPipeline.Name.Should().BeEquivalentTo(expectedName);
+ }
+
+ [Fact]
+ public void ShouldAddPushTrigger()
+ {
+ // given
+ string[] inputBranches = { "main", "dev" };
+
+ // when
+ var pipelineBuilder = GitHubPipelineBuilder.CreateNewPipeline()
+ .OnPush(inputBranches);
+
+ var actualPipeline = GetPipeline(pipelineBuilder);
+
+ // then
+ actualPipeline.OnEvents.Push.Should().NotBeNull();
+ actualPipeline.OnEvents.Push.Branches.Should().BeEquivalentTo(inputBranches);
+ }
+
+ [Fact]
+ public void ShouldAddPullRequestTrigger()
+ {
+ // given
+ string[] inputBranches = { "main", "feature/*" };
+
+ // when
+ var pipelineBuilder = GitHubPipelineBuilder.CreateNewPipeline()
+ .OnPullRequest(inputBranches);
+
+ var actualPipeline = GetPipeline(pipelineBuilder);
+
+ // then
+ actualPipeline.OnEvents.PullRequest.Should().NotBeNull();
+ actualPipeline.OnEvents.PullRequest.Branches.Should().BeEquivalentTo(inputBranches);
+ }
+
+ [Fact]
+ public void ShouldAddJobToPipeline()
+ {
+ // given
+ string inputJobName = "build";
+ string inputRunsOn = BuildMachines.WindowsLatest;
+ string inputTaskName = "Restore";
+
+ string expectedRunsOn = inputRunsOn;
+ string expectedTaskName = inputTaskName;
+
+ // when
+ var pipelineBuilder = GitHubPipelineBuilder.CreateNewPipeline()
+ .AddJob(inputJobName, job =>
+ job.RunsOn(inputRunsOn)
+ .AddRestoreStep(inputTaskName));
+
+ var actualPipeline = GetPipeline(pipelineBuilder);
+
+ // then
+ var actualJob = actualPipeline.Jobs[inputJobName];
+ actualJob.Should().NotBeNull();
+ actualJob.RunsOn.Should().Be(expectedRunsOn);
+ actualJob.Steps.Should().HaveCount(1);
+
+ actualJob.Steps[0].Should().BeOfType()
+ .Which.Name.Should().Be(expectedTaskName);
+ }
+
+ [Fact]
+ public void ShouldSavePipelineToFile()
+ {
+ // given
+ string randomFileName = GetRandomFileName();
+ string randomPipelineName = GetRandomString();
+
+ GithubPipeline randomPipeline =
+ CreateRandomGithubPipeline(randomPipelineName);
+
+ GithubPipeline inputPipeline = randomPipeline;
+ string inputPath = randomFileName;
+ string inputPipelineName = randomPipelineName;
+
+ this.aDotNetClientMock.Setup(client =>
+ client.SerializeAndWriteToFile(
+ inputPipeline,
+ inputPath))
+ .Verifiable();
+
+ this.gitHubPipelineBuilder.SetName(inputPipelineName);
+
+ // when
+ this.gitHubPipelineBuilder.SaveToFile(inputPath);
+
+ // then
+ this.aDotNetClientMock.Verify(client =>
+ client.SerializeAndWriteToFile(
+ It.IsAny(),
+ It.IsAny()),
+ Times.Once);
+
+ this.aDotNetClientMock.VerifyNoOtherCalls();
+ }
+ }
+}
diff --git a/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.cs b/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.cs
new file mode 100644
index 0000000..f362b5a
--- /dev/null
+++ b/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.cs
@@ -0,0 +1,58 @@
+// ---------------------------------------------------------------------------
+// Copyright (c) Hassan Habib & Shri Humrudha Jagathisun All rights reserved.
+// Licensed under the MIT License.
+// See License.txt in the project root for license information.
+// ---------------------------------------------------------------------------
+
+using System.IO;
+using ADotNet.Clients;
+using ADotNet.Clients.Builders;
+using ADotNet.Models.Pipelines.GithubPipelines.DotNets;
+using Moq;
+using Tynamix.ObjectFiller;
+
+namespace ADotNet.Tests.Unit.Clients.Builders
+{
+ public partial class GitHubPipelineBuilderTests
+ {
+ private readonly Mock aDotNetClientMock;
+ private readonly GitHubPipelineBuilder gitHubPipelineBuilder;
+
+ public GitHubPipelineBuilderTests()
+ {
+ this.aDotNetClientMock = new Mock();
+
+ this.gitHubPipelineBuilder = new GitHubPipelineBuilder(
+ aDotNetClient: aDotNetClientMock.Object);
+ }
+
+ private static GithubPipeline GetPipeline(GitHubPipelineBuilder builder)
+ {
+ var privateField = typeof(GitHubPipelineBuilder)
+ .GetField(
+ name: "githubPipeline",
+ bindingAttr: System.Reflection.BindingFlags.NonPublic
+ | System.Reflection.BindingFlags.Instance);
+
+ return (GithubPipeline)privateField.GetValue(builder);
+ }
+
+ private static string GetRandomString() =>
+ new MnemonicString(wordCount: GetRandomNumber()).GetValue();
+
+ private static int GetRandomNumber() =>
+ new IntRange(min: 2, max: 10).GetValue();
+
+ private static string GetRandomFileName() =>
+ Path.GetRandomFileName();
+
+ private static GithubPipeline CreateRandomGithubPipeline(string name) =>
+ CreateGithubPipelineFiller(name).Create();
+
+ private static GithubPipeline CreateRandomGithubPipeline() =>
+ CreateGithubPipelineFiller(name: GetRandomString()).Create();
+
+ private static Filler CreateGithubPipelineFiller(string name) =>
+ new Filler();
+ }
+}