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(); + } +}