diff --git a/GitHelperApp/Application.cs b/GitHelperApp/Application.cs index 46e124f..c5534fb 100644 --- a/GitHelperApp/Application.cs +++ b/GitHelperApp/Application.cs @@ -15,6 +15,8 @@ namespace GitHelperApp; [Subcommand(typeof(SearchWorkItemsCommand))] [Subcommand(typeof(CreateCustomPrCommand))] [Subcommand(typeof(GetRepositoriesCommand))] +// [Subcommand(typeof(RunPipelinesCommand))] +[Subcommand(typeof(GetBuildsCommand))] public sealed class Application { /// diff --git a/GitHelperApp/Commands/GetBuildsCommand.cs b/GitHelperApp/Commands/GetBuildsCommand.cs new file mode 100644 index 0000000..7df6090 --- /dev/null +++ b/GitHelperApp/Commands/GetBuildsCommand.cs @@ -0,0 +1,68 @@ +using GitHelperApp.Commands.Interfaces; +using GitHelperApp.Models; +using GitHelperApp.Services.Interfaces; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; + +namespace GitHelperApp.Commands; + +/// +/// Special command to process the builds and get latest build runs. +/// +public sealed class GetBuildsCommand : ICustomCommand +{ + private readonly ILogger _logger; + private readonly IPipelineService _pipelineService; + private readonly IOutputService _outputService; + + [Option(CommandOptionType.SingleValue, Description = "Print to console", ShortName = "pc")] + private bool IsPrintToConsole { get; } + + [Option(CommandOptionType.SingleValue, Description = "Print to file", ShortName = "pf")] + private bool IsPrintToFile { get; } + + [Option(CommandOptionType.SingleValue, Description = "Branch", ShortName = "b")] + private string Branch { get; } + + [Option(CommandOptionType.SingleValue, Description = "Environment", ShortName = "e")] + private string Environment { get; } + + public GetBuildsCommand(ILogger logger, IPipelineService pipelineService, IOutputService outputService) + { + _logger = logger; + _pipelineService = pipelineService; + _outputService = outputService; + } + + public async Task OnExecuteAsync(CommandLineApplication command, IConsole console) + { + try + { + var (runId, directory) = _outputService.InitializeOutputBatch("GetBuilds"); + + // start pipelines + _logger.LogInformation("Start searching for builds..."); + + var settings = new PipelineRunSettings + { + Branch = Branch, + Environment = Environment + }; + + var buildResults = await _pipelineService.GetBuildDetailsAsync(settings); + + _logger.LogInformation($"Builds processed: {buildResults.Count}"); + + // output the results + _logger.LogInformation("Output build run results..."); + + _outputService.OutputBuildDetailsResult(buildResults, runId, directory, IsPrintToConsole, IsPrintToFile); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while searching builds on Azure DevOps"); + + throw; + } + } +} \ No newline at end of file diff --git a/GitHelperApp/Commands/RunPipelinesCommand.cs b/GitHelperApp/Commands/RunPipelinesCommand.cs new file mode 100644 index 0000000..a5ae126 --- /dev/null +++ b/GitHelperApp/Commands/RunPipelinesCommand.cs @@ -0,0 +1,69 @@ +using GitHelperApp.Commands.Interfaces; +using GitHelperApp.Models; +using GitHelperApp.Services.Interfaces; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; + +namespace GitHelperApp.Commands; + +// TODO: implement this command later... + +public sealed class RunPipelinesCommand : ICustomCommand +{ + private readonly ILogger _logger; + private readonly IPipelineService _pipelineService; + private readonly IOutputService _outputService; + + [Option(CommandOptionType.SingleValue, Description = "Print to console", ShortName = "pc")] + private bool IsPrintToConsole { get; } + + [Option(CommandOptionType.SingleValue, Description = "Print to file", ShortName = "pf")] + private bool IsPrintToFile { get; } + + [Option(CommandOptionType.SingleValue, Description = "Branch", ShortName = "b")] + private string Branch { get; } + + [Option(CommandOptionType.SingleValue, Description = "Environment", ShortName = "e")] + private string Environment { get; } + + [Option(CommandOptionType.SingleValue, Description = "Dry run", ShortName = "d")] + private bool DryRun { get; } + + public RunPipelinesCommand(ILogger logger, IPipelineService pipelineService, IOutputService outputService) + { + _logger = logger; + _pipelineService = pipelineService; + _outputService = outputService; + } + + public async Task OnExecuteAsync(CommandLineApplication command, IConsole console) + { + try + { + var (runId, directory) = _outputService.InitializeOutputBatch("RunPipelines"); + + // start pipelines + _logger.LogInformation("Start pipelines for repositories..."); + + var settings = new PipelineRunSettings + { + Branch = Branch, + Environment = Environment + }; + var runResults = await _pipelineService.RunPipelineAsync(settings, DryRun); + + _logger.LogInformation($"Pipelines processed: {runResults.Count}"); + + // output the results + _logger.LogInformation("Output pipelines run results..."); + + // TODO: implement the logic to print results + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while running pipelines on Azure DevOps"); + + throw; + } + } +} \ No newline at end of file diff --git a/GitHelperApp/DependencyInjection.cs b/GitHelperApp/DependencyInjection.cs index 61dae96..14b64f1 100644 --- a/GitHelperApp/DependencyInjection.cs +++ b/GitHelperApp/DependencyInjection.cs @@ -35,6 +35,8 @@ public static IServiceCollection InitializeDependencies(this IServiceCollection services.AddSingleton(); services.AddTransient(); + services.AddSingleton(); + // add content generators services.AddSingleton(); diff --git a/GitHelperApp/Generators/FileNameGenerator.cs b/GitHelperApp/Generators/FileNameGenerator.cs index dddc7a3..349825b 100644 --- a/GitHelperApp/Generators/FileNameGenerator.cs +++ b/GitHelperApp/Generators/FileNameGenerator.cs @@ -31,6 +31,9 @@ public string CreateFileNameForWorkItems(string directory, string runId) => public string CreateFileNameForRepositories(string directory, string runId) => Path.Combine(_appConfig.OutputDirectory, directory, $"Repositories-{runId}.{GetExtension(_appConfig.OutputFormat)}"); + public string CreateFileNameForBuildDetails(string directory, string runId) => + Path.Combine(_appConfig.OutputDirectory, directory, $"Builds-{runId}.{GetExtension(_appConfig.OutputFormat)}"); + private static string GetExtension(string format) { return format switch diff --git a/GitHelperApp/Generators/Interfaces/IContentGenerator.cs b/GitHelperApp/Generators/Interfaces/IContentGenerator.cs index d8fc923..65d8d9c 100644 --- a/GitHelperApp/Generators/Interfaces/IContentGenerator.cs +++ b/GitHelperApp/Generators/Interfaces/IContentGenerator.cs @@ -16,4 +16,5 @@ public interface IContentGenerator List ProcessWorkItemsSearchResults(List witResults); List ProcessSummaryTableResult(List aggregatedResult); List ProcessRepositoriesResult(List repositoryModels); + IReadOnlyCollection ProcessBuildDetailsResult(List buildResults); } \ No newline at end of file diff --git a/GitHelperApp/Generators/Interfaces/IFileNameGenerator.cs b/GitHelperApp/Generators/Interfaces/IFileNameGenerator.cs index 9f93efc..c4d1cf3 100644 --- a/GitHelperApp/Generators/Interfaces/IFileNameGenerator.cs +++ b/GitHelperApp/Generators/Interfaces/IFileNameGenerator.cs @@ -10,4 +10,5 @@ public interface IFileNameGenerator string CreateFileNameForPrIds(string directory, string runId); string CreateFileNameForWorkItems(string directory, string runId); string CreateFileNameForRepositories(string directory, string runId); + string CreateFileNameForBuildDetails(string directory, string runId); } \ No newline at end of file diff --git a/GitHelperApp/Generators/MarkdownContentGenerator.cs b/GitHelperApp/Generators/MarkdownContentGenerator.cs index a97d8bd..bcfa80a 100644 --- a/GitHelperApp/Generators/MarkdownContentGenerator.cs +++ b/GitHelperApp/Generators/MarkdownContentGenerator.cs @@ -160,4 +160,10 @@ public List ProcessRepositoriesResult(List repositoryMo return lines; } + + public IReadOnlyCollection ProcessBuildDetailsResult(List buildResults) + { + // TODO: no need to add the logic here because simple markdown file is not supported the tables + return Enumerable.Empty().ToList(); + } } \ No newline at end of file diff --git a/GitHelperApp/Generators/MarkdownTableContentGenerator.cs b/GitHelperApp/Generators/MarkdownTableContentGenerator.cs index ba14afe..6d4599b 100644 --- a/GitHelperApp/Generators/MarkdownTableContentGenerator.cs +++ b/GitHelperApp/Generators/MarkdownTableContentGenerator.cs @@ -165,7 +165,19 @@ public List ProcessRepositoriesResult(List repositoryMo return lines; } - private IEnumerable CreateRepositoriesTable(List repositoryModels) + public IReadOnlyCollection ProcessBuildDetailsResult(List buildResults) + { + var lines = new List(); + + lines.Add($"# Build details (Count = {buildResults.Count})"); + lines.AddRange(CreateBuildDetailsTable(buildResults)); + + return lines; + } + + #region Helpers. + + private static IEnumerable CreateRepositoriesTable(List repositoryModels) { return repositoryModels .OrderBy(x => x.Name) @@ -176,9 +188,7 @@ private IEnumerable CreateRepositoriesTable(List reposi }) .ToMarkdownTable(new[] { "#", "Title" }); } - - #region Helpers. - + private static IEnumerable CreatePullRequestList(List prResults) { return prResults.Where(x => x.PullRequestId != 0) @@ -242,6 +252,29 @@ private static IEnumerable CreateSummaryTable(List }) .ToMarkdownTable(new[] { "#", "Repository", "Build Pipeline", "PR", "Work Items Count" }); } + + private static IEnumerable CreateBuildDetailsTable(List buildResults) + { + return buildResults + .Where(x => x != null) + .Select((x, index) => new + { + Index = index + 1, + Repo = $"[{x.RepositoryName}]({x.RepositoryUrl})", + x.Environment, + x.SourceBranch, + SourceLink = $"[{x.SourceVersion}]({x.SourceCommitLink})", + Link = x.Status == "None" + ? $"[Pipeline]({x.BuildLink})" + : $"[Build ({x.Status})]({x.BuildLink})", + x.RequestedFor, + x.StartTime, + x.FinishTime, + CurrentLink = $"[{x.CurrentCommit}]({x.CurrentCommitLink})", + x.Message + }) + .ToMarkdownTable(new[] { "#", "Repository", "Environment", "Branch", "Commit", "Build", "Requested For", "Start", "End", "Current Commit", "Status" }); + } #endregion } \ No newline at end of file diff --git a/GitHelperApp/Generators/TextFileContentGenerator.cs b/GitHelperApp/Generators/TextFileContentGenerator.cs index f2ed9b0..0951d6c 100644 --- a/GitHelperApp/Generators/TextFileContentGenerator.cs +++ b/GitHelperApp/Generators/TextFileContentGenerator.cs @@ -159,4 +159,10 @@ public List ProcessRepositoriesResult(List repositoryMo return lines; } + + public IReadOnlyCollection ProcessBuildDetailsResult(List buildResults) + { + // TODO: no need to add the logic here because text file is not supported the tables + return Enumerable.Empty().ToList(); + } } \ No newline at end of file diff --git a/GitHelperApp/GitHelperApp.csproj b/GitHelperApp/GitHelperApp.csproj index 494299c..5841f0e 100644 --- a/GitHelperApp/GitHelperApp.csproj +++ b/GitHelperApp/GitHelperApp.csproj @@ -11,10 +11,10 @@ - + - - + + @@ -55,6 +55,9 @@ Always + + Always + diff --git a/GitHelperApp/GitHelperApp/appsettings.DD.json b/GitHelperApp/GitHelperApp/appsettings.DD.json new file mode 100644 index 0000000..ef1d484 --- /dev/null +++ b/GitHelperApp/GitHelperApp/appsettings.DD.json @@ -0,0 +1,14 @@ +{ + "RepositoriesConfig": { + "DefaultSourceBranch": "dev", + "DefaultDestinationBranch": "release", + "DefaultTeamProject": "MSG", + "Repositories": [ + { + "Name": "user-account-service", + "Path": "C:\\Projects\\Matrix\\user-account-service", + "PipelineId": 1012 + } + ] + } +} \ No newline at end of file diff --git a/GitHelperApp/Helpers/GitBranchHelper.cs b/GitHelperApp/Helpers/GitBranchHelper.cs index f9b831f..5817c8f 100644 --- a/GitHelperApp/Helpers/GitBranchHelper.cs +++ b/GitHelperApp/Helpers/GitBranchHelper.cs @@ -7,4 +7,14 @@ public static class GitBranchHelper { public static string GetRefName(string branchName) => $"origin/{branchName}"; public static string GetRefNameForAzure(string branchName) => $"refs/heads/{branchName}"; + + /// + /// Remove the branch ref header - 'refs/heads/'. + /// + /// Branch name with full ref from the Azure DevOps. + /// Returns the branch name without ref name. + public static string RemoveRefName(string refBranchName) + { + return refBranchName.AsSpan(11).ToString(); + } } \ No newline at end of file diff --git a/GitHelperApp/Models/BuildDetails.cs b/GitHelperApp/Models/BuildDetails.cs new file mode 100644 index 0000000..95ced70 --- /dev/null +++ b/GitHelperApp/Models/BuildDetails.cs @@ -0,0 +1,23 @@ +namespace GitHelperApp.Models; + +public sealed class BuildDetails +{ + public string RepositoryName { get; set; } + public string RepositoryUrl { get; set; } + public int BuildId { get; set; } + public string RequestedFor { get; set; } + public DateTime FinishTime { get; set; } + public DateTime StartTime { get; set; } + public string Status { get; set; } + public string SourceBranch { get; set; } + public string SourceVersion { get; set; } + public string SourceCommitLink { get; set; } + + public string Environment { get; set; } + + public string BuildLink { get; set; } + + public string CurrentCommit { get; set; } + public string CurrentCommitLink { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/GitHelperApp/Models/PipelineResult.cs b/GitHelperApp/Models/PipelineResult.cs new file mode 100644 index 0000000..d670010 --- /dev/null +++ b/GitHelperApp/Models/PipelineResult.cs @@ -0,0 +1,8 @@ +namespace GitHelperApp.Models; + +public sealed class PipelineResult +{ + public string Name { get; set; } + public string Url { get; set; } + public int Id { get; set; } +} \ No newline at end of file diff --git a/GitHelperApp/Models/PipelineRunSettings.cs b/GitHelperApp/Models/PipelineRunSettings.cs new file mode 100644 index 0000000..df7f4b2 --- /dev/null +++ b/GitHelperApp/Models/PipelineRunSettings.cs @@ -0,0 +1,10 @@ +namespace GitHelperApp.Models; + +/// +/// Settings to run the pipeline from specific branch to the specific environment. +/// +public sealed class PipelineRunSettings +{ + public string Branch { get; set; } + public string Environment { get; set; } +} \ No newline at end of file diff --git a/GitHelperApp/Properties/launchSettings.json b/GitHelperApp/Properties/launchSettings.json index 0ede7b9..c1a3eec 100644 --- a/GitHelperApp/Properties/launchSettings.json +++ b/GitHelperApp/Properties/launchSettings.json @@ -97,6 +97,34 @@ "environmentVariables": { "GHA_ENVIRONMENT": "Local" } + }, + "GitHelperApp-RunPipeline-DryRun": { + "commandName": "Project", + "commandLineArgs": "run-pipelines -pf true -b dev -e Test -d true", + "environmentVariables": { + "GHA_ENVIRONMENT": "DD" + } + }, + "GitHelperApp-GetBuilds-Dev-Test": { + "commandName": "Project", + "commandLineArgs": "get-builds -pf true -b dev -e Test", + "environmentVariables": { + "GHA_ENVIRONMENT": "DR" + } + }, + "GitHelperApp-GetBuilds-Release-Staging": { + "commandName": "Project", + "commandLineArgs": "get-builds -pf true -b release -e Staging", + "environmentVariables": { + "GHA_ENVIRONMENT": "DR" + } + }, + "GitHelperApp-GetBuilds-Release-Prod": { + "commandName": "Project", + "commandLineArgs": "get-builds -pf true -b release -e Production", + "environmentVariables": { + "GHA_ENVIRONMENT": "DR" + } } } } \ No newline at end of file diff --git a/GitHelperApp/Services/AzureDevOpsService.cs b/GitHelperApp/Services/AzureDevOpsService.cs index 34a886b..2f8c1e7 100644 --- a/GitHelperApp/Services/AzureDevOpsService.cs +++ b/GitHelperApp/Services/AzureDevOpsService.cs @@ -1,7 +1,11 @@ -using GitHelperApp.Configuration; +using System.Text.Json; +using GitHelperApp.Configuration; using GitHelperApp.Helpers; +using GitHelperApp.Models; using GitHelperApp.Services.Interfaces; +using Microsoft.Azure.Pipelines.WebApi; using Microsoft.Extensions.Options; +using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.TeamFoundation.SourceControl.WebApi; using Microsoft.TeamFoundation.WorkItemTracking.WebApi; @@ -19,6 +23,8 @@ public sealed class AzureDevOpsService : IAzureDevOpsService private readonly GitHttpClient _gitClient; private readonly AzureDevOpsConfig _config; private readonly WorkItemTrackingHttpClient _workItemTrackingHttpClient; + private readonly BuildHttpClient _buildHttpClient; + private readonly PipelinesHttpClient _pipelinesHttpClient; public AzureDevOpsService(IOptions config) { @@ -31,6 +37,10 @@ public AzureDevOpsService(IOptions config) _gitClient = connection.GetClient(); _workItemTrackingHttpClient = connection.GetClient(); + _buildHttpClient = connection.GetClient(); + + // TODO: here is not the best solution to create instance of the client because used in other way than other clients( + _pipelinesHttpClient = new PipelinesHttpClient(new Uri(_config.CollectionUrl), creds); } public async Task GetRepositoryAsync(Guid repositoryId) @@ -186,6 +196,77 @@ public async Task> GetRepositoriesListAsync() return await _gitClient.GetRepositoriesAsync(_config.TeamProject); } + public async Task> GetLastCommitAsync(GitRepository repository, string branch) + { + var result = await _gitClient.GetCommitsAsync(repository.Id, + new GitQueryCommitsCriteria { ItemVersion = new GitVersionDescriptor { Version = branch } }, top: 1); + return result; + } + + public async Task GetLastBuildDetailsAsync(string teamProject, int buildId) + { + var tmp = await _buildHttpClient.GetBuildsAsync(teamProject, new[] { buildId }, top: 1); + + return tmp.FirstOrDefault(); + } + + public async Task GetLastBuildDetailsAsync(string teamProject, int buildId, string branchName) + { + var azureBranch = GitBranchHelper.GetRefNameForAzure(branchName); + var builds = await _buildHttpClient.GetBuildsAsync(teamProject, new[] { buildId }, branchName: azureBranch, top: 1); + + return builds.FirstOrDefault(); + } + + public async Task> GetBuildDetailsAsync(string teamProject, int buildId, int top = 10) + { + var builds = await _buildHttpClient.GetBuildsAsync(teamProject, new[] { buildId }, top: top); + + return builds; + } + + public async Task> GetBuildDetailsAsync(string teamProject, int buildId, string branchName, int top = 10) + { + var azureBranch = GitBranchHelper.GetRefNameForAzure(branchName); + var builds = await _buildHttpClient.GetBuildsAsync(teamProject, new[] { buildId }, branchName: azureBranch, top: top); + + return builds; + } + + public async Task GetPipelineAsyncAsync(string teamProject, int pipelineId) + { + var pipeline = await _pipelinesHttpClient.GetPipelineAsync(teamProject, pipelineId); + + // var runs = await _pipelinesHttpClient.ListRunsAsync(teamProject, pipelineId); + // var run = await _pipelinesHttpClient.GetRunAsync(teamProject, pipelineId, runs.First().Id); + + return pipeline; + } + + public async Task RunPipelineAsyncAsync(string teamProject, int pipelineId, PipelineRunSettings settings, bool isDryRun = false) + { + // TODO: this functionality is now working so it needed to wait for final API to be working :( + + // var repositories = new Dictionary + // { + // { "self", new RepositoryResourceParameters { RefName = GetRefName(settings.Branch) } } + // }; + + var tmp = await _pipelinesHttpClient.ListRunsAsync(teamProject, pipelineId); + + // var resourcesString = $@"{{'repositories': {{'self': {{'refName': '{GetRefName(settings.Branch)}'}}}}}}"; + var pipelineParameters = new RunPipelineParameters + { + PreviewRun = isDryRun, + TemplateParameters = new Dictionary + { + { "Environment", settings.Environment } + } + }; + var result = await _pipelinesHttpClient.RunPipelineAsync(pipelineParameters, teamProject, pipelineId); + return result; + } + public string BuildPullRequestUrl(string teamProject, string repositoryName, int pullRequestId) { return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_git/{Uri.EscapeDataString(repositoryName)}/pullrequest/{pullRequestId}"; @@ -196,13 +277,28 @@ public string BuildWorkItemUrl(string teamProject, string workItemId) return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_workitems/edit/{workItemId}"; } - public string BuildRepositoryUrl(string teamProject, string name) + public string BuildRepositoryUrl(string teamProject, string repositoryName) { - return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_git/{Uri.EscapeDataString(name)}"; + return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_git/{Uri.EscapeDataString(repositoryName)}"; } public string BuildPipelineUrl(string teamProject, int pipelineId) { return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_build?definitionId={pipelineId}"; } + + public string BuildBuildResultUrl(string teamProject, int buildId) + { + return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_build/results?buildId={buildId}&view=results"; + } + + public string BuildRepositoryCommitUrl(string teamProject, string repositoryName, string commit) + { + return $"{_config.CollectionUrl}/{Uri.EscapeDataString(teamProject)}/_git/{Uri.EscapeDataString(repositoryName)}/commit/{commit}"; + } + + private RunResourcesParameters DeserializeRunResourcesParameters(string value) + { + return JsonSerializer.Deserialize(value); + } } \ No newline at end of file diff --git a/GitHelperApp/Services/Interfaces/IAzureDevOpsService.cs b/GitHelperApp/Services/Interfaces/IAzureDevOpsService.cs index 60a366a..22e1cd6 100644 --- a/GitHelperApp/Services/Interfaces/IAzureDevOpsService.cs +++ b/GitHelperApp/Services/Interfaces/IAzureDevOpsService.cs @@ -1,4 +1,7 @@ -using Microsoft.TeamFoundation.Core.WebApi; +using GitHelperApp.Models; +using Microsoft.Azure.Pipelines.WebApi; +using Microsoft.TeamFoundation.Build.WebApi; +using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.TeamFoundation.SourceControl.WebApi; using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; using Microsoft.VisualStudio.Services.WebApi; @@ -27,8 +30,19 @@ public interface IAzureDevOpsService Task> GetRepositoriesListAsync(string teamProject); Task> GetRepositoriesListAsync(); + Task> GetLastCommitAsync(GitRepository repository, string branch); + Task GetLastBuildDetailsAsync(string teamProject, int buildId); + Task GetLastBuildDetailsAsync(string teamProject, int buildId, string branchName); + Task> GetBuildDetailsAsync(string teamProject, int buildId, int top = 10); + Task> GetBuildDetailsAsync(string teamProject, int buildId, string branchName, int top = 10); + + Task GetPipelineAsyncAsync(string teamProject, int pipelineId); + Task RunPipelineAsyncAsync(string teamProject, int pipelineId, PipelineRunSettings settings, bool isDryRun = false); + string BuildPullRequestUrl(string teamProject, string repositoryName, int pullRequestId); string BuildWorkItemUrl(string teamProject, string workItemId); - string BuildRepositoryUrl(string teamProject, string name); + string BuildRepositoryUrl(string teamProject, string repositoryName); string BuildPipelineUrl(string teamProject, int pipelineId); + string BuildBuildResultUrl(string teamProject, int buildId); + string BuildRepositoryCommitUrl(string teamProject, string repositoryName, string commit); } \ No newline at end of file diff --git a/GitHelperApp/Services/Interfaces/IOutputService.cs b/GitHelperApp/Services/Interfaces/IOutputService.cs index f1aab6d..9e9e746 100644 --- a/GitHelperApp/Services/Interfaces/IOutputService.cs +++ b/GitHelperApp/Services/Interfaces/IOutputService.cs @@ -23,4 +23,6 @@ void OutputWorkItemsSearchResult(List compareResults, List repositoryModels, string runId, string directory, bool isPrintToConsole, bool isPrintToFile); + + void OutputBuildDetailsResult(List buildResults, string runId, string directory, bool isPrintToConsole, bool isPrintToFile); } \ No newline at end of file diff --git a/GitHelperApp/Services/Interfaces/IPipelineService.cs b/GitHelperApp/Services/Interfaces/IPipelineService.cs new file mode 100644 index 0000000..83e0b6a --- /dev/null +++ b/GitHelperApp/Services/Interfaces/IPipelineService.cs @@ -0,0 +1,18 @@ +using GitHelperApp.Models; + +namespace GitHelperApp.Services.Interfaces; + +/// +/// Service to work with builds and pipelines. +/// +public interface IPipelineService +{ + Task> RunPipelineAsync(PipelineRunSettings settings, bool isDryRun = false); + + /// + /// Get build details - runs, commits, etc. + /// + /// Settings for builds. + /// Returns list with build details for each repository. + Task> GetBuildDetailsAsync(PipelineRunSettings settings); +} \ No newline at end of file diff --git a/GitHelperApp/Services/OutputService.cs b/GitHelperApp/Services/OutputService.cs index 0097fe4..906b635 100644 --- a/GitHelperApp/Services/OutputService.cs +++ b/GitHelperApp/Services/OutputService.cs @@ -159,6 +159,23 @@ public void OutputRepositoriesResults(List repositoryModels, st } } + public void OutputBuildDetailsResult(List buildResults, string runId, string directory, bool isPrintToConsole, + bool isPrintToFile) + { + var contentGenerator = _contentGeneratorFactory.GetContentGenerator(_appConfig.OutputFormat); + var lines = contentGenerator.ProcessBuildDetailsResult(buildResults); + + if (isPrintToConsole) + { + OutputHelper.OutputResultToConsole(lines); + } + + if (isPrintToFile) + { + OutputHelper.OutputResultToFile(lines, _fileNameGenerator.CreateFileNameForBuildDetails(directory, runId)); + } + } + #region Helpers. private static string BuildDirectoryName(string commandName) diff --git a/GitHelperApp/Services/PipelineService.cs b/GitHelperApp/Services/PipelineService.cs new file mode 100644 index 0000000..04bfc0f --- /dev/null +++ b/GitHelperApp/Services/PipelineService.cs @@ -0,0 +1,196 @@ +using GitHelperApp.Configuration; +using GitHelperApp.Extensions; +using GitHelperApp.Helpers; +using GitHelperApp.Models; +using GitHelperApp.Services.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.TeamFoundation.Build.WebApi; + +namespace GitHelperApp.Services; + +/// +/// Service to work with pipelines and other builds information. +/// +public sealed class PipelineService : IPipelineService +{ + private readonly ILogger _logger; + private readonly RepositoriesConfig _repositoriesConfig; + private readonly IAzureDevOpsService _azureDevOpsService; + + public PipelineService(ILogger logger, IAzureDevOpsService azureDevOpsService, + IOptions repositoriesConfig) + { + _logger = logger; + _azureDevOpsService = azureDevOpsService; + _repositoriesConfig = repositoriesConfig.Value; + } + + // TODO: implement later this method later if needed + /// + public async Task> RunPipelineAsync(PipelineRunSettings settings, bool isDryRun = false) + { + var result = new List(); + + foreach (var repositoryConfig in _repositoriesConfig.Repositories) + { + _logger.LogInformation($"Running the pipeline for {repositoryConfig.Name}..."); + + repositoryConfig.GetRepositoryConfig(_repositoriesConfig); + + // var pipeline = await _azureDevOpsService.GetPipelineAsyncAsync(repositoryConfig.TeamProject, repositoryConfig.PipelineId); + // + // var run = await _azureDevOpsService.RunPipelineAsyncAsync(repositoryConfig.TeamProject, repositoryConfig.PipelineId, settings, isDryRun); + // + // result.Add(new PipelineResult + // { + // Id = run.Id, + // Name = run.Name, + // Url = run.Url + // }); + } + + return result; + } + + /// + public async Task> GetBuildDetailsAsync(PipelineRunSettings settings) + { + var tasks = new List(_repositoriesConfig.Repositories.Count); + + foreach (var repositoryConfig in _repositoriesConfig.Repositories) + { + _logger.LogInformation($"Get the build details for {repositoryConfig.Name}..."); + + repositoryConfig.GetRepositoryConfig(_repositoriesConfig); + + tasks.Add(GetLastBuildDetailsForEnvironmentAsync(repositoryConfig.TeamProject, + _repositoriesConfig.DefaultTeamProject, repositoryConfig.Name, + repositoryConfig.PipelineId, settings.Branch, settings.Environment)); + } + + await Task.WhenAll(tasks); + + return tasks.Select(task => ((Task)task).Result).ToList(); + } + + #region Helpers and main logic. + + // private async Task GetPipelineDetailsAsync(string teamProject, int pipelineId, string branchName) + // { + // var pipeline = await _azureDevOpsService.GetPipelineAsyncAsync(teamProject, pipelineId); + // + // var build = await _azureDevOpsService.GetLastBuildDetailsAsync(teamProject, pipelineId); + // + // var buildBranch = await _azureDevOpsService.GetLastBuildDetailsAsync(teamProject, pipelineId, branchName); + // } + + private async Task GetLastBuildDetailsForEnvironmentAsync(string teamProject, string defaultTeamProject, + string repositoryName, int pipelineId, string branchName, string environmentName) + { + // get repository details + var repo = await _azureDevOpsService.GetRepositoryByNameAsync(repositoryName, teamProject); + var commits = await _azureDevOpsService.GetLastCommitAsync(repo, branchName); + + // get all build details first + var build = await GetFromAllBuildsAsync(defaultTeamProject, pipelineId, environmentName); + + // try to search builds based on the branch + if (build == null) + { + build = await GetFromBranchBuildsAsync(defaultTeamProject, pipelineId, environmentName, branchName); + } + + return build != null + ? CreateBuildDetails(build, teamProject, defaultTeamProject, repositoryName, branchName, commits.FirstOrDefault().CommitId) + : CreateEmptyBuildDetails(teamProject, defaultTeamProject, repositoryName, environmentName, commits.FirstOrDefault().CommitId, pipelineId); + } + + private async Task GetFromAllBuildsAsync(string teamProject, int buildId, string environmentName) + { + var builds = await _azureDevOpsService.GetBuildDetailsAsync(teamProject, buildId, 20); + var build = builds.FirstOrDefault(x => ExtractEnvironmentName(x.TemplateParameters) == environmentName); + return build; + } + + private async Task GetFromBranchBuildsAsync(string teamProject, int buildId, string environmentName, string branchName) + { + var builds = await _azureDevOpsService.GetBuildDetailsAsync(teamProject, buildId, branchName, 20); + var build = builds.FirstOrDefault(x => ExtractEnvironmentName(x.TemplateParameters) == environmentName); + return build; + } + + private BuildDetails CreateBuildDetails(Build build, string teamProject, string defaultTeamProject, string repositoryName, string branchName, string commit) + { + var buildDetails = new BuildDetails + { + RepositoryName = repositoryName, + RepositoryUrl = _azureDevOpsService.BuildRepositoryUrl(teamProject, repositoryName), + BuildId = build.Id, + RequestedFor = build.RequestedFor.DisplayName, + FinishTime = build.FinishTime.GetValueOrDefault(), + StartTime = build.StartTime.GetValueOrDefault(), + Status = build.Status.GetValueOrDefault().ToString(), + SourceBranch = GitBranchHelper.RemoveRefName(build.SourceBranch), + SourceVersion = build.SourceVersion, + SourceCommitLink = _azureDevOpsService.BuildRepositoryCommitUrl(teamProject, repositoryName, build.SourceVersion), + Environment = ExtractEnvironmentName(build.TemplateParameters), + BuildLink = _azureDevOpsService.BuildBuildResultUrl(defaultTeamProject, build.Id), + CurrentCommit = commit, + CurrentCommitLink = _azureDevOpsService.BuildRepositoryCommitUrl(teamProject, repositoryName, commit) + }; + + var isLatestCommit = String.Compare(build.SourceVersion, commit, StringComparison.InvariantCultureIgnoreCase) == 0; + + string statusMessage; + if (buildDetails.SourceBranch == branchName) + { + if (isLatestCommit) + { + statusMessage = $"Same as {branchName}"; + } + else + { + statusMessage = $"Behind the {branchName}"; + } + } + else + { + statusMessage = $"{branchName} isn't deployed"; + } + + buildDetails.Message = statusMessage; + + return buildDetails; + } + + private BuildDetails CreateEmptyBuildDetails(string teamProject, string defaultTeamProject, string repositoryName, string environmentName, string commit, int pipelineId) + { + return new BuildDetails + { + Environment = environmentName, + RepositoryName = repositoryName, + RepositoryUrl = _azureDevOpsService.BuildRepositoryUrl(teamProject, repositoryName), + CurrentCommit = commit, + CurrentCommitLink = _azureDevOpsService.BuildRepositoryCommitUrl(teamProject, repositoryName, commit), + Status = "None", + BuildLink = _azureDevOpsService.BuildPipelineUrl(defaultTeamProject, pipelineId) + }; + } + + private static string ExtractEnvironmentName(IReadOnlyDictionary templateParameters) + { + if (templateParameters.ContainsKey("environment")) + { + return templateParameters["environment"]; + } + if (templateParameters.ContainsKey("Environment")) + { + return templateParameters["Environment"]; + } + + return String.Empty; + } + + #endregion +} \ No newline at end of file diff --git a/GitHelperApp/Services/PullRequestService.cs b/GitHelperApp/Services/PullRequestService.cs index b2c08b2..11cc965 100644 --- a/GitHelperApp/Services/PullRequestService.cs +++ b/GitHelperApp/Services/PullRequestService.cs @@ -86,8 +86,8 @@ public async Task> SearchPullRequestsAsync(string PullRequestId = gitPullRequest.PullRequestId, Title = gitPullRequest.Title, Description = gitPullRequest.Description, - SourceBranch = RemoveRefName(gitPullRequest.SourceRefName), - DestinationBranch = RemoveRefName(gitPullRequest.TargetRefName), + SourceBranch = GitBranchHelper.RemoveRefName(gitPullRequest.SourceRefName), + DestinationBranch = GitBranchHelper.RemoveRefName(gitPullRequest.TargetRefName), RepositoryName = repositoryConfig.Name, Url = _azureDevOpsService.BuildPullRequestUrl(repositoryConfig.TeamProject, repo.Name, gitPullRequest.PullRequestId), WorkItems = workItemsFlorPr.Select(x => x.ToModel(_azureDevOpsService.BuildWorkItemUrl(repositoryConfig.TeamProject, x.Id))).ToList(), @@ -274,15 +274,5 @@ private static PullRequestStatus ConvertStatus(string status) }; } - /// - /// Remove the branch ref header - 'refs/heads/'. - /// - /// Branch name with full ref from the Azure DevOps. - /// Returns the branch name without ref name. - private static string RemoveRefName(string refBranchName) - { - return refBranchName.AsSpan(11).ToString(); - } - #endregion } \ No newline at end of file diff --git a/GitHelperApp/appsettings.DD.json b/GitHelperApp/appsettings.DD.json new file mode 100644 index 0000000..170c114 --- /dev/null +++ b/GitHelperApp/appsettings.DD.json @@ -0,0 +1,14 @@ +{ + "RepositoriesConfig": { + "DefaultSourceBranch": "dev", + "DefaultDestinationBranch": "dev", + "DefaultTeamProject": "MSG", + "Repositories": [ + { + "Name": "avail-gateway-svc", + "Path": "C:\\Projects\\Matrix\\avail-gateway-svc", + "PipelineId": 1019 + } + ] + } +} \ No newline at end of file