diff --git a/docs/RepoM.Plugin.AzureDevOps.md b/docs/RepoM.Plugin.AzureDevOps.md index f5b66ca2..00a757f7 100644 --- a/docs/RepoM.Plugin.AzureDevOps.md +++ b/docs/RepoM.Plugin.AzureDevOps.md @@ -12,10 +12,13 @@ The following default configuration is used: ```json { - "Version": 1, + "Version": 2, "Settings": { "PersonalAccessToken": null, - "BaseUrl": null + "BaseUrl": null, + "DefaultProjectId": null, + "IntervalUpdatePullRequests": "00:04:00", + "IntervalUpdateProjects": "00:10:00" } } ``` @@ -24,7 +27,10 @@ Properties: - `PersonalAccessToken`: Personal access token (PAT) to access Azure Devops. The PAT should be granted access to `todo` rights. To create a PAT, goto `https://dev.azure.com/[my-organisation]/_usersSettings/tokens`. -- `BaseUrl`: The base url of azure devops for your organisation (ie. `https://dev.azure.com/[my-organisation]/`). +- `BaseUrl`: The base url of azure devops for your organisation (ie. `https://dev.azure.com/[my-organisation]/`). +- `DefaultProjectId`: Default project id to use when no project id provided in the repository action. Should be a GUID. +- `IntervalUpdatePullRequests`: Interval RepoM should update the list of open pull requests from Azure DevOps. Defaults to `4` minutes (ie. `00:04:00`). +- `IntervalUpdateProjects`: Interval RepoM should update the list of projects from Azure DevOps. Defaults to `10` minutes (ie. `00:10:00`). ## azure-devops-create-prs@1 diff --git a/src/RepoM.Plugin.AzureDevOps/ActionProvider/ActionAzureDevOpsCreatePullRequestsV2Mapper.cs b/src/RepoM.Plugin.AzureDevOps/ActionProvider/ActionAzureDevOpsCreatePullRequestsV2Mapper.cs new file mode 100644 index 00000000..5a422e53 --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/ActionProvider/ActionAzureDevOpsCreatePullRequestsV2Mapper.cs @@ -0,0 +1,112 @@ +namespace RepoM.Plugin.AzureDevOps.ActionProvider; + +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using RepoM.Api.Git; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; +using RepoM.Core.Plugin.Expressions; +using RepoM.Core.Plugin.RepositoryActions.Actions; +using RepoM.Plugin.AzureDevOps.ActionProvider.Options; +using RepoM.Plugin.AzureDevOps.Internal; +using RepositoryAction = Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; + +[UsedImplicitly] +internal class ActionAzureDevOpsCreatePullRequestsV2Mapper : IActionToRepositoryActionMapper +{ + private readonly IAzureDevOpsPullRequestService _service; + private readonly IRepositoryExpressionEvaluator _expressionEvaluator; + private readonly ILogger _logger; + + public ActionAzureDevOpsCreatePullRequestsV2Mapper(IAzureDevOpsPullRequestService service, IRepositoryExpressionEvaluator expressionEvaluator, ILogger logger) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _expressionEvaluator = expressionEvaluator ?? throw new ArgumentNullException(nameof(expressionEvaluator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public bool CanMap(RepositoryAction action) + { + return action is RepositoryActionAzureDevOpsCreatePullRequestsV2; + } + + public bool CanHandleMultipleRepositories() + { + return false; + } + + public IEnumerable Map(RepositoryAction action, IEnumerable repository, ActionMapperComposition actionMapperComposition) + { + return Map(action as RepositoryActionAzureDevOpsCreatePullRequestsV2, repository.First()); + } + + private IEnumerable Map(RepositoryActionAzureDevOpsCreatePullRequestsV2? action, Repository repository) + { + if (action == null) + { + return Array.Empty(); + } + + if (!_expressionEvaluator.EvaluateBooleanExpression(action.Active, repository)) + { + return Array.Empty(); + } + + if (repository.HasLocalChanges || repository.CurrentBranch.Equals(action.ToBranch, StringComparison.OrdinalIgnoreCase)) + { + return Array.Empty(); + } + + if (string.IsNullOrWhiteSpace(action.ProjectId)) + { + return Array.Empty(); + } + + var projectId = _expressionEvaluator.EvaluateStringExpression(action.ProjectId, repository); + + if (string.IsNullOrWhiteSpace(projectId)) + { + return Array.Empty(); + } + + if (!string.IsNullOrWhiteSpace(action.ToBranch)) + { + // check if branch exists! + if (!repository.Branches.Contains(action.ToBranch)) + { + _logger.LogInformation("Branch {branch} does not exist", action.ToBranch); + return Array.Empty(); + } + } + else + { + return Array.Empty(); + } + + bool hasAutoComplete = action.AutoComplete != null; + bool isDraft = _expressionEvaluator.EvaluateBooleanExpression(action.IsDraft, repository); // not oke + bool includeWorkItems = _expressionEvaluator.EvaluateBooleanExpression(action.IncludeWorkItems, repository); // not oke + bool openInBrowser = _expressionEvaluator.EvaluateBooleanExpression(action.OpenInBrowser, repository); // not oke + + return new List() + { + new(action.Name ?? $"Create Pull Request {(hasAutoComplete ? "(with auto-complete)" : string.Empty)}", repository) + { + Action = new DelegateAction((_, _) => + { + if (hasAutoComplete) + { + _service.CreatePullRequestWithAutoCompleteAsync(repository, projectId, action.ReviewerIds, action.ToBranch, (int)action.AutoComplete.MergeStrategy, action.PrTitle, isDraft, includeWorkItems, openInBrowser, action.AutoComplete.DeleteSourceBranch, action.AutoComplete.TransitionWorkItems).GetAwaiter().GetResult(); + } + else + { + _service.CreatePullRequestAsync(repository, projectId, action.ReviewerIds, action.ToBranch, action.PrTitle, isDraft, isDraft, openInBrowser).GetAwaiter().GetResult(); + } + }), + }, + }; + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.cs b/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.cs new file mode 100644 index 00000000..773630ff --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.cs @@ -0,0 +1,29 @@ +namespace RepoM.Plugin.AzureDevOps.ActionProvider.Options; + +/// +/// Merge strategies Azure Devops supports. +/// +public enum RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy +{ + /// + /// A two-parent, no-fast-forward merge. The source branch is unchanged. This is the default behavior. + /// + NoFastForward = 1, + + /// + /// Put all changes from the pull request into a single-parent commit. + /// + Squash, + + /// + /// Rebase the source branch on top of the target branch HEAD commit, and fast-forward the target branch. + /// The source branch is updated during the rebase operation. + /// + Rebase, + + /// + /// Rebase the source branch on top of the target branch HEAD commit, and create a two-parent, + /// no-fast-forward merge. The source branch is updated during the rebase operation. + /// + RebaseMerge, +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV1.cs b/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV1.cs index 5629148d..c832cb9d 100644 --- a/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV1.cs +++ b/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV1.cs @@ -109,9 +109,9 @@ public class RepositoryActionAzureDevOpsCreatePullRequestsAutoCompleteOptionsV1 /// The merge strategy. Possible values are `NoFastForward`, `Squash` and `Rebase`, and `RebaseMerge`. /// [Required] - [PropertyType(typeof(RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategyV1))] - [PropertyDefaultTypedValueAttribute(RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategyV1.NoFastForward)] - public RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategyV1 MergeStrategy { get; set; } = RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategyV1.NoFastForward; + [PropertyType(typeof(RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy))] + [PropertyDefaultTypedValueAttribute(RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.NoFastForward)] + public RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy MergeStrategy { get; set; } = RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.NoFastForward; /// /// Boolean specifying if the source branche should be deleted afer completion. @@ -128,29 +128,4 @@ public class RepositoryActionAzureDevOpsCreatePullRequestsAutoCompleteOptionsV1 [PropertyType(typeof(bool))] [PropertyDefaultBoolValue(true)] public bool TransitionWorkItems { get; set; } = true; -} - -public enum RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategyV1 -{ - /// - /// A two-parent, no-fast-forward merge. The source branch is unchanged. This is the default behavior. - /// - NoFastForward = 1, - - /// - /// Put all changes from the pull request into a single-parent commit. - /// - Squash, - - /// - /// Rebase the source branch on top of the target branch HEAD commit, and fast-forward the target branch. - /// The source branch is updated during the rebase operation. - /// - Rebase, - - /// - /// Rebase the source branch on top of the target branch HEAD commit, and create a two-parent, - /// no-fast-forward merge. The source branch is updated during the rebase operation. - /// - RebaseMerge, } \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV2.cs b/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV2.cs new file mode 100644 index 00000000..16416932 --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/ActionProvider/Options/RepositoryActionAzureDevOpsCreatePullRequestsV2.cs @@ -0,0 +1,116 @@ +namespace RepoM.Plugin.AzureDevOps.ActionProvider.Options; + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data; + +/// +/// Action menu item to create a pull request in Azure Devops. +/// +[RepositoryAction(TYPE)] +public sealed class RepositoryActionAzureDevOpsCreatePullRequestsV2 : RepositoryAction +{ + /// + /// RepositoryAction type. + /// + public const string TYPE = "azure-devops-create-prs@2"; + + /// + /// The azure devops project id. If none given, the default project id, set in the plugin configuration, will be used. At least one is required. + /// + [EvaluatedProperty] + [PropertyType(typeof(string))] + public string? ProjectId { get; set; } + + /// + /// Pull Request title. When not provided, the title will be defined based on the branch name. + /// Title will be the last part of the branchname split on `/`, so `feature/123-testBranch` will result in title `123-testBranch` + /// + [EvaluatedProperty] + [PropertyType(typeof(string))] + public string? Title { get; set; } + + /// + /// Name of the branch the pull request should be merged into. For instance `develop`, or `main`. + /// + [Required] + [EvaluatedProperty] + [PropertyType(typeof(string))] + public string? ToBranch { get; set; } = string.Empty; + + /// + /// Required reviewers. An id should be a valid Azure DevOps user id (ie. GUID), or email address known in Azure Devops. + /// + [EvaluatedProperty] + [PropertyType(typeof(string))] + public string? Reviewer { get; set; } + + /// + /// List of required reviewers. An id should be a valid Azure DevOps user id (ie. GUID), or email address known in Azure Devops. + /// + [EvaluatedProperty] + [PropertyType(typeof(List))] + public List Reviewers { get; set; } = new(); + + /// + /// Boolean specifying if th PR should be marked as draft. + /// + [Required] + [EvaluatedProperty] + [PropertyType(typeof(bool))] + [PropertyDefaultBoolValue(false)] + public string? IsDraft { get; set; } = "false"; + + /// + /// Boolean specifying if workitems should be included in the PR. The workitems will be found by using the commit messages. + /// + [Required] + [EvaluatedProperty] + [PropertyType(typeof(bool))] + [PropertyDefaultBoolValue(true)] + public string? IncludeWorkItems { get; set; } = "true"; + + /// + /// Boolean specifying if the Pull request should be opened in the browser after creation. + /// + [Required] + [EvaluatedProperty] + [PropertyType(typeof(bool))] + [PropertyDefaultBoolValue(default)] + public string? OpenInBrowser { get; set; } + + /// + /// Auto complete options for the pull request. If not set, autocomplete will be `off`. + /// + [PropertyType(typeof(RepositoryActionAzureDevOpsCreatePullRequestsAutoCompleteOptionsV2))] + public RepositoryActionAzureDevOpsCreatePullRequestsAutoCompleteOptionsV2? AutoComplete { get; set; } +} + +/// +/// Auto complete options. +/// +public class RepositoryActionAzureDevOpsCreatePullRequestsAutoCompleteOptionsV2 +{ + /// + /// The merge strategy. Possible values are `NoFastForward`, `Squash` and `Rebase`, and `RebaseMerge`. + /// + [PropertyType(typeof(RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy))] + [PropertyDefaultTypedValueAttribute(RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.NoFastForward)] + public RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy MergeStrategy { get; set; } = RepositoryActionAzureDevOpsCreatePullRequestsMergeStrategy.NoFastForward; + + /// + /// Boolean specifying if the source branche should be deleted afer completion. + /// + [EvaluatedProperty] + [PropertyType(typeof(bool))] + [PropertyDefaultBoolValue(true)] + public string? DeleteSourceBranch { get; set; } = "true"; + + /// + /// Boolean specifying if related workitems should be transitioned to the next state. + /// + [EvaluatedProperty] + [PropertyType(typeof(bool))] + [PropertyDefaultBoolValue(true)] + public string? TransitionWorkItems { get; set; } = "true"; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs b/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs index ff7fb48d..4bc84440 100644 --- a/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs +++ b/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs @@ -1,5 +1,6 @@ namespace RepoM.Plugin.AzureDevOps; +using System; using System.Threading.Tasks; using JetBrains.Annotations; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; @@ -28,15 +29,27 @@ private static async Task ExtractAndRegisterConfiguration(Container container, I { var version = await packageConfiguration.GetConfigurationVersionAsync().ConfigureAwait(false); - AzureDevopsConfigV1? config = null; - if (version == CurrentConfigVersion.VERSION) + AzureDevopsConfigV2? config = null!; + + if (version == AzureDevopsConfigV1.VERSION) + { + AzureDevopsConfigV1? configV1 = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); + config = ConvertV1ToV2(configV1); + await packageConfiguration.PersistConfigurationAsync(config, AzureDevopsConfigV2.VERSION).ConfigureAwait(false); + } + else if (version == AzureDevopsConfigV2.VERSION) { - config = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); + config = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); } config ??= await PersistDefaultConfigAsync(packageConfiguration).ConfigureAwait(false); - container.RegisterInstance(new AzureDevopsConfiguration(config.BaseUrl, config.PersonalAccessToken)); + container.RegisterInstance(new AzureDevopsConfiguration( + config.BaseUrl, + config.PersonalAccessToken, + config.DefaultProjectId, + config.IntervalUpdateProjects ?? TimeSpan.FromMinutes(10), + config.IntervalUpdatePullRequests ?? TimeSpan.FromMinutes(4))); } private static void RegisterServices(Container container) @@ -54,10 +67,35 @@ private static void RegisterServices(Container container) } /// This method is used by reflection to generate documentation file> - private static async Task PersistDefaultConfigAsync(IPackageConfiguration packageConfiguration) + private static async Task PersistDefaultConfigAsync(IPackageConfiguration packageConfiguration) { - var config = new AzureDevopsConfigV1(); + var config = CreateDefaultAzureDevopsConfigV2(); await packageConfiguration.PersistConfigurationAsync(config, CurrentConfigVersion.VERSION).ConfigureAwait(false); return config; } + + private static AzureDevopsConfigV2 CreateDefaultAzureDevopsConfigV2() + { + return new AzureDevopsConfigV2 + { + BaseUrl = null, + PersonalAccessToken = null, + DefaultProjectId = null, + IntervalUpdateProjects = TimeSpan.FromMinutes(10), + IntervalUpdatePullRequests = TimeSpan.FromMinutes(4), + }; + } + + private static AzureDevopsConfigV2 ConvertV1ToV2(AzureDevopsConfigV1? configV1) + { + AzureDevopsConfigV2 defaultConfig = CreateDefaultAzureDevopsConfigV2(); + return new AzureDevopsConfigV2 + { + BaseUrl = configV1?.BaseUrl, + PersonalAccessToken = configV1?.PersonalAccessToken, + DefaultProjectId = defaultConfig.DefaultProjectId, + IntervalUpdateProjects = defaultConfig.IntervalUpdateProjects, + IntervalUpdatePullRequests = defaultConfig.IntervalUpdatePullRequests, + }; + } } \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs index 97d0c9ea..b1ff6eb4 100644 --- a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs +++ b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs @@ -80,8 +80,8 @@ public Task InitializeAsync() return Task.CompletedTask; } - _updateTimer1 = new Timer(async _ => await UpdatePullRequests(_azureDevopsGitClient), null, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(4)); - _updateTimer2 = new Timer(async _ => await UpdateProjectsAsync(_azureDevopsGitClient), null, TimeSpan.FromSeconds(7), TimeSpan.FromMinutes(10)); + _updateTimer1 = new Timer(async _ => await UpdatePullRequests(_azureDevopsGitClient), null, TimeSpan.FromSeconds(5), _configuration.IntervalUpdatePullRequests); + _updateTimer2 = new Timer(async _ => await UpdateProjectsAsync(_azureDevopsGitClient), null, TimeSpan.FromSeconds(7), _configuration.IntervalUpdateProjects); return Task.CompletedTask; } diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs index 445ac513..aa69ba41 100644 --- a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs +++ b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs @@ -4,9 +4,12 @@ namespace RepoM.Plugin.AzureDevOps.Internal; internal class AzureDevopsConfiguration : IAzureDevopsConfiguration { - public AzureDevopsConfiguration(string? url, string? pat) + public AzureDevopsConfiguration(string? url, string? pat, string? defaultProjectId, TimeSpan intervalUpdateProjects, TimeSpan intervalUpdatePullRequests) { AzureDevOpsPersonalAccessToken = pat; + DefaultProjectId = defaultProjectId; + IntervalUpdateProjects = intervalUpdateProjects; + IntervalUpdatePullRequests = intervalUpdatePullRequests; try { @@ -21,4 +24,11 @@ public AzureDevopsConfiguration(string? url, string? pat) public string? AzureDevOpsPersonalAccessToken { get; } public Uri? AzureDevOpsBaseUrl { get; } + + public string? DefaultProjectId { get; } + + public TimeSpan IntervalUpdateProjects { get; } + + public TimeSpan IntervalUpdatePullRequests { get; } + } \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs b/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs index 6e0e3001..f0b6c0ac 100644 --- a/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs +++ b/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs @@ -7,4 +7,10 @@ internal interface IAzureDevopsConfiguration string? AzureDevOpsPersonalAccessToken { get; } Uri? AzureDevOpsBaseUrl { get; } + + string? DefaultProjectId { get; } + + TimeSpan IntervalUpdateProjects { get; } + + TimeSpan IntervalUpdatePullRequests { get; } } \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/WorkItemExtractor.cs b/src/RepoM.Plugin.AzureDevOps/Internal/WorkItemExtractor.cs index ddd3e03d..71778c5d 100644 --- a/src/RepoM.Plugin.AzureDevOps/Internal/WorkItemExtractor.cs +++ b/src/RepoM.Plugin.AzureDevOps/Internal/WorkItemExtractor.cs @@ -12,7 +12,7 @@ public static string[] GetDistinctWorkItemsFromCommitMessages(IEnumerable results = new(); - foreach (string commitMessage in commitMessages) + foreach (var commitMessage in commitMessages) { MatchCollection matches = _workItemRegex.Matches(commitMessage); if (matches.Any(m => m.Success)) diff --git a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs index 36ec03c5..8e1f3798 100644 --- a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs +++ b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs @@ -1,9 +1,14 @@ namespace RepoM.Plugin.AzureDevOps.PersistentConfiguration; +using System; + /// DO NOT CHANGE PROPERTYNAMES, TYPES, or VISIBILITIES /// Module configuration (version 1) +[Obsolete("Use Version V2. This version is supported until 2023-08-17 and will be removed after.")] public class AzureDevopsConfigV1 { + internal const int VERSION = 1; + /// /// Personal access token (PAT) to access Azure Devops. The PAT should be granted access to `todo` rights. /// To create a PAT, goto `https://dev.azure.com/[my-organisation]/_usersSettings/tokens`. diff --git a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV2.cs b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV2.cs new file mode 100644 index 00000000..4bc24fc2 --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV2.cs @@ -0,0 +1,36 @@ +namespace RepoM.Plugin.AzureDevOps.PersistentConfiguration; + +using System; + +/// DO NOT CHANGE PROPERTYNAMES, TYPES, or VISIBILITIES +/// Module configuration (version 2) +public class AzureDevopsConfigV2 +{ + internal const int VERSION = 2; + + /// + /// Personal access token (PAT) to access Azure Devops. The PAT should be granted access to `todo` rights. + /// To create a PAT, goto `https://dev.azure.com/[my-organisation]/_usersSettings/tokens`. + /// + public string? PersonalAccessToken { get; init; } + + /// + /// The base url of azure devops for your organisation (ie. `https://dev.azure.com/[my-organisation]/`). + /// + public string? BaseUrl { get; init; } + + /// + /// Default project id to use when no project id provided in the repository action. Should be a GUID. + /// + public string? DefaultProjectId { get; init; } + + /// + /// Interval RepoM should update the list of open pull requests from Azure DevOps. Defaults to `4` minutes (ie. `00:04:00`). + /// + public TimeSpan? IntervalUpdatePullRequests { get; init; } + + /// + /// Interval RepoM should update the list of projects from Azure DevOps. Defaults to `10` minutes (ie. `00:10:00`). + /// + public TimeSpan? IntervalUpdateProjects { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs index b2cbd5f7..2cd1d332 100644 --- a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs +++ b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs @@ -2,5 +2,5 @@ namespace RepoM.Plugin.AzureDevOps.PersistentConfiguration; internal static class CurrentConfigVersion { - public const int VERSION = 1; + public const int VERSION = AzureDevopsConfigV2.VERSION; } \ No newline at end of file diff --git a/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs b/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs index 84e17e10..b67badcd 100644 --- a/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs +++ b/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs @@ -31,9 +31,20 @@ public AzureDevOpsPackageTests() PersonalAccessToken = "PAT", BaseUrl = "https://dev.azure.com/MyOrg", }; + var azureDevopsConfigV2 = new AzureDevopsConfigV2 + { + PersonalAccessToken = "PAT", + BaseUrl = "https://dev.azure.com/MyOrg", + DefaultProjectId = "xx", + IntervalUpdateProjects = TimeSpan.FromMinutes(34), + IntervalUpdatePullRequests = TimeSpan.FromMinutes(4), + }; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(1 as int?)); A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).ReturnsLazily(() => azureDevopsConfigV1); + A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).ReturnsLazily(() => azureDevopsConfigV2); A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).Returns(Task.CompletedTask); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 2)).Returns(Task.CompletedTask); } [Fact] @@ -53,7 +64,7 @@ public async Task RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAr [Theory] [InlineData(null)] - [InlineData(2)] + [InlineData(3)] [InlineData(10)] public async Task RegisterServices_ShouldPersistNewConfig_WhenVersionIsNotCorrect(int? version) { @@ -66,41 +77,43 @@ public async Task RegisterServices_ShouldPersistNewConfig_WhenVersionIsNotCorrec await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, AzureDevopsConfigV2.VERSION)).MustHaveHappenedOnceExactly(); // implicit, Verify throws when container is not valid. _container.Verify(VerificationOption.VerifyAndDiagnose); } - + [Fact] - public async Task RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() + public async Task RegisterServices_ShouldConvertAndPersistNewConfig_WhenConfigIsVersion1() { // arrange + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(AzureDevopsConfigV1.VERSION as int?)); + RegisterExternals(_container); var sut = new AzureDevOpsPackage(); // act await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - Assert.Throws(() => _container.Verify(VerificationOption.VerifyAndDiagnose)); + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).MustHaveHappenedOnceExactly() + .Then(A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).MustHaveHappened()) + .Then(A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, AzureDevopsConfigV2.VERSION)).MustHaveHappened()); + + // implicit, Verify throws when container is not valid. + _container.Verify(VerificationOption.VerifyAndDiagnose); } - [Theory] - [InlineData(null)] - [InlineData(2)] - [InlineData(10)] - public async Task RegisterServices_ShouldPersistConfigWhenNotCorrectVersion(int? version) + [Fact] + public async Task RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() { // arrange - A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); - RegisterExternals(_container); var sut = new AzureDevOpsPackage(); - + // act await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + Assert.Throws(() => _container.Verify(VerificationOption.VerifyAndDiagnose)); } private void RegisterExternals(Container container) diff --git a/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage#desc.verified.md b/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage#desc.verified.md index 5df28510..04f020ed 100644 --- a/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage#desc.verified.md +++ b/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage#desc.verified.md @@ -6,10 +6,13 @@ The following default configuration is used: ```json { - "Version": 1, + "Version": 2, "Settings": { "PersonalAccessToken": null, - "BaseUrl": null + "BaseUrl": null, + "DefaultProjectId": null, + "IntervalUpdatePullRequests": "00:04:00", + "IntervalUpdateProjects": "00:10:00" } } ``` @@ -19,3 +22,6 @@ Properties: - `PersonalAccessToken`: Personal access token (PAT) to access Azure Devops. The PAT should be granted access to `todo` rights. To create a PAT, goto `https://dev.azure.com/[my-organisation]/_usersSettings/tokens`. - `BaseUrl`: The base url of azure devops for your organisation (ie. `https://dev.azure.com/[my-organisation]/`). +- `DefaultProjectId`: Default project id to use when no project id provided in the repository action. Should be a GUID. +- `IntervalUpdatePullRequests`: Interval RepoM should update the list of open pull requests from Azure DevOps. Defaults to `4` minutes (ie. `00:04:00`). +- `IntervalUpdateProjects`: Interval RepoM should update the list of projects from Azure DevOps. Defaults to `10` minutes (ie. `00:10:00`). diff --git a/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage.verified.json b/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage.verified.json index 5566f298..f688176a 100644 --- a/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage.verified.json +++ b/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.DocsModuleSettings_AzureDevOpsPackage.verified.json @@ -1,7 +1,10 @@ { - "Version": 1, + "Version": 2, "Settings": { "PersonalAccessToken": null, - "BaseUrl": null + "BaseUrl": null, + "DefaultProjectId": null, + "IntervalUpdatePullRequests": "00:04:00", + "IntervalUpdateProjects": "00:10:00" } } \ No newline at end of file diff --git a/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.VerifyChanges.verified.txt b/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.VerifyChanges.verified.txt index 2552127b..457d4325 100644 --- a/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.VerifyChanges.verified.txt +++ b/tests/RepoM.Plugin.Misc.Tests/Configuration/ModuleSettingsDocs/DocsModuleSettingsTests.VerifyChanges.verified.txt @@ -1,6 +1,8 @@ { AzureDevOpsPackage: { - $type: AzureDevopsConfigV1 + $type: AzureDevopsConfigV2, + IntervalUpdatePullRequests: 00:04:00, + IntervalUpdateProjects: 00:10:00 }, ClipboardPackage: null, EverythingPackage: null,