diff --git a/ADotNet/ADotNet.csproj b/ADotNet/ADotNet.csproj
index 254ea25..15dcdc8 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..7509aea
--- /dev/null
+++ b/ADotNet/Clients/Builders/GitHubPipelineBuilder.cs
@@ -0,0 +1,77 @@
+// ---------------------------------------------------------------------------
+// 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
+{
+ 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;
+ }
+
+ public static GitHubPipelineBuilder CreateNewPipeline()
+ {
+ var aDotNetClient = new ADotNetClient();
+
+ return new GitHubPipelineBuilder(aDotNetClient);
+ }
+
+ public GitHubPipelineBuilder SetName(string name)
+ {
+ this.githubPipeline.Name = name;
+
+ return this;
+ }
+
+ public GitHubPipelineBuilder OnPush(params string[] branches)
+ {
+ this.githubPipeline.OnEvents.Push = new PushEvent
+ {
+ Branches = branches
+ };
+
+ return this;
+ }
+
+ public GitHubPipelineBuilder OnPullRequest(params string[] branches)
+ {
+ this.githubPipeline.OnEvents.PullRequest = new PullRequestEvent
+ {
+ Branches = branches
+ };
+
+ return this;
+ }
+
+ public GitHubPipelineBuilder AddJob(string jobIdentifier, Action configureJob)
+ {
+ var jobBuilder = new JobBuilder();
+
+ configureJob(jobBuilder);
+
+ this.githubPipeline.Jobs[jobIdentifier] = jobBuilder.Build();
+
+ return this;
+ }
+
+ 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..2062794
--- /dev/null
+++ b/ADotNet/Clients/Builders/JobBuilder.cs
@@ -0,0 +1,123 @@
+// ---------------------------------------------------------------------------
+// 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
+{
+ public class JobBuilder
+ {
+ private readonly Job job;
+
+ internal JobBuilder()
+ {
+ this.job = new Job
+ {
+ Steps = new List(),
+ EnvironmentVariables = null
+ };
+ }
+
+ public JobBuilder WithName(string name)
+ {
+ this.job.Name = name;
+ return this;
+ }
+
+ public JobBuilder RunsOn(string machine)
+ {
+ this.job.RunsOn = machine;
+ return this;
+ }
+
+ public JobBuilder AddEnvironmentVariable(string key, string value)
+ {
+ this.job.EnvironmentVariables ??= new Dictionary();
+
+ this.job.EnvironmentVariables[key] = value;
+ return this;
+ }
+
+ public JobBuilder AddEnvironmentVariables(Dictionary variables)
+ {
+ this.job.EnvironmentVariables ??= new Dictionary();
+
+ foreach (var variable in variables)
+ {
+ this.job.EnvironmentVariables[variable.Key] = variable.Value;
+ }
+
+ return this;
+ }
+
+ public JobBuilder AddCheckoutStep(string name = "Check out")
+ {
+ this.job.Steps.Add(new CheckoutTaskV2 { Name = name });
+
+ return this;
+ }
+
+ 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;
+ }
+
+ public JobBuilder AddRestoreStep(string name = "Restore")
+ {
+ this.job.Steps.Add(new RestoreTask { Name = name });
+
+ return this;
+ }
+
+ public JobBuilder AddBuildStep(string name = "Build")
+ {
+ this.job.Steps.Add(new DotNetBuildTask { Name = name });
+
+ return this;
+ }
+
+ 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;
+ }
+
+ public JobBuilder AddGenericStep(string name, string runCommand)
+ {
+ this.job.Steps.Add(new GithubTask
+ {
+ Name = name,
+ Run = runCommand
+ });
+
+ return this;
+ }
+
+ public Job Build() => this.job;
+ }
+
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/EventsV2.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/EventsV2.cs
new file mode 100644
index 0000000..833a7e9
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/EventsV2.cs
@@ -0,0 +1,24 @@
+// ---------------------------------------------------------------------------
+// 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 YamlDotNet.Serialization;
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets
+{
+ public class EventsV2
+ {
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public PushEvent Push { get; set; }
+
+ [YamlMember(Alias = "pull_request", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public PullRequestEvent PullRequest { get; set; }
+
+ public ScheduledEvent[] Schedule { get; set; }
+
+ [YamlMember(Alias = "workflow_dispatch")]
+ public WorkflowDispatchEvent WorkflowDispatch { get; set; }
+ }
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/ScheduledEvent.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/ScheduledEvent.cs
new file mode 100644
index 0000000..4d9ecfa
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/ScheduledEvent.cs
@@ -0,0 +1,16 @@
+// ---------------------------------------------------------------------------
+// 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 YamlDotNet.Serialization;
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets
+{
+ public class ScheduledEvent
+ {
+ [YamlMember(Order = 0, DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public string Cron { get; set; }
+ }
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/InstallPlaywrightBrowsersTask.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/InstallPlaywrightBrowsersTask.cs
new file mode 100644
index 0000000..a7a4c97
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/InstallPlaywrightBrowsersTask.cs
@@ -0,0 +1,17 @@
+// ---------------------------------------------------------------------------
+// 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 YamlDotNet.Serialization;
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks
+{
+ public class InstallPlaywrightBrowsersTask : GithubTask
+ {
+ [YamlIgnore]
+ public string ProjectName { get; set; }
+ public override string Run => $"pwsh ./{ProjectName}/bin/Debug/net9.0/playwright.ps1 install";
+ }
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/InstallPlaywrightTask.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/InstallPlaywrightTask.cs
new file mode 100644
index 0000000..607861c
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/InstallPlaywrightTask.cs
@@ -0,0 +1,13 @@
+// ---------------------------------------------------------------------------
+// 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.
+// ---------------------------------------------------------------------------
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks
+{
+ public class InstallPlaywrightTask : GithubTask
+ {
+ public override string Run { get; set; } = "dotnet tool install --global Microsoft.Playwright.CLI";
+ }
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/RunUntilFailureTask.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/RunUntilFailureTask.cs
new file mode 100644
index 0000000..85417ad
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/RunUntilFailureTask.cs
@@ -0,0 +1,21 @@
+// ---------------------------------------------------------------------------
+// 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 YamlDotNet.Serialization;
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks
+{
+ public class RunUntilFailureTask : GithubTask
+ {
+ [YamlMember(Order = 1, Alias = "shell", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public override string Shell => "bash";
+
+ [YamlMember(Order = 2, Alias = "run", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public override string Run => "for i in {1..1000};" +
+ @" do echo ""Run #$i""; dotnet test --no-build " +
+ "--verbosity normal --filter 'FullyQualifiedName!~Integrations' || exit 1; done";
+ }
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/WorkloadUpdateTask.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/WorkloadUpdateTask.cs
new file mode 100644
index 0000000..f5c02cc
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/Tasks/WorkloadUpdateTask.cs
@@ -0,0 +1,13 @@
+// ---------------------------------------------------------------------------
+// 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.
+// ---------------------------------------------------------------------------
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks
+{
+ public class WorkloadUpdateTask : GithubTask
+ {
+ public override string Run { get; set; } = "dotnet workload update";
+ }
+}
diff --git a/ADotNet/Models/Pipelines/GithubPipelines/DotNets/WorkflowDispatchEvent.cs b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/WorkflowDispatchEvent.cs
new file mode 100644
index 0000000..923778a
--- /dev/null
+++ b/ADotNet/Models/Pipelines/GithubPipelines/DotNets/WorkflowDispatchEvent.cs
@@ -0,0 +1,11 @@
+// ---------------------------------------------------------------------------
+// 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.
+// ---------------------------------------------------------------------------
+
+namespace ADotNet.Models.Pipelines.GithubPipelines.DotNets
+{
+ public class WorkflowDispatchEvent
+ { }
+}
diff --git a/AdoNet.Tests.Console/Program.cs b/AdoNet.Tests.Console/Program.cs
index 06d673f..b6d8780 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,33 @@ static void Main(string[] args)
};
adoClient.SerializeAndWriteToFile(githubPipeline, "github-pipelines.yaml");
+
+ 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 .\\OtripleS.Api.Infrastructure.Provision\\OtripleS.Web.Api.Infrastructure.Provision.csproj"))
+ .SaveToFile("github-pipelines-2.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..faa1a65
--- /dev/null
+++ b/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.Logic.cs
@@ -0,0 +1,145 @@
+// ---------------------------------------------------------------------------
+// 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..8ed1ebc
--- /dev/null
+++ b/AdoNet.Tests.Unit/Clients/Builders/GitHubPipelineBuilderTests.cs
@@ -0,0 +1,55 @@
+// ---------------------------------------------------------------------------
+// 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("githubPipeline", 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();
+ }
+}