/// <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); Build build = await remote.GetBuildAsync(_options.Id); if (build != null) { Console.Write(UxHelpers.GetBuildDescription(build)); } else { Console.WriteLine($"Could not find build with id '{_options.Id}'"); return(Constants.ErrorCode); } return(Constants.SuccessCode); } catch (Exception e) { Logger.LogError(e, $"Error: Failed to retrieve build with id '{_options.Id}'"); return(Constants.ErrorCode); } }
public async override Task <int> ExecuteAsync() { if (!(_options.Released ^ _options.NotReleased)) { Console.WriteLine("Please specify either --released or --not-released."); return(Constants.ErrorCode); } try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); Build updatedBuild = await remote.UpdateBuildAsync(_options.Id, new BuildUpdate { Released = _options.Released }); Console.WriteLine($"Updated build {_options.Id} with new information."); Console.WriteLine(UxHelpers.GetTextBuildDescription(updatedBuild)); } catch (Exception e) { Logger.LogError(e, $"Error: Failed to update build with id '{_options.Id}'"); return(Constants.ErrorCode); } return(Constants.SuccessCode); }
public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetRemote(_options, _options.Repository, Logger); // Users can ignore the flag and pass in -regex: but to prevent typos we'll avoid that. _options.Branch = _options.UseBranchAsRegex ? $"-regex:{_options.Branch}" : GitHelpers.NormalizeBranchName(_options.Branch); if (!(await UxHelpers.VerifyAndConfirmBranchExistsAsync(remote, _options.Repository, _options.Branch, !_options.NoConfirmation))) { Console.WriteLine("Aborting default channel creation."); return(Constants.ErrorCode); } await remote.AddDefaultChannelAsync(_options.Repository, _options.Branch, _options.Channel); return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to add a new default channel association."); return(Constants.ErrorCode); } }
public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetRemote(_options, _options.Repository, Logger); _options.Branch = GitHelpers.NormalizeBranchName(_options.Branch); if (!(await UxHelpers.VerifyAndConfirmBranchExistsAsync(remote, _options.Repository, _options.Branch, !_options.NoConfirmation))) { Console.WriteLine("Aborting default channel creation."); return(Constants.ErrorCode); } await remote.AddDefaultChannelAsync(_options.Repository, _options.Branch, _options.Channel); return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to add a new default channel association."); return(Constants.ErrorCode); } }
private async Task LogDependencyGraph(DependencyGraph graph) { using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.OutputFile)) { await writer.WriteLineAsync($"Repositories:"); await LogDependencyGraphNode(writer, graph.Root, " "); await LogIncoherencies(writer, graph); } }
public override async Task <int> ExecuteAsync() { try { RemoteFactory remoteFactory = new RemoteFactory(_options); var barOnlyRemote = await remoteFactory.GetBarOnlyRemoteAsync(Logger); List <DefaultChannel> defaultChannels = (await barOnlyRemote.GetDefaultChannelsAsync()).ToList(); defaultChannels.Add( new DefaultChannel(0, "https://github.com/dotnet/arcade", true) { Branch = "master", Channel = await barOnlyRemote.GetChannelAsync(".NET Tools - Latest") } ); defaultChannels.Add( new DefaultChannel(0, "https://github.com/dotnet/arcade", true) { Branch = "release/3.x", Channel = await barOnlyRemote.GetChannelAsync(".NET 3 Tools") } ); List <Subscription> subscriptions = (await barOnlyRemote.GetSubscriptionsAsync()).ToList(); // Build, then prune out what we don't want to see if the user specified // channels. DependencyFlowGraph flowGraph = DependencyFlowGraph.Build(defaultChannels, subscriptions); Channel targetChannel = null; if (!string.IsNullOrEmpty(_options.Channel)) { // Resolve the channel. targetChannel = await UxHelpers.ResolveSingleChannel(barOnlyRemote, _options.Channel); if (targetChannel == null) { return(Constants.ErrorCode); } } if (targetChannel != null) { flowGraph.PruneGraph(node => IsInterestingNode(targetChannel, node), edge => IsInterestingEdge(edge)); } await LogGraphViz(targetChannel, flowGraph); return(Constants.SuccessCode); } catch (Exception exc) { Logger.LogError(exc, "Something failed while getting the dependency graph."); return(Constants.ErrorCode); } }
internal static string GetString(List <DependencyDetail> expectedDependencies) { StringBuilder stringBuilder = new StringBuilder(); foreach (DependencyDetail dependency in expectedDependencies) { stringBuilder.AppendLine(UxHelpers.DependencyToString(dependency)); } return(stringBuilder.ToString()); }
public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); IEnumerable <Subscription> subscriptions = await _options.FilterSubscriptions(remote); if (!subscriptions.Any()) { 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 (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve subscriptions"); return(Constants.ErrorCode); } }
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); } }
/// <summary> /// Resolve channel based on the input options. If no channel could be resolved /// based on the input options, returns null. /// </summary> /// <returns>Default channel or null</returns> protected async Task <DefaultChannel> ResolveSingleChannel() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); IEnumerable <DefaultChannel> potentialDefaultChannels = await remote.GetDefaultChannelsAsync(); // User should have supplied id or a combo of the channel name, repo, and branch. if (_options.Id != -1) { DefaultChannel defaultChannel = potentialDefaultChannels.SingleOrDefault(d => d.Id == _options.Id); if (defaultChannel == null) { Console.WriteLine($"Could not find a default channel with id {_options.Id}"); } return(defaultChannel); } else if (string.IsNullOrEmpty(_options.Repository) || string.IsNullOrEmpty(_options.Channel) || string.IsNullOrEmpty(_options.Branch)) { Console.WriteLine("Please specify either the default channel id with --id or a combination of --channel, --branch and --repo"); return(null); } // Otherwise, filter based on the other inputs. If more than one resolves, then print the possible // matches and return null var matchingChannels = potentialDefaultChannels.Where(d => { return((string.IsNullOrEmpty(_options.Repository) || d.Repository.Contains(_options.Repository, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrEmpty(_options.Channel) || d.Channel.Name.Contains(_options.Channel, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrEmpty(_options.Branch) || d.Branch.Contains(GitHelpers.NormalizeBranchName(_options.Branch), StringComparison.OrdinalIgnoreCase))); }); if (!matchingChannels.Any()) { Console.WriteLine($"No channels found matching the specified criteria."); return(null); } else if (matchingChannels.Count() != 1) { Console.WriteLine($"More than one channel matching the specified criteria. Please change your options to be more specific."); foreach (DefaultChannel defaultChannel in matchingChannels) { Console.WriteLine($" {UxHelpers.GetDefaultChannelDescriptionString(defaultChannel)}"); } return(null); } else { return(matchingChannels.Single()); } }
/// <summary> /// Log the dependency graph as a simple flat list of repo/sha combinations /// that contribute to this graph. /// </summary> /// <param name="graph">Graph to log</param> private async Task LogFlatDependencyGraph(DependencyGraph graph) { using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.OutputFile)) { await writer.WriteLineAsync($"Repositories:"); foreach (DependencyGraphNode node in graph.Nodes) { await LogBasicNodeDetails(writer, node, " "); } await LogIncoherencies(writer, graph); } }
/// <summary> /// Deletes a build from 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}' is not assigned to channel '{targetChannel.Name}'"); return(Constants.SuccessCode); } Console.WriteLine($"Deleting the following build from channel '{targetChannel.Name}':"); Console.WriteLine(); Console.Write(UxHelpers.GetTextBuildDescription(build)); await remote.DeleteBuildFromChannelAsync(_options.Id, targetChannel.Id); // Let the user know they can trigger subscriptions if they'd like. Console.WriteLine("Subscriptions can be triggered to revert to the previous state using the following command:"); Console.WriteLine($"darc trigger-subscriptions --source-repo {build.GitHubRepository ?? build.AzureDevOpsRepository} --channel {targetChannel.Name}"); return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Error: Failed to delete build '{_options.Id}' from channel '{_options.Channel}'."); 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 override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); IEnumerable <Subscription> subscriptions = await _options.FilterSubscriptions(remote); if (!subscriptions.Any()) { 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}")) { // 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); } string subscriptionInfo = UxHelpers.GetTextSubscriptionDescription(subscription, mergePolicies); Console.Write(subscriptionInfo); } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve subscriptions"); return(Constants.ErrorCode); } }
/// <summary> /// Retrieve information about the default association between builds of a specific branch/repo /// and a channel. /// </summary> /// <returns></returns> public override async Task <int> ExecuteAsync() { try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); IEnumerable <DefaultChannel> defaultChannels = (await remote.GetDefaultChannelsAsync()) .Where(defaultChannel => { return((string.IsNullOrEmpty(_options.SourceRepository) || defaultChannel.Repository.Contains(_options.SourceRepository, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrEmpty(_options.Branch) || defaultChannel.Branch.Contains(_options.Branch, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrEmpty(_options.Channel) || defaultChannel.Channel.Name.Contains(_options.Channel, StringComparison.OrdinalIgnoreCase))); }) .OrderBy(df => df.Repository); if (defaultChannels.Count() == 0) { Console.WriteLine("No matching channels were found."); } // Write out a simple list of each channel's name foreach (DefaultChannel defaultChannel in defaultChannels) { Console.WriteLine(UxHelpers.GetDefaultChannelDescriptionString(defaultChannel)); } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve default channel information."); return(Constants.ErrorCode); } }
public override async Task <int> ExecuteAsync() { try { RemoteFactory remoteFactory = new RemoteFactory(_options); var barOnlyRemote = await remoteFactory.GetBarOnlyRemoteAsync(Logger); Channel targetChannel = null; if (!string.IsNullOrEmpty(_options.Channel)) { // Resolve the channel. targetChannel = await UxHelpers.ResolveSingleChannel(barOnlyRemote, _options.Channel); if (targetChannel == null) { return(Constants.ErrorCode); } } var flowGraph = await barOnlyRemote.GetDependencyFlowGraph( targetChannel?.Id ?? 0, _options.Days, includeArcade : true, includeBuildTimes : _options.IncludeBuildTimes, includeDisabledSubscriptions : _options.IncludeDisabledSubscriptions, includedFrequencies : _options.IncludedFrequencies?.ToList()); await LogGraphVizAsync(targetChannel, flowGraph, _options.IncludeBuildTimes); return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception exc) { Logger.LogError(exc, "Something failed while getting the dependency graph."); return(Constants.ErrorCode); } }
/// <summary> /// /// </summary> /// <param name="channels"></param> /// <returns></returns> private HashSet <string> ComputeChannelsToEvaluate(IEnumerable <Channel> channels) { if (!string.IsNullOrEmpty(_options.Channel)) { HashSet <string> channelsToTarget = new HashSet <string>(StringComparer.OrdinalIgnoreCase); Channel targetChannel = UxHelpers.ResolveSingleChannel(channels, _options.Channel); if (targetChannel != null) { channelsToTarget.Add(targetChannel.Name); } return(channelsToTarget); } else { // Look up all channels return(channels.Select(c => c.Name).ToHashSet()); } }
/// <summary> /// Log the graph in graphviz (dot) format. /// </summary> /// <param name="graph">Graph to log</param> /// <remarks> /// Example of a graphviz graph description /// /// digraph graphname { /// a -> b -> c; /// b -> d; /// } /// /// For more info see https://www.graphviz.org/ /// </remarks> /// <returns>Async task</returns> private async Task LogGraphViz(DependencyGraph graph) { using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.GraphVizOutputFile)) { await writer.WriteLineAsync("digraph repositoryGraph {"); await writer.WriteLineAsync(" node [shape=record]"); foreach (DependencyGraphNode node in graph.Nodes) { StringBuilder nodeBuilder = new StringBuilder(); // First add the node name nodeBuilder.Append($" {UxHelpers.CalculateGraphVizNodeName(node)}"); // Then add the label. label looks like [label="<info here>"] nodeBuilder.Append("[label=\""); // Append friendly repo name nodeBuilder.Append(UxHelpers.GetSimpleRepoName(node.Repository)); nodeBuilder.Append(@"\n"); // Append short commit sha nodeBuilder.Append(node.Commit.Substring(0, node.Commit.Length < 10 ? node.Commit.Length : 10)); // Append a build string (with newline) if available if (node.ContributingBuilds != null && node.ContributingBuilds.Any()) { Build newestBuild = node.ContributingBuilds.OrderByDescending(b => b.DateProduced).First(); nodeBuilder.Append($"\\n{newestBuild.DateProduced.ToString("g")} (UTC)"); } // Append a diff string if the graph contains diff info. GitDiff diffFrom = node.DiffFrom; if (diffFrom != null) { if (!diffFrom.Valid) { nodeBuilder.Append("\\ndiff unknown"); } else if (diffFrom.Ahead != 0 || diffFrom.Behind != 0) { if (node.DiffFrom.Ahead != 0) { nodeBuilder.Append($"\\nahead: {node.DiffFrom.Ahead} commits"); } if (node.DiffFrom.Behind != 0) { nodeBuilder.Append($"\\nbehind: {node.DiffFrom.Behind} commits"); } } else { nodeBuilder.Append("\\nlatest"); } } // Append end of label and end of node. nodeBuilder.Append("\"];"); // Write it out. await writer.WriteLineAsync(nodeBuilder.ToString()); // Now write the edges foreach (DependencyGraphNode childNode in node.Children) { await writer.WriteLineAsync($" {UxHelpers.CalculateGraphVizNodeName(node)} -> {UxHelpers.CalculateGraphVizNodeName(childNode)}"); } } await writer.WriteLineAsync("}"); } }
/// <summary> /// Log the graph in graphviz (dot) format. /// </summary> /// <param name="graph">Graph to log</param> /// <remarks> /// Example of a graphviz graph description /// /// digraph graphname { /// a -> b -> c; /// b -> d; /// } /// /// For more info see https://www.graphviz.org/ /// </remarks> /// <returns>Async task</returns> private async Task LogGraphVizAsync(Channel targetChannel, DependencyFlowGraph graph, bool includeBuildTimes) { StringBuilder subgraphClusterWriter = null; bool writeToSubgraphCluster = targetChannel != null; if (writeToSubgraphCluster) { subgraphClusterWriter = new StringBuilder(); subgraphClusterWriter.AppendLine($" subgraph cluster_{UxHelpers.CalculateGraphVizNodeName(targetChannel.Name)} {{"); subgraphClusterWriter.AppendLine($" label = \"{targetChannel.Name}\""); } using (StreamWriter writer = UxHelpers.GetOutputFileStreamOrConsole(_options.GraphVizOutputFile)) { await writer.WriteLineAsync("digraph repositoryGraph {"); await writer.WriteLineAsync(" node [shape=record]"); foreach (DependencyFlowNode node in graph.Nodes) { StringBuilder nodeBuilder = new StringBuilder(); string style = node.OnLongestBuildPath ? "style=\"diagonals,bold\" color=red" : ""; // First add the node name nodeBuilder.Append($" {UxHelpers.CalculateGraphVizNodeName(node)}"); // Then add the label. label looks like [label="<info here>"] nodeBuilder.Append($"[{style}\nlabel=\""); // Append friendly repo name nodeBuilder.Append(UxHelpers.GetSimpleRepoName(node.Repository)); nodeBuilder.Append(@"\n"); // Append branch name nodeBuilder.Append(node.Branch); if (includeBuildTimes) { // Append best case nodeBuilder.Append(@"\n"); nodeBuilder.Append($"Best Case: {Math.Round(node.BestCasePathTime, 2, MidpointRounding.AwayFromZero)} min"); nodeBuilder.Append(@"\n"); // Append worst case nodeBuilder.Append($"Worst Case: {Math.Round(node.WorstCasePathTime, 2, MidpointRounding.AwayFromZero)} min"); nodeBuilder.Append(@"\n"); // Append build times nodeBuilder.Append($"Official Build Time: {Math.Round(node.OfficialBuildTime, 2, MidpointRounding.AwayFromZero)} min"); nodeBuilder.Append(@"\n"); nodeBuilder.Append($"PR Build Time: {Math.Round(node.PrBuildTime, 2, MidpointRounding.AwayFromZero)} min"); } // Append end of label and end of node. nodeBuilder.Append("\"];"); // If highlighting a specific channel, Add those nodes to a subgraph cluster // if they output to the subgraph cluster. if (writeToSubgraphCluster && node.OutputChannels.Any(c => c == targetChannel.Name)) { subgraphClusterWriter.AppendLine($" {UxHelpers.CalculateGraphVizNodeName(node)}"); } // Write it out. await writer.WriteLineAsync(nodeBuilder.ToString()); } // Now write all the edges foreach (DependencyFlowEdge edge in graph.Edges) { string fromNode = UxHelpers.CalculateGraphVizNodeName(edge.From); string toNode = UxHelpers.CalculateGraphVizNodeName(edge.To); string label = $"{edge.Subscription.Channel.Name} ({edge.Subscription.Policy.UpdateFrequency})"; if (writeToSubgraphCluster && edge.Subscription.Channel.Name == targetChannel.Name) { subgraphClusterWriter.AppendLine($" {fromNode} -> {toNode} [{GetEdgeStyle(edge)}]"); } else { await writer.WriteLineAsync($" {fromNode} -> {toNode} [{GetEdgeStyle(edge)}]"); } } if (writeToSubgraphCluster) { await writer.WriteLineAsync(subgraphClusterWriter.ToString()); await writer.WriteLineAsync(" }"); } // Write a legend await writer.WriteLineAsync(" subgraph cluster1 {"); await writer.WriteLineAsync(" rankdir=RL;"); await writer.WriteLineAsync(" label = \"Legend\""); await writer.WriteLineAsync(" shape = rectangle;"); await writer.WriteLineAsync(" color = black;"); await writer.WriteLineAsync(" a[style = invis];"); await writer.WriteLineAsync(" b[style = invis];"); await writer.WriteLineAsync(" c[style = invis];"); await writer.WriteLineAsync(" d[style = invis];"); await writer.WriteLineAsync(" e[style = invis];"); await writer.WriteLineAsync(" f[style = invis];"); await writer.WriteLineAsync(" g[style = \"diagonals,bold\" color=red];"); await writer.WriteLineAsync(" h[style = \"diagonals,bold\" color=red];"); await writer.WriteLineAsync(" c->d[label = \"Updated Every Build\", style = bold];"); await writer.WriteLineAsync(" a->b[label = \"Updated Every Day\", style = dashed];"); await writer.WriteLineAsync(" e->f[label = \"Disabled/Updated On-demand\", style = dotted];"); await writer.WriteLineAsync(" g->h[label = \"Longest Build Path\", color=\"red:invis:red\"];"); await writer.WriteLineAsync(" }"); await writer.WriteLineAsync(" subgraph cluster2{"); await writer.WriteLineAsync(" rankdir=BT;"); await writer.WriteLineAsync(" style=invis;"); await writer.WriteLineAsync(" note[shape=plaintext label=\"Best Case: Time through the graph assuming no dependency flow\nWorst Case: Time through the graph with dependency flow (PRs)\"];"); await writer.WriteLineAsync(" }"); await writer.WriteLineAsync(" d->note[lhead=cluster2, ltail=cluster1, style=invis];"); await writer.WriteLineAsync("}"); } }
/// <summary> /// Compute the subscription health metrics to run based on the channels and input repositories /// </summary> /// <param name="channelsToEvaluate">Channels in the initial set.</param> /// <param name="reposToEvaluate">Repositories in the initial set.</param> /// <param name="subscriptions">Subscriptions in the build asset registry.</param> /// <returns>List of subscription health metrics</returns> /// <remarks> /// Subscription health is based on repo and branch. We need to find all the combinations that /// that make sense to evaluate. /// /// Since this is a target-of-subscription focused metric, use the set of target repos+branches from /// <paramref name="subscriptions"/> where the target repo is in <paramref name="reposToEvaluate"/> and /// the source channel is in <paramref name="channelsToEvaluate"/> or the target branch is in branches. /// Also add in repos (in <paramref name="reposToEvaluate"/>) who have a default channel association targeting /// a channel in <paramref name="channelsToEvaluate"/> /// /// Note that this will currently miss completely untargeted branches, until those have at least one /// default channel or subscription. This is a fairly benign limitation. /// </remarks> private List <Func <Task <HealthMetricWithOutput> > > ComputeSubscriptionHealthMetricsToRun(HashSet <string> channelsToEvaluate, HashSet <string> reposToEvaluate, IEnumerable <Subscription> subscriptions, IEnumerable <DefaultChannel> defaultChannels) { IRemoteFactory remoteFactory = new RemoteFactory(_options); HashSet <(string repo, string branch)> repoBranchCombinations = GetRepositoryBranchCombinations(channelsToEvaluate, reposToEvaluate, subscriptions, defaultChannels); return(repoBranchCombinations.Select <(string repo, string branch), Func <Task <HealthMetricWithOutput> > >(t => async() => { SubscriptionHealthMetric healthMetric = new SubscriptionHealthMetric(t.repo, t.branch, d => true, Logger, remoteFactory); await healthMetric.EvaluateAsync(); StringBuilder outputBuilder = new StringBuilder(); if (healthMetric.ConflictingSubscriptions.Any()) { outputBuilder.AppendLine($" Conflicting subscriptions:"); foreach (var conflict in healthMetric.ConflictingSubscriptions) { outputBuilder.AppendLine($" {conflict.Asset} would be updated by the following subscriptions:"); foreach (var subscription in conflict.Subscriptions) { outputBuilder.AppendLine($" {UxHelpers.GetSubscriptionDescription(subscription)} ({subscription.Id})"); } } } if (healthMetric.DependenciesMissingSubscriptions.Any()) { outputBuilder.AppendLine($" Dependencies missing subscriptions:"); foreach (DependencyDetail dependency in healthMetric.DependenciesMissingSubscriptions) { outputBuilder.AppendLine($" {dependency.Name}"); } } if (healthMetric.DependenciesThatDoNotFlow.Any()) { outputBuilder.AppendLine($" Dependencies that do not flow automatically (disabled or frequency=none):"); foreach (DependencyDetail dependency in healthMetric.DependenciesThatDoNotFlow) { outputBuilder.AppendLine($" {dependency.Name}"); } } if (healthMetric.UnusedSubscriptions.Any()) { outputBuilder.AppendLine($" Subscriptions that do not have any effect:"); foreach (Subscription subscription in healthMetric.UnusedSubscriptions) { outputBuilder.AppendLine($" {UxHelpers.GetSubscriptionDescription(subscription)} ({subscription.Id})"); } } return new HealthMetricWithOutput(healthMetric, outputBuilder.ToString()); }) .ToList()); }
/// <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 'subscription-status' operation /// </summary> /// <param name="options"></param> public override async Task <int> ExecuteAsync() { if ((_options.Enable && _options.Disable) || (!_options.Enable && !_options.Disable)) { Console.WriteLine("Please specify either --enable or --disable"); return(Constants.ErrorCode); } string presentTenseStatusMessage = _options.Enable ? "enable" : "disable"; string pastTenseStatusMessage = _options.Enable ? "enabled" : "disabled"; string actionStatusMessage = _options.Enable ? "Enabling" : "Disabling"; try { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); bool noConfirm = _options.NoConfirmation; List <Subscription> subscriptionsToEnableDisable = 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); subscriptionsToEnableDisable.Add(subscription); } catch (RestApiException e) when(e.Response.Status == (int)HttpStatusCode.NotFound) { Console.WriteLine($"Subscription with id '{_options.Id}' was not found."); return(Constants.ErrorCode); } } else { if (!_options.HasAnyFilters()) { Console.WriteLine($"Please specify one or more filters to select which subscriptions should be {pastTenseStatusMessage} (see help)."); return(Constants.ErrorCode); } IEnumerable <Subscription> subscriptions = await _options.FilterSubscriptions(remote); if (!subscriptions.Any()) { Console.WriteLine("No subscriptions found matching the specified criteria."); return(Constants.ErrorCode); } subscriptionsToEnableDisable.AddRange(subscriptions); } // Filter away subscriptions that already have the desired state: subscriptionsToEnableDisable = subscriptionsToEnableDisable.Where(s => s.Enabled != _options.Enable).ToList(); if (!subscriptionsToEnableDisable.Any()) { Console.WriteLine($"All subscriptions are already {pastTenseStatusMessage}."); return(Constants.SuccessCode); } if (!noConfirm) { // Print out the list of subscriptions about to be enabled/disabled. Console.WriteLine($"Will {presentTenseStatusMessage} the following {subscriptionsToEnableDisable.Count} subscriptions..."); foreach (var subscription in subscriptionsToEnableDisable) { Console.WriteLine($" {UxHelpers.GetSubscriptionDescription(subscription)}"); } if (!UxHelpers.PromptForYesNo("Continue?")) { Console.WriteLine($"No subscriptions {pastTenseStatusMessage}, exiting."); return(Constants.ErrorCode); } } Console.Write($"{actionStatusMessage} {subscriptionsToEnableDisable.Count} subscriptions...{(noConfirm ? Environment.NewLine : "")}"); foreach (var subscription in subscriptionsToEnableDisable) { // If noConfirm was passed, print out the subscriptions as we go if (noConfirm) { Console.WriteLine($" {UxHelpers.GetSubscriptionDescription(subscription)}"); } SubscriptionUpdate subscriptionToUpdate = new SubscriptionUpdate { ChannelName = subscription.Channel.Name, SourceRepository = subscription.SourceRepository, Enabled = _options.Enable, Policy = subscription.Policy }; subscriptionToUpdate.Policy.Batchable = subscription.Policy.Batchable; subscriptionToUpdate.Policy.UpdateFrequency = subscription.Policy.UpdateFrequency; subscriptionToUpdate.Policy.MergePolicies = subscription.Policy.MergePolicies; var updatedSubscription = await remote.UpdateSubscriptionAsync( subscription.Id.ToString(), subscriptionToUpdate); } Console.WriteLine("done"); return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Unexpected error while {actionStatusMessage.ToLower()} subscriptions."); 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.BuildUri) || !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.BuildUri)) { if (_options.Id != 0 || !string.IsNullOrEmpty(_options.Repo) || !string.IsNullOrEmpty(_options.Commit)) { Console.WriteLine("--uri should not be used with other options."); return(Constants.ErrorCode); } Console.WriteLine("This option is not yet supported."); return(Constants.ErrorCode); } 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); } else if (_options.Id != 0 || !string.IsNullOrEmpty(_options.BuildUri)) { Console.WriteLine("--repo and --commit should not be used with other options."); return(Constants.ErrorCode); } matchingBuilds = (await remote.GetBuildsAsync(_options.Repo, _options.Commit)).ToList(); } 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() { IRemote remote = RemoteFactory.GetBarOnlyRemote(_options, Logger); try { Channel targetChannel = null; if (!string.IsNullOrEmpty(_options.Channel)) { targetChannel = await UxHelpers.ResolveSingleChannel(remote, _options.Channel); if (targetChannel == null) { return(Constants.ErrorCode); } } // Starting with the remote, get information on the asset name + version List <Asset> matchingAssets = (await remote.GetAssetsAsync(name: _options.Name, version: _options.Version)).ToList(); string queryDescriptionString = $"name '{_options.Name}'{(!string.IsNullOrEmpty(_options.Version) ? $" and version '{_options.Version}'" : "")}" + $"{(targetChannel != null ? $" on channel '{targetChannel.Name}'" : "")} in the last {_options.MaxAgeInDays} days"; // Only print the lookup string if the output type is text. if (_options.OutputFormat == DarcOutputType.text) { Console.WriteLine($"Looking up assets with {queryDescriptionString}"); } // Walk the assets and look up the corresponding builds, potentially filtering based on channel // if there is a target channel int maxAgeInDays = _options.MaxAgeInDays; var now = DateTimeOffset.Now; int checkedAssets = 0; List <(Asset asset, Build build)> matchingAssetsAfterDate = new List <(Asset, Build)>(); foreach (Asset asset in matchingAssets) { // Get build info for asset Build buildInfo = await remote.GetBuildAsync(asset.BuildId); if (now.Subtract(buildInfo.DateProduced).TotalDays > maxAgeInDays) { break; } checkedAssets++; if (targetChannel != null && !buildInfo.Channels.Any(c => c.Id == targetChannel.Id)) { continue; } matchingAssetsAfterDate.Add((asset, buildInfo)); } if (!matchingAssetsAfterDate.Any()) { Console.WriteLine($"No assets found with {queryDescriptionString}"); int remaining = matchingAssets.Count - checkedAssets; if (remaining > 0) { Console.WriteLine($"Skipping build lookup for {remaining} assets. Consider increasing --max-age to check the rest."); } return(Constants.ErrorCode); } switch (_options.OutputFormat) { case DarcOutputType.text: foreach ((Asset asset, Build build) in matchingAssetsAfterDate) { Console.WriteLine($"{asset.Name} @ {asset.Version}"); Console.Write(UxHelpers.GetTextBuildDescription(build)); Console.WriteLine("Locations:"); if (asset.Locations.Any()) { foreach (var location in asset.Locations) { if (location.IsValid) { Console.WriteLine($"- {location.Location} ({location.Type})"); } } } else { Console.WriteLine("- None"); } Console.WriteLine(); } break; case DarcOutputType.json: var assets = matchingAssetsAfterDate.Select(assetAndBuild => { return(new { name = assetAndBuild.asset.Name, version = assetAndBuild.asset.Version, build = UxHelpers.GetJsonBuildDescription(assetAndBuild.build), locations = assetAndBuild.asset.Locations.Select(location => location.Location) }); }); Console.WriteLine(JsonConvert.SerializeObject(assets, Formatting.Indented)); break; } return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, "Error: Failed to retrieve information about assets."); 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); } }
/// <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> /// 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> /// 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.Status == (int)HttpStatusCode.NotFound) { Console.WriteLine($"Subscription with id '{_options.Id}' was not found."); return(Constants.ErrorCode); } } else { if (!_options.HasAnyFilters()) { Console.WriteLine($"Please specify one or more filters to select which subscriptions should be triggered (see help)."); return(Constants.ErrorCode); } IEnumerable <Subscription> subscriptions = await _options.FilterSubscriptions(remote); if (!subscriptions.Any()) { Console.WriteLine("No subscriptions found matching the specified criteria."); return(Constants.ErrorCode); } subscriptionsToTrigger.AddRange(subscriptions); } if (_options.Build != 0) { var specificBuild = await remote.GetBuildAsync(_options.Build); if (specificBuild == null) { Console.WriteLine($"No build found in the BAR with id '{_options.Build}'"); return(Constants.ErrorCode); } // If the user specified repo and a build number, error out if anything doesn't match. if (!_options.SubscriptionParameterMatches(_options.SourceRepository, specificBuild.GitHubRepository)) { Console.WriteLine($"Build #{_options.Build} was made with repo {specificBuild.GitHubRepository} and does not match provided value ({_options.SourceRepository})"); return(Constants.ErrorCode); } Console.WriteLine($"Subscription updates will use Build # {_options.Build} instead of latest available"); } // Filter away subscriptions that are disabled List <Subscription> disabledSubscriptions = subscriptionsToTrigger.Where(s => !s.Enabled).ToList(); subscriptionsToTrigger = subscriptionsToTrigger.Where(s => s.Enabled).ToList(); if (disabledSubscriptions.Any()) { Console.WriteLine($"The following {disabledSubscriptions.Count} subscription(s) are disabled and will not be triggered"); foreach (var subscription in disabledSubscriptions) { Console.WriteLine($" {UxHelpers.GetSubscriptionDescription(subscription)}"); } } if (!subscriptionsToTrigger.Any()) { Console.WriteLine("No enabled subscriptions found matching the specified criteria."); return(Constants.ErrorCode); } 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)}"); } if (!UxHelpers.PromptForYesNo("Continue?")) { 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)}"); } if (_options.Build > 0) { await remote.TriggerSubscriptionAsync(subscription.Id.ToString(), _options.Build); } else { await remote.TriggerSubscriptionAsync(subscription.Id.ToString()); } } Console.WriteLine("done"); return(Constants.SuccessCode); } catch (AuthenticationException e) { Console.WriteLine(e.Message); return(Constants.ErrorCode); } 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); 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.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>() }); } string repository = _options.Repository; string branch = _options.Branch; // If in quiet (non-interactive mode), ensure that all options were passed, then // just call the remote API if (_options.Quiet) { if (string.IsNullOrEmpty(repository) || string.IsNullOrEmpty(branch)) { Logger.LogError($"Missing input parameters for merge policies. Please see command help or remove --quiet/-q for interactive mode"); return(Constants.ErrorCode); } } else { // Look up existing merge policies if the repository and branch were specified, and the user didn't // specify policies on the command line. In this case, they typically want to update if (!mergePolicies.Any() && !string.IsNullOrEmpty(repository) && !string.IsNullOrEmpty(branch)) { mergePolicies = (await remote.GetRepositoryMergePoliciesAsync(repository, branch)).ToList(); } // Help the user along with a form. We'll use the API to gather suggested values // from existing subscriptions based on the input parameters. SetRepositoryMergePoliciesPopUp initEditorPopUp = new SetRepositoryMergePoliciesPopUp("set-policies/set-policies-todo", Logger, repository, branch, mergePolicies, Constants.AvailableMergePolicyYamlHelp); UxManager uxManager = new UxManager(_options.GitLocation, Logger); int exitCode = uxManager.PopUp(initEditorPopUp); if (exitCode != Constants.SuccessCode) { return(exitCode); } repository = initEditorPopUp.Repository; branch = initEditorPopUp.Branch; mergePolicies = initEditorPopUp.MergePolicies; } IRemote verifyRemote = RemoteFactory.GetRemote(_options, repository, Logger); IEnumerable <RepositoryBranch> targetRepository = await verifyRemote.GetRepositoriesAsync(repository); if (targetRepository == null || !targetRepository.Any()) { Console.WriteLine($"The target repository '{repository}' doesn't have a Maestro installation. Aborting merge policy creation."); return(Constants.ErrorCode); } if (!(await UxHelpers.VerifyAndConfirmBranchExistsAsync(verifyRemote, repository, branch, !_options.Quiet))) { Console.WriteLine("Aborting merge policy creation."); return(Constants.ErrorCode); } try { await remote.SetRepositoryMergePoliciesAsync( repository, branch, mergePolicies); Console.WriteLine($"Successfully updated merge policies for {repository}@{branch}."); 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) { Logger.LogError($"Failed to set repository auto merge policies: {e.Response.Content}"); return(Constants.ErrorCode); } catch (Exception e) { Logger.LogError(e, $"Failed to set merge policies."); 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.Status == (int)HttpStatusCode.NotFound) { Console.WriteLine($"Subscription with id '{_options.Id}' was not found."); return(Constants.ErrorCode); } } else { if (!_options.HasAnyFilters()) { Console.WriteLine($"Please specify one or more filters to select which subscriptions should be triggered (see help)."); return(Constants.ErrorCode); } IEnumerable <Subscription> subscriptions = await _options.FilterSubscriptions(remote); if (!subscriptions.Any()) { 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)}"); } if (!UxHelpers.PromptForYesNo("Continue?")) { 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); } }