public async Task <IEnumerable <Subscription> > FilterSubscriptions(IRemote remote) { IEnumerable <DefaultChannel> defaultChannels = await remote.GetDefaultChannelsAsync(); return((await remote.GetSubscriptionsAsync()).Where(subscription => { return SubcriptionFilter(subscription, defaultChannels); })); }
/// <summary> /// Evaluate the metric. /// </summary> /// <param name="remoteFactory">Remote factory</param> /// <returns>True if the metric passed, false otherwise</returns> public override async Task EvaluateAsync() { IRemote remote = await RemoteFactory.GetRemoteAsync(Repository, Logger); Logger.LogInformation("Evaluating subscription health metrics for {repo}@{branch}", Repository, Branch); // Get subscriptions that target this repo/branch Subscriptions = (await remote.GetSubscriptionsAsync(targetRepo: Repository)) .Where(s => s.TargetBranch.Equals(Branch, StringComparison.OrdinalIgnoreCase)).ToList(); // Get the dependencies of the repository/branch. Skip pinned and subscriptions tied to another // dependency (coherent parent), as well as those not selected by the dependency selector. try { Dependencies = (await remote.GetDependenciesAsync(Repository, Branch)) .Where(d => !d.Pinned && string.IsNullOrEmpty(d.CoherentParentDependencyName)) .Where(d => DependencySelector(d)) .ToList(); } catch (DependencyFileNotFoundException) { // When the dependency file is not found, then we're good as long as this repo is not // targeted by any subscriptions if (Subscriptions.Any()) { MissingVersionDetailsFile = true; Result = HealthResult.Failed; return; } else { Result = HealthResult.Passed; return; } } Dictionary <string, Subscription> latestAssets = await GetLatestAssetsAndComputeConflicts(remote); ComputeSubscriptionUse(latestAssets); // Determine the result. A conflict or missing subscription is an error. // A non-flowing subscription or unused subscription is a warning if (DependenciesMissingSubscriptions.Any() || ConflictingSubscriptions.Any()) { Result = HealthResult.Failed; } else if (UnusedSubscriptions.Any() || DependenciesThatDoNotFlow.Any()) { Result = HealthResult.Warning; } else { Result = HealthResult.Passed; } }
public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); IEnumerable <RepositoryBranch> allRepositories = await remote.GetRepositoriesAsync(); IEnumerable <RepositoryBranch> filteredRepositories = allRepositories.Where(repositories => (string.IsNullOrEmpty(_options.Repo) || repositories.Repository.Contains(_options.Repo, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrEmpty(_options.Branch) || repositories.Branch.Contains(_options.Branch, StringComparison.OrdinalIgnoreCase))); // List only those repos and branches that are targeted by a batchable subscription (active) unless the user // passes --all. if (!_options.All) { HashSet <string> batchableTargets = (await remote.GetSubscriptionsAsync()) .Where(s => s.Policy.Batchable) .Select <Subscription, string>(s => $"{s.TargetRepository}{s.TargetBranch}") .ToHashSet(StringComparer.OrdinalIgnoreCase); var targetedRepositories = filteredRepositories.Where(r => batchableTargets.Contains($"{r.Repository}{r.Branch}")); // If the number of repositories we're about to print is less than what we could have printed, then print a // message. int difference = filteredRepositories.Count() - targetedRepositories.Count(); if (difference != 0) { Console.WriteLine($"Filtered {difference} policies for branches not targeted by an active batchable subscription. To include, pass --all.{Environment.NewLine}"); } filteredRepositories = targetedRepositories; } foreach (var repository in filteredRepositories) { Console.WriteLine($"{repository.Repository} @ {repository.Branch}"); Console.Write(UxHelpers.GetMergePoliciesDescription(repository.MergePolicies)); } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve repositories"); return(Constants.ErrorCode); } }
public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); var subscriptions = (await remote.GetSubscriptionsAsync()).Where(subscription => { return(_options.SubcriptionFilter(subscription)); }); if (subscriptions.Count() == 0) { Console.WriteLine("No subscriptions found matching the specified criteria."); return(Constants.ErrorCode); } // Based on the current output schema, sort by source repo, target repo, target branch, etc. // Concat the input strings as a simple sorting mechanism. foreach (var subscription in subscriptions.OrderBy(subscription => $"{subscription.SourceRepository}{subscription.Channel}{subscription.TargetRepository}{subscription.TargetBranch}")) { Console.WriteLine($"{subscription.SourceRepository} ({subscription.Channel.Name}) ==> '{subscription.TargetRepository}' ('{subscription.TargetBranch}')"); Console.WriteLine($" - Id: {subscription.Id}"); Console.WriteLine($" - Update Frequency: {subscription.Policy.UpdateFrequency}"); Console.WriteLine($" - Enabled: {subscription.Enabled}"); Console.WriteLine($" - Batchable: {subscription.Policy.Batchable}"); // If batchable, the merge policies come from the repository IEnumerable <MergePolicy> mergePolicies = subscription.Policy.MergePolicies; if (subscription.Policy.Batchable == true) { mergePolicies = await remote.GetRepositoryMergePoliciesAsync(subscription.TargetRepository, subscription.TargetBranch); } Console.Write(UxHelpers.GetMergePoliciesDescription(mergePolicies, " ")); // Currently the API only returns the last applied build for requests to specific subscriptions. // This will be fixed, but for now, don't print the last applied build otherwise. if (subscription.LastAppliedBuild != null) { Console.WriteLine($" - Last Build: {subscription.LastAppliedBuild.AzureDevOpsBuildNumber} ({subscription.LastAppliedBuild.Commit})"); } } return(Constants.SuccessCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve subscriptions"); return(Constants.ErrorCode); } }
/// <summary> /// Assigns a build to a channel. /// </summary> /// <returns>Process exit code.</returns> public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); // Find the build to give someone info Build build = await remote.GetBuildAsync(_options.Id); if (build == null) { Console.WriteLine($"Could not find a build with id '{_options.Id}'"); return(Constants.ErrorCode); } Channel targetChannel = await UxHelpers.ResolveSingleChannel(remote, _options.Channel); if (targetChannel == null) { return(Constants.ErrorCode); } if (build.Channels.Any(c => c.Id == targetChannel.Id)) { Console.WriteLine($"Build '{build.Id}' has already been assigned to '{targetChannel.Name}'"); return(Constants.SuccessCode); } Console.WriteLine($"Assigning the following build to channel '{targetChannel.Name}':"); Console.WriteLine(); OutputHelpers.PrintBuild(build); await remote.AssignBuildToChannel(_options.Id, targetChannel.Id); // Be helpful. Let the user know what will happen. string buildRepo = build.GitHubRepository ?? build.AzureDevOpsRepository; List <Subscription> applicableSubscriptions = (await remote.GetSubscriptionsAsync( sourceRepo: buildRepo, channelId: targetChannel.Id)).ToList(); PrintSubscriptionInfo(applicableSubscriptions); return(Constants.SuccessCode); } catch (Exception e) { Logger.LogError(e, $"Error: Failed to assign build '{_options.Id}' to channel '{_options.Channel}'."); return(Constants.ErrorCode); } }
public async Task UpdateLongestBuildPathAsync(CancellationToken cancellationToken) { using (Logger.BeginScope($"Updating Longest Build Path table")) { List <Channel> channels = Context.Channels.Select(c => new Channel() { Id = c.Id, Name = c.Name }).ToList(); // Get the flow graph IRemote barOnlyRemote = await RemoteFactory.GetBarOnlyRemoteAsync(Logger); List <Microsoft.DotNet.Maestro.Client.Models.DefaultChannel> defaultChannels = (await barOnlyRemote.GetDefaultChannelsAsync()).ToList(); List <Microsoft.DotNet.Maestro.Client.Models.Subscription> subscriptions = (await barOnlyRemote.GetSubscriptionsAsync()).ToList(); IEnumerable <string> frequencies = new[] { "everyWeek", "twiceDaily", "everyDay", "everyBuild", "none", }; Logger.LogInformation($"Will update '{channels.Count}' channels"); foreach (var channel in channels) { // Build, then prune out what we don't want to see if the user specified channels. DependencyFlowGraph flowGraph = await DependencyFlowGraph.BuildAsync(defaultChannels, subscriptions, barOnlyRemote, 30); flowGraph.PruneGraph( node => DependencyFlowGraph.IsInterestingNode(channel.Name, node), edge => DependencyFlowGraph.IsInterestingEdge(edge, false, frequencies)); if (flowGraph.Nodes.Count > 0) { flowGraph.MarkBackEdges(); flowGraph.CalculateLongestBuildPaths(); flowGraph.MarkLongestBuildPath(); // Get the nodes on the longest path and order them by path time so that the // contributing repos are in the right order List <DependencyFlowNode> longestBuildPathNodes = flowGraph.Nodes .Where(n => n.OnLongestBuildPath) .OrderByDescending(n => n.BestCasePathTime) .ToList(); LongestBuildPath lbp = new LongestBuildPath() { ChannelId = channel.Id, BestCaseTimeInMinutes = longestBuildPathNodes.Max(n => n.BestCasePathTime), WorstCaseTimeInMinutes = longestBuildPathNodes.Max(n => n.WorstCasePathTime), ContributingRepositories = String.Join(';', longestBuildPathNodes.Select(n => $"{n.Repository}@{n.Branch}").ToArray()), ReportDate = DateTimeOffset.UtcNow, }; Logger.LogInformation($"Will update {channel.Name} to best case time {lbp.BestCaseTimeInMinutes} and worst case time {lbp.WorstCaseTimeInMinutes}"); await Context.LongestBuildPaths.AddAsync(lbp); } } await Context.SaveChangesAsync(); } }
/// <summary> /// Implements the 'add-subscription' operation /// </summary> /// <param name="options"></param> public override async Task <int> ExecuteAsync() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); if (_options.IgnoreChecks.Count() > 0 && !_options.AllChecksSuccessfulMergePolicy) { Console.WriteLine($"--ignore-checks must be combined with --all-checks-passed"); return(Constants.ErrorCode); } // Parse the merge policies List <MergePolicy> mergePolicies = new List <MergePolicy>(); if (_options.NoExtraCommitsMergePolicy) { mergePolicies.Add( new MergePolicy { Name = MergePolicyConstants.NoExtraCommitsMergePolicyName }); } if (_options.AllChecksSuccessfulMergePolicy) { mergePolicies.Add( new MergePolicy { Name = MergePolicyConstants.AllCheckSuccessfulMergePolicyName, Properties = ImmutableDictionary.Create <string, JToken>() .Add(MergePolicyConstants.IgnoreChecksMergePolicyPropertyName, JToken.FromObject(_options.IgnoreChecks)) }); } if (_options.NoRequestedChangesMergePolicy) { mergePolicies.Add( new MergePolicy { Name = MergePolicyConstants.NoRequestedChangesMergePolicyName, Properties = ImmutableDictionary.Create <string, JToken>() }); } if (_options.StandardAutoMergePolicies) { mergePolicies.Add( new MergePolicy { Name = MergePolicyConstants.StandardMergePolicyName, Properties = ImmutableDictionary.Create <string, JToken>() }); } if (_options.Batchable && mergePolicies.Count > 0) { Console.WriteLine("Batchable subscriptions cannot be combined with merge policies. " + "Merge policies are specified at a repository+branch level."); return(Constants.ErrorCode); } string channel = _options.Channel; string sourceRepository = _options.SourceRepository; string targetRepository = _options.TargetRepository; string targetBranch = GitHelpers.NormalizeBranchName(_options.TargetBranch); string updateFrequency = _options.UpdateFrequency; bool batchable = _options.Batchable; // If in quiet (non-interactive mode), ensure that all options were passed, then // just call the remote API if (_options.Quiet && !_options.ReadStandardIn) { if (string.IsNullOrEmpty(channel) || string.IsNullOrEmpty(sourceRepository) || string.IsNullOrEmpty(targetRepository) || string.IsNullOrEmpty(targetBranch) || string.IsNullOrEmpty(updateFrequency) || !Constants.AvailableFrequencies.Contains(updateFrequency, StringComparer.OrdinalIgnoreCase)) { Logger.LogError($"Missing input parameters for the subscription. Please see command help or remove --quiet/-q for interactive mode"); return(Constants.ErrorCode); } } else { // Grab existing subscriptions to get suggested values. // TODO: When this becomes paged, set a max number of results to avoid // pulling too much. var suggestedRepos = remote.GetSubscriptionsAsync(); var suggestedChannels = remote.GetChannelsAsync(); // Help the user along with a form. We'll use the API to gather suggested values // from existing subscriptions based on the input parameters. AddSubscriptionPopUp addSubscriptionPopup = new AddSubscriptionPopUp("add-subscription/add-subscription-todo", Logger, channel, sourceRepository, targetRepository, targetBranch, updateFrequency, batchable, mergePolicies, (await suggestedChannels).Select(suggestedChannel => suggestedChannel.Name), (await suggestedRepos).SelectMany(subscription => new List <string> { subscription.SourceRepository, subscription.TargetRepository }).ToHashSet(), Constants.AvailableFrequencies, Constants.AvailableMergePolicyYamlHelp); UxManager uxManager = new UxManager(_options.GitLocation, Logger); int exitCode = _options.ReadStandardIn ? uxManager.ReadFromStdIn(addSubscriptionPopup) : uxManager.PopUp(addSubscriptionPopup); if (exitCode != Constants.SuccessCode) { return(exitCode); } channel = addSubscriptionPopup.Channel; sourceRepository = addSubscriptionPopup.SourceRepository; targetRepository = addSubscriptionPopup.TargetRepository; targetBranch = addSubscriptionPopup.TargetBranch; updateFrequency = addSubscriptionPopup.UpdateFrequency; mergePolicies = addSubscriptionPopup.MergePolicies; batchable = addSubscriptionPopup.Batchable; } try { // If we are about to add a batchable subscription and the merge policies are empty for the // target repo/branch, warn the user. if (batchable) { var existingMergePolicies = await remote.GetRepositoryMergePoliciesAsync(targetRepository, targetBranch); if (!existingMergePolicies.Any()) { Console.WriteLine("Warning: Batchable subscription doesn't have any repository merge policies. " + "PRs will not be auto-merged."); Console.WriteLine($"Please use 'darc set-repository-policies --repo {targetRepository} --branch {targetBranch}' " + $"to set policies.{Environment.NewLine}"); } } // Verify the target IRemote targetVerifyRemote = RemoteFactory.GetRemote(_options, targetRepository, Logger); if (!(await UxHelpers.VerifyAndConfirmBranchExistsAsync(targetVerifyRemote, targetRepository, targetBranch, !_options.Quiet))) { Console.WriteLine("Aborting subscription creation."); return(Constants.ErrorCode); } // Verify the source. IRemote sourceVerifyRemote = RemoteFactory.GetRemote(_options, sourceRepository, Logger); if (!(await UxHelpers.VerifyAndConfirmRepositoryExistsAsync(sourceVerifyRemote, sourceRepository, !_options.Quiet))) { Console.WriteLine("Aborting subscription creation."); return(Constants.ErrorCode); } var newSubscription = await remote.CreateSubscriptionAsync(channel, sourceRepository, targetRepository, targetBranch, updateFrequency, batchable, mergePolicies); Console.WriteLine($"Successfully created new subscription with id '{newSubscription.Id}'."); // Prompt the user to trigger the subscription unless they have explicitly disallowed it if (!_options.NoTriggerOnCreate) { bool triggerAutomatically = _options.TriggerOnCreate || UxHelpers.PromptForYesNo("Trigger this subscription immediately?"); if (triggerAutomatically) { await remote.TriggerSubscriptionAsync(newSubscription.Id.ToString()); Console.WriteLine($"Subscription '{newSubscription.Id}' triggered."); } } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (RestApiException e) when(e.Response.Status == (int)System.Net.HttpStatusCode.BadRequest) { // Could have been some kind of validation error (e.g. channel doesn't exist) Logger.LogError($"Failed to create subscription: {e.Response.Content}"); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Failed to create subscription."); return(Constants.ErrorCode); } }
/// <summary> /// Implements the 'update-subscription' operation /// </summary> /// <param name="options"></param> public override async Task <int> ExecuteAsync() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); // First, try to get the subscription. If it doesn't exist the call will throw and the exception will be // caught by `RunOperation` Subscription subscription = await remote.GetSubscriptionAsync(_options.Id); var suggestedRepos = remote.GetSubscriptionsAsync(); var suggestedChannels = remote.GetChannelsAsync(); UpdateSubscriptionPopUp updateSubscriptionPopUp = new UpdateSubscriptionPopUp( "update-subscription/update-subscription-todo", Logger, subscription, (await suggestedChannels).Select(suggestedChannel => suggestedChannel.Name), (await suggestedRepos).SelectMany(subs => new List <string> { subscription.SourceRepository, subscription.TargetRepository }).ToHashSet(), Constants.AvailableFrequencies, Constants.AvailableMergePolicyYamlHelp); UxManager uxManager = new UxManager(Logger); int exitCode = uxManager.PopUp(updateSubscriptionPopUp); if (exitCode != Constants.SuccessCode) { return(exitCode); } string channel = updateSubscriptionPopUp.Channel; string sourceRepository = updateSubscriptionPopUp.SourceRepository; string updateFrequency = updateSubscriptionPopUp.UpdateFrequency; bool batchable = updateSubscriptionPopUp.Batchable; bool enabled = updateSubscriptionPopUp.Enabled; List <MergePolicy> mergePolicies = updateSubscriptionPopUp.MergePolicies; try { SubscriptionUpdate subscriptionToUpdate = new SubscriptionUpdate { ChannelName = channel ?? subscription.Channel.Name, SourceRepository = sourceRepository ?? subscription.SourceRepository, Enabled = enabled, Policy = subscription.Policy, }; subscriptionToUpdate.Policy.Batchable = batchable; subscriptionToUpdate.Policy.UpdateFrequency = Enum.Parse <UpdateFrequency>(updateFrequency); subscriptionToUpdate.Policy.MergePolicies = mergePolicies?.ToImmutableList(); var updatedSubscription = await remote.UpdateSubscriptionAsync( _options.Id, subscriptionToUpdate); Console.WriteLine($"Successfully updated subscription with id '{updatedSubscription.Id}'."); return(Constants.SuccessCode); } catch (RestApiException e) when(e.Response.StatusCode == System.Net.HttpStatusCode.BadRequest) { // Could have been some kind of validation error (e.g. channel doesn't exist) Logger.LogError($"Failed to update subscription: {e.Response.Content}"); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Failed to update subscription."); return(Constants.ErrorCode); } }
/// <summary> /// Triggers subscriptions /// </summary> /// <returns></returns> public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); bool noConfirm = _options.NoConfirmation; List <Subscription> subscriptionsToTrigger = new List <Subscription>(); if (!string.IsNullOrEmpty(_options.Id)) { // Look up subscription so we can print it later. try { Subscription subscription = await remote.GetSubscriptionAsync(_options.Id); subscriptionsToTrigger.Add(subscription); } catch (RestApiException e) when(e.Response.StatusCode == HttpStatusCode.NotFound) { Console.WriteLine($"Subscription with id '{_options.Id}' was not found."); return(Constants.ErrorCode); } } else { if (string.IsNullOrEmpty(_options.TargetRepository) && string.IsNullOrEmpty(_options.TargetBranch) && string.IsNullOrEmpty(_options.SourceRepository) && string.IsNullOrEmpty(_options.Channel)) { Console.WriteLine($"Please specify one or more filters to select which subscriptions should be triggered (see help)."); return(Constants.ErrorCode); } IEnumerable <Subscription> subscriptions = (await remote.GetSubscriptionsAsync()).Where(subscription => { return(_options.SubcriptionFilter(subscription)); }); if (subscriptions.Count() == 0) { Console.WriteLine("No subscriptions found matching the specified criteria."); return(Constants.ErrorCode); } subscriptionsToTrigger.AddRange(subscriptions); } if (!noConfirm) { // Print out the list of subscriptions about to be triggered. Console.WriteLine($"Will trigger the following {subscriptionsToTrigger.Count} subscriptions..."); foreach (var subscription in subscriptionsToTrigger) { Console.WriteLine($" {UxHelpers.GetSubscriptionDescription(subscription)}"); } char keyChar; do { Console.Write("Continue? (y/n) "); ConsoleKeyInfo keyInfo = Console.ReadKey(); keyChar = char.ToUpperInvariant(keyInfo.KeyChar); Console.WriteLine(); }while (keyChar != 'Y' && keyChar != 'N'); if (keyChar == 'N') { Console.WriteLine($"No subscriptions triggered, exiting."); return(Constants.ErrorCode); } } Console.Write($"Triggering {subscriptionsToTrigger.Count} subscriptions...{(noConfirm ? Environment.NewLine : "")}"); foreach (var subscription in subscriptionsToTrigger) { // If noConfirm was passed, print out the subscriptions as we go if (noConfirm) { Console.WriteLine($" {UxHelpers.GetSubscriptionDescription(subscription)}"); } await remote.TriggerSubscriptionAsync(subscription.Id.ToString()); } Console.WriteLine($"done"); return(Constants.SuccessCode); } catch (Exception e) { Logger.LogError(e, "Unexpected error while triggering subscriptions."); return(Constants.ErrorCode); } }
public override async Task <int> ExecuteAsync() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); IEnumerable <Subscription> subscriptions = await remote.GetSubscriptionsAsync(); IEnumerable <DefaultChannel> defaultChannels = await remote.GetDefaultChannelsAsync(); IEnumerable <Channel> channels = await remote.GetChannelsAsync(); HashSet <string> channelsToEvaluate = ComputeChannelsToEvaluate(channels); HashSet <string> reposToEvaluate = ComputeRepositoriesToEvaluate(defaultChannels, subscriptions); // Print out what will be evaluated. If no channels or repos are in the initial sets, then // this is currently an error. Because different PKPIs apply to different input items differently, // this check may not be useful in the future. if (channelsToEvaluate.Any()) { Console.WriteLine("Evaluating the following channels:"); foreach (string channel in channelsToEvaluate) { Console.WriteLine($" {channel}"); } } else { Console.WriteLine($"There were no channels found to evaluate based on inputs, exiting."); return(Constants.ErrorCode); } if (reposToEvaluate.Any()) { Console.WriteLine("Evaluating the following repositories:"); foreach (string repo in reposToEvaluate) { Console.WriteLine($" {repo}"); } } else { Console.WriteLine($"There were no repositories found to evaluate based on inputs, exiting."); return(Constants.ErrorCode); } Console.WriteLine(); // Compute metrics, then run in parallel. List <Func <Task <HealthMetricWithOutput> > > metricsToRun = ComputeMetricsToRun(channelsToEvaluate, reposToEvaluate, subscriptions, defaultChannels, channels); // Run the metrics HealthMetricWithOutput[] results = await Task.WhenAll <HealthMetricWithOutput>(metricsToRun.Select(metric => metric())); // Walk through and print the results out bool passed = true; foreach (var healthResult in results) { if (healthResult.Metric.Result != HealthResult.Passed) { passed = false; } Console.WriteLine($"{healthResult.Metric.MetricDescription} - ({healthResult.Metric.Result})"); if (healthResult.Metric.Result != HealthResult.Passed) { Console.WriteLine(); Console.WriteLine(healthResult.FormattedConsoleOutput); } } return(passed ? Constants.SuccessCode : Constants.ErrorCode); }
/// <summary> /// Gets the latest build for a repo /// </summary> /// <returns>Process exit code.</returns> public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); // Calculate out possible repos based on the input strings. // Today the DB has no way of searching for builds by substring, so for now // grab source/targets repos of subscriptions matched on substring, // and then add the explicit repo from the options. // Then search channels by substring // Then run GetLatestBuild for each permutation. var subscriptions = await remote.GetSubscriptionsAsync(); var possibleRepos = subscriptions .SelectMany(subscription => new List <string> { subscription.SourceRepository, subscription.TargetRepository }) .Where(r => r.Contains(_options.Repo, StringComparison.OrdinalIgnoreCase)) .ToHashSet(StringComparer.OrdinalIgnoreCase); possibleRepos.Add(_options.Repo); var channels = (await remote.GetChannelsAsync()) .Where(c => string.IsNullOrEmpty(_options.Channel) || c.Name.Contains(_options.Channel, StringComparison.OrdinalIgnoreCase)); if (!channels.Any()) { Console.WriteLine($"Could not find a channel with name containing '{_options.Channel}'"); return(Constants.ErrorCode); } bool foundBuilds = false; foreach (string possibleRepo in possibleRepos) { foreach (Channel channel in channels) { Build latestBuild = await remote.GetLatestBuildAsync(possibleRepo, channel.Id); if (latestBuild != null) { if (foundBuilds) { Console.WriteLine(); } foundBuilds = true; Console.Write(UxHelpers.GetTextBuildDescription(latestBuild)); } } } if (!foundBuilds) { Console.WriteLine("No latest build found matching the specified criteria"); return(Constants.ErrorCode); } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve latest build."); return(Constants.ErrorCode); } }
/// <summary> /// Implements the 'update-subscription' operation /// </summary> /// <param name="options"></param> public override async Task <int> ExecuteAsync() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); // First, try to get the subscription. If it doesn't exist the call will throw and the exception will be // caught by `RunOperation` Subscription subscription = await remote.GetSubscriptionAsync(_options.Id); var suggestedRepos = remote.GetSubscriptionsAsync(); var suggestedChannels = remote.GetChannelsAsync(); UpdateSubscriptionPopUp updateSubscriptionPopUp = new UpdateSubscriptionPopUp( "update-subscription/update-subscription-todo", Logger, subscription, (await suggestedChannels).Select(suggestedChannel => suggestedChannel.Name), (await suggestedRepos).SelectMany(subs => new List <string> { subscription.SourceRepository, subscription.TargetRepository }).ToHashSet(), Constants.AvailableFrequencies, Constants.AvailableMergePolicyYamlHelp); UxManager uxManager = new UxManager(_options.GitLocation, Logger); int exitCode = uxManager.PopUp(updateSubscriptionPopUp); if (exitCode != Constants.SuccessCode) { return(exitCode); } string channel = updateSubscriptionPopUp.Channel; string sourceRepository = updateSubscriptionPopUp.SourceRepository; string updateFrequency = updateSubscriptionPopUp.UpdateFrequency; bool batchable = updateSubscriptionPopUp.Batchable; bool enabled = updateSubscriptionPopUp.Enabled; List <MergePolicy> mergePolicies = updateSubscriptionPopUp.MergePolicies; try { SubscriptionUpdate subscriptionToUpdate = new SubscriptionUpdate { ChannelName = channel ?? subscription.Channel.Name, SourceRepository = sourceRepository ?? subscription.SourceRepository, Enabled = enabled, Policy = subscription.Policy, }; subscriptionToUpdate.Policy.Batchable = batchable; subscriptionToUpdate.Policy.UpdateFrequency = Enum.Parse <UpdateFrequency>(updateFrequency); subscriptionToUpdate.Policy.MergePolicies = mergePolicies?.ToImmutableList(); var updatedSubscription = await remote.UpdateSubscriptionAsync( _options.Id, subscriptionToUpdate); Console.WriteLine($"Successfully updated subscription with id '{updatedSubscription.Id}'."); // Determine whether the subscription should be triggered. if (!_options.NoTriggerOnUpdate) { bool triggerAutomatically = _options.TriggerOnUpdate; // Determine whether we should prompt if the user hasn't explicitly // said one way or another. We shouldn't prompt if nothing changes or // if non-interesting options have changed if (!triggerAutomatically && ((subscriptionToUpdate.ChannelName != subscription.Channel.Name) || (subscriptionToUpdate.SourceRepository != subscription.SourceRepository) || (subscriptionToUpdate.Enabled.Value && !subscription.Enabled) || (subscriptionToUpdate.Policy.UpdateFrequency != UpdateFrequency.None && subscriptionToUpdate.Policy.UpdateFrequency != subscription.Policy.UpdateFrequency))) { triggerAutomatically = UxHelpers.PromptForYesNo("Trigger this subscription immediately?"); } if (triggerAutomatically) { await remote.TriggerSubscriptionAsync(updatedSubscription.Id.ToString()); Console.WriteLine($"Subscription '{updatedSubscription.Id}' triggered."); } } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (RestApiException e) when(e.Response.Status == (int)System.Net.HttpStatusCode.BadRequest) { // Could have been some kind of validation error (e.g. channel doesn't exist) Logger.LogError($"Failed to update subscription: {e.Response.Content}"); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Failed to update subscription."); return(Constants.ErrorCode); } }
/// <summary> /// Get a specific build of a repository /// </summary> /// <returns>Process exit code.</returns> public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); List <Build> matchingBuilds = null; if (_options.Id != 0) { if (!string.IsNullOrEmpty(_options.Repo) || !string.IsNullOrEmpty(_options.Commit)) { Console.WriteLine("--id should not be used with other options."); return(Constants.ErrorCode); } matchingBuilds = new List <Build>() { await remote.GetBuildAsync(_options.Id) }; } else if (!string.IsNullOrEmpty(_options.Repo) || !string.IsNullOrEmpty(_options.Commit)) { if (string.IsNullOrEmpty(_options.Repo) != string.IsNullOrEmpty(_options.Commit)) { Console.WriteLine("--repo and --commit should be used together."); return(Constants.ErrorCode); } var subscriptions = await remote.GetSubscriptionsAsync(); var possibleRepos = subscriptions .SelectMany(subscription => new List <string> { subscription.SourceRepository, subscription.TargetRepository }) .Where(r => r.Contains(_options.Repo, StringComparison.OrdinalIgnoreCase)) .ToHashSet(StringComparer.OrdinalIgnoreCase); matchingBuilds = new List <Build>(); foreach (string repo in possibleRepos) { matchingBuilds.AddRange(await remote.GetBuildsAsync(repo, _options.Commit)); } } else { Console.WriteLine("Please specify --id, --uri, or --repo and --commit to lookup a build."); return(Constants.ErrorCode); } // Print the build info. if (!matchingBuilds.Any()) { Console.WriteLine($"Could not any builds matching the given criteria."); return(Constants.ErrorCode); } switch (_options.OutputFormat) { case DarcOutputType.text: foreach (Build build in matchingBuilds) { Console.Write(UxHelpers.GetTextBuildDescription(build)); } break; case DarcOutputType.json: Console.WriteLine(JsonConvert.SerializeObject( matchingBuilds.Select(build => UxHelpers.GetJsonBuildDescription(build)), Formatting.Indented)); break; default: throw new NotImplementedException($"Output format type {_options.OutputFormat} not yet supported for get-build."); } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve build information."); return(Constants.ErrorCode); } }
public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); var subscriptions = (await remote.GetSubscriptionsAsync()).Where(subscription => { return(_options.SubcriptionFilter(subscription)); }); if (subscriptions.Count() == 0) { Console.WriteLine("No subscriptions found matching the specified criteria."); return(Constants.ErrorCode); } // Based on the current output schema, sort by source repo, target repo, target branch, etc. // Concat the input strings as a simple sorting mechanism. foreach (var subscription in subscriptions.OrderBy(subscription => $"{subscription.SourceRepository}{subscription.Channel}{subscription.TargetRepository}{subscription.TargetBranch}")) { Console.WriteLine($"{subscription.SourceRepository} ({subscription.Channel.Name}) ==> '{subscription.TargetRepository}' ('{subscription.TargetBranch}')"); Console.WriteLine($" - Id: {subscription.Id}"); Console.WriteLine($" - Update Frequency: {subscription.Policy.UpdateFrequency}"); Console.WriteLine($" - Enabled: {subscription.Enabled}"); Console.WriteLine($" - Batchable: {subscription.Policy.Batchable}"); // If batchable, the merge policies come from the repository IEnumerable <MergePolicy> mergePolicies = subscription.Policy.MergePolicies; if (subscription.Policy.Batchable == true) { mergePolicies = await remote.GetRepositoryMergePoliciesAsync(subscription.TargetRepository, subscription.TargetBranch); } if (mergePolicies.Any()) { Console.WriteLine($" - Merge Policies:"); foreach (MergePolicy mergePolicy in mergePolicies) { Console.WriteLine($" {mergePolicy.Name}"); if (mergePolicy.Properties != null) { foreach (var mergePolicyProperty in mergePolicy.Properties) { // The merge policy property is a key value pair. For formatting, turn it into a string. // It's often a JToken, so handle appropriately // 1. If the number of lines in the string is 1, write on same line as key // 2. If the number of lines in the string is more than one, start on new // line and indent. string valueString = mergePolicyProperty.Value.ToString(); string[] valueLines = valueString.Split(System.Environment.NewLine); string keyString = $" {mergePolicyProperty.Key} = "; Console.Write(keyString); if (valueLines.Length == 1) { Console.WriteLine(valueString); } else { string indentString = new string(' ', keyString.Length); Console.WriteLine(); foreach (string line in valueLines) { Console.WriteLine($"{indentString}{line}"); } } } } } } else { Console.WriteLine($" - Merge Policies: []"); } // Currently the API only returns the last applied build for requests to specific subscriptions. // This will be fixed, but for now, don't print the last applied build otherwise. if (subscription.LastAppliedBuild != null) { Console.WriteLine($" - Last Build: {subscription.LastAppliedBuild.AzureDevOpsBuildNumber} ({subscription.LastAppliedBuild.Commit})"); } } return(Constants.SuccessCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve subscriptions"); return(Constants.ErrorCode); } }
/// <summary> /// Implements the 'add-subscription' operation /// </summary> /// <param name="options"></param> public override async Task <int> ExecuteAsync() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); if (_options.IgnoreChecks.Count() > 0 && !_options.AllChecksSuccessfulMergePolicy) { Console.WriteLine($"--ignore-checks must be combined with --all-checks-passed"); return(Constants.ErrorCode); } // Parse the merge policies List <MergePolicy> mergePolicies = new List <MergePolicy>(); if (_options.NoExtraCommitsMergePolicy) { mergePolicies.Add( new MergePolicy { Name = "NoExtraCommits" }); } if (_options.AllChecksSuccessfulMergePolicy) { mergePolicies.Add( new MergePolicy { Name = "AllChecksSuccessful", Properties = ImmutableDictionary.Create <string, JToken>() .Add("ignoreChecks", JToken.FromObject(_options.IgnoreChecks)) }); } if (_options.NoRequestedChangesMergePolicy) { mergePolicies.Add( new MergePolicy { Name = "NoRequestedChanges", Properties = ImmutableDictionary.Create <string, JToken>() }); } if (_options.StandardAutoMergePolicies) { mergePolicies.Add( new MergePolicy { Name = "Standard", Properties = ImmutableDictionary.Create <string, JToken>() }); } if (_options.Batchable && mergePolicies.Count > 0) { Console.WriteLine("Batchable subscriptions cannot be combined with merge policies. " + "Merge policies are specified at a repository+branch level."); return(Constants.ErrorCode); } string channel = _options.Channel; string sourceRepository = _options.SourceRepository; string targetRepository = _options.TargetRepository; string targetBranch = _options.TargetBranch; string updateFrequency = _options.UpdateFrequency; bool batchable = _options.Batchable; // If in quiet (non-interactive mode), ensure that all options were passed, then // just call the remote API if (_options.Quiet) { if (string.IsNullOrEmpty(channel) || string.IsNullOrEmpty(sourceRepository) || string.IsNullOrEmpty(targetRepository) || string.IsNullOrEmpty(targetBranch) || string.IsNullOrEmpty(updateFrequency) || !Constants.AvailableFrequencies.Contains(updateFrequency, StringComparer.OrdinalIgnoreCase)) { Logger.LogError($"Missing input parameters for the subscription. Please see command help or remove --quiet/-q for interactive mode"); return(Constants.ErrorCode); } } else { // Grab existing subscriptions to get suggested values. // TODO: When this becomes paged, set a max number of results to avoid // pulling too much. var suggestedRepos = remote.GetSubscriptionsAsync(); var suggestedChannels = remote.GetChannelsAsync(); // Help the user along with a form. We'll use the API to gather suggested values // from existing subscriptions based on the input parameters. AddSubscriptionPopUp initEditorPopUp = new AddSubscriptionPopUp("add-subscription/add-subscription-todo", Logger, channel, sourceRepository, targetRepository, targetBranch, updateFrequency, batchable, mergePolicies, (await suggestedChannels).Select(suggestedChannel => suggestedChannel.Name), (await suggestedRepos).SelectMany(subscription => new List <string> { subscription.SourceRepository, subscription.TargetRepository }).ToHashSet(), Constants.AvailableFrequencies, Constants.AvailableMergePolicyYamlHelp); UxManager uxManager = new UxManager(Logger); int exitCode = uxManager.PopUp(initEditorPopUp); if (exitCode != Constants.SuccessCode) { return(exitCode); } channel = initEditorPopUp.Channel; sourceRepository = initEditorPopUp.SourceRepository; targetRepository = initEditorPopUp.TargetRepository; targetBranch = initEditorPopUp.TargetBranch; updateFrequency = initEditorPopUp.UpdateFrequency; mergePolicies = initEditorPopUp.MergePolicies; batchable = initEditorPopUp.Batchable; } try { var newSubscription = await remote.CreateSubscriptionAsync(channel, sourceRepository, targetRepository, targetBranch, updateFrequency, batchable, mergePolicies); Console.WriteLine($"Successfully created new subscription with id '{newSubscription.Id}'."); return(Constants.SuccessCode); } catch (RestApiException e) when(e.Response.StatusCode == System.Net.HttpStatusCode.BadRequest) { // Could have been some kind of validation error (e.g. channel doesn't exist) Logger.LogError($"Failed to create subscription: {e.Response.Content}"); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Failed to create subscription."); return(Constants.ErrorCode); } }