tools/sdk-ai-bots/AzureSdkQaBot/GithubPrActions.cs (927 lines of code) (raw):
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Microsoft.TeamsAI;
using Microsoft.TeamsAI.AI.Action;
using System.Text.RegularExpressions;
using Octokit;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.SemanticKernel;
using AzureSdkQaBot.Model;
namespace AzureSdkQaBot
{
public class GitHubPrActions
{
private readonly IGitHubClient _gitHubClient;
private readonly ILogger _logger;
private IKernel _kernel;
private const string ReviewWorkflow_MgmtPlane = "https://aka.ms/azsdk/pr-diagram";
private const string ReviewWorkflow_DataPlane = "https://eng.ms/docs/products/azure-developer-experience/design/api-specs-pr/api-specs-pr?tabs=dataplane";
private IList<string> _referencesMgmtPlane = new List<string>() { $"[Workflow Reference]({ReviewWorkflow_MgmtPlane})" };
private IList<string> _referencesDataPlane = new List<string>() { $"[Workflow Reference]({ReviewWorkflow_DataPlane})" };
private QuestionAnsweringActions _questionAnsweringActions;
// DONOT use below fields directly and use corresponding Get{} method to save the GitHub api call times because they're not initialized by default
private PullRequest? _pullRequest;
private IReadOnlyList<Label>? _labels;
// private IOrderedEnumerable<CheckRun>? _checkRun;
public GitHubPrActions(Application<AppState, AppStateManager> application, IKernel kernel, IGitHubClient client, ILogger logger)
{
_gitHubClient = client;
_logger = logger;
_questionAnsweringActions = new QuestionAnsweringActions(application, kernel, _logger);
}
[Action("NonGitHubPRHandler")]
public async Task<bool> NonGitHubPRHandler([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
this._logger.LogInformation($"NonGitHubPRHandler:");
string prLink = (string)entities["prLink"];
if (string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"NonGitHubPRHandler: PR link:{prLink}. Query: {query}");
string reply = "";
string action = "";
if (!prLink.Contains("https://github.com/"))
{
reply = Constants.Message_Error_NonGithub_PR;
}
// add support message for tool failures
reply = AddSupportMessageForTools(turnContext, reply);
// call QA function
(string additionalAnswer, List<string>? relevancies) = await _questionAnsweringActions.QuestionAnsweringHandler(turnContext, turnState);
// get the relevance links based on user input or PR labels to differentiate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext);
Attachment card;
if (additionalAnswer.StartsWith("Sorry, I do not know"))
{
additionalAnswer = "";
relevancies = null;
}
card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, additionalAnswer, references, relevancies, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
this._logger.LogError($"NonGitHubPRHandler: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
[Action("PullRequestReviewNextStep")]
public async Task<bool> PullRequestReviewNextStep([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
string prLink = (string)entities["prLink"];
if (string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"PullRequestReviewNextStep: PR link:{prLink}. Query: {query}");
(string org, string repo, int prNum) = GetPrInfoFromPrLink(prLink);
string reply = "";
string action = "";
// Get pull request info
if (prNum != 0)
{
(bool isPrMerged, reply) = await IsPrMerged(org, repo, prNum);
if (reply == Constants.Message_Error_Null_PR)
{
reply += $" {prLink}";
}
else if (!isPrMerged)
{
// Check the pull request labels, check result, and prompt next step
(reply, action) = await CheckPrStatus(org, repo, prNum);
}
}
// add support message for tool failures
reply = AddSupportMessageForTools(turnContext, reply);
// call QA function
(string additionalAnswer, List<string>? relevancies) = await _questionAnsweringActions.QuestionAnsweringHandler(turnContext, turnState);
// get the relevance links based on user input or PR labels to differenciate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext, org, repo, prNum);
Attachment card;
if (additionalAnswer.StartsWith("Sorry, I do not know"))
{
additionalAnswer = "";
relevancies = null;
}
card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, additionalAnswer, references, relevancies, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
this._logger.LogError($"PullRequestReviewNextStep: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
[Action("MergePullRequest")]
public async Task<bool> MergePullRequest([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
string prLink = "";
if (entities != null)
{
prLink = (string)entities["prLink"];
}
if (entities == null || string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"MergePullRequest: PR link:{prLink}. Query: {query}");
(string org, string repo, int prNum) = GetPrInfoFromPrLink(prLink);
string reply = "";
string action = "";
// Get pull request info
if (prNum != 0)
{
(bool isPrMerged, reply) = await IsPrMerged(org, repo, prNum);
if (reply == Constants.Message_Error_Null_PR)
{
reply += $" {prLink}";
}
else if(!isPrMerged)
{
// Try to merge the PR
(reply, action) = await DoPrMerge(org, repo, prNum);
}
}
// get the relevance links based on user input or PR labels to differenciate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext, org, repo, prNum);
var card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, "", references, null, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
Console.WriteLine("Error:" + ex);
this._logger.LogError($"MergePullRequest: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
[Action("PrBreakingChangeReview")]
public async Task<bool> PullRequestBreakingChangeReview([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
string prLink = (string)entities["prLink"];
if (string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"PrBreakingChangeReview: PR link:{prLink}. Query: {query}");
(string org, string repo, int prNum) = GetPrInfoFromPrLink(prLink);
string reply = "";
string action = "";
// Get pull request info
if (prNum != 0)
{
(bool isPrMerged, reply) = await IsPrMerged(org, repo, prNum);
if (reply == Constants.Message_Error_Null_PR)
{
reply += $" {prLink}";
}
else if (!isPrMerged)
{
IReadOnlyList<Label> labels = await GetPrLabels(org, repo, prNum);
bool isMgmtPlane = false;
if (labels.Any(x => x.Name == Constants.Label_ResourceManager))
{
isMgmtPlane = true;
}
// check breaking change
(bool completeBcReview, reply, action) = CheckBreakingChangeReview(labels);
if (completeBcReview || reply == "")
{
// sdk breaking change review
if (!labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go || x.Name == Constants.Label_SDKBreakingChange_Python || x.Name == Constants.Label_SDKBreakingChange_PythonTrack2 || x.Name == Constants.Label_SDKBreakingChange_JavaScript))
{
reply = Constants.Message_SDKBreakingChangeReview_NotNeeded;
}
else if ((labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go_Approval))
|| (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python || x.Name == Constants.Label_SDKBreakingChange_PythonTrack2) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python_Approval))
|| (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript_Approval)))
{
bool completeArmReview = false;
if (isMgmtPlane)
{
(completeArmReview, reply, action) = CheckArmReview(labels);
}
if (isMgmtPlane && !completeArmReview && reply != "")
{
// arm review isn't finished
reply = Constants.Message_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
action = Constants.Action_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
}
// arm review completes or not needed
else if (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go_Approval))
{
reply = Constants.Message_SDKBreakingChangeGo_NotFinished;
action = Constants.Action_SDKBreakingChangeGo_NotFinished;
}
else if (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python_Approval))
{
reply = Constants.Message_SDKBreakingChangePython_NotFinished;
action = Constants.Action_SDKBreakingChangePython_NotFinished;
}
else if (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript_Approval))
{
reply = Constants.Message_SDKBreakingChangeJavaScript_NotFinished;
action = Constants.Action_SDKBreakingChangeJavaScript_NotFinished;
}
}
else
{
reply = Constants.Message_BreakingChangeReview_Finished;
action = Constants.Action_BreakingChangeReview_Finished;
}
}
}
// add support message for tool failures
reply = AddSupportMessageForTools(turnContext, reply);
}
// call QA function
(string additionalAnswer, List<string>? relevancies) = await _questionAnsweringActions.QuestionAnsweringHandler(turnContext, turnState);
// get the relevance links based on user input or PR labels to differenciate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext, org, repo, prNum);
Attachment card;
if (additionalAnswer.StartsWith("Sorry, I do not know"))
{
additionalAnswer = "";
relevancies = null;
}
card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, additionalAnswer, references, relevancies, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
Console.WriteLine("Error:" + ex);
this._logger.LogError($"PrBreakingChangeReview: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
[Action("PrBreakingChangeReview-Go")]
public async Task<bool> PullRequestGoSdkBreakingChangeReview([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
string prLink = (string)entities["prLink"];
if (string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"PrBreakingChangeReview-Go: PR link:{prLink}. Query: {query}");
(string org, string repo, int prNum) = GetPrInfoFromPrLink(prLink);
string reply = "";
string action = "";
// Get pull request info
if (prNum != 0)
{
(bool isPrMerged, reply) = await IsPrMerged(org, repo, prNum);
if (reply == Constants.Message_Error_Null_PR)
{
reply += $" {prLink}";
}
else if (!isPrMerged)
{
IReadOnlyList<Label> labels = await GetPrLabels(org, repo, prNum);
// check breaking change
(bool completeBcReview, reply, action) = CheckBreakingChangeReview(labels);
if (completeBcReview || reply == "")
{
// sdk breaking change review
if (!labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go))
{
reply = Constants.Message_SDKBreakingChangeReview_NotNeeded;
}
else if (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Go_Approval))
{
(bool completeArmReview, reply, action) = CheckArmReview(labels);
if (!completeArmReview && reply != "")
{
// arm review isn't finished
reply = Constants.Message_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
action = Constants.Action_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
}
else
{
reply = Constants.Message_SDKBreakingChangeGo_NotFinished;
action = Constants.Action_SDKBreakingChangeGo_NotFinished;
}
}
else
{
reply = Constants.Message_GoSdkReview_Finished;
action = Constants.Action_BreakingChangeReview_Finished;
}
}
}
// add support message for tool failures
reply = AddSupportMessageForTools(turnContext, reply);
}
// call QA function
(string additionalAnswer, List<string>? relevancies) = await _questionAnsweringActions.QuestionAnsweringHandler(turnContext, turnState);
// get the relevance links based on user input or PR labels to differenciate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext, org, repo, prNum);
Attachment card;
if (additionalAnswer.StartsWith("Sorry, I do not know"))
{
additionalAnswer = "";
relevancies = null;
}
card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, additionalAnswer, references, relevancies, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
Console.WriteLine("Error:" + ex);
this._logger.LogError($"PrBreakingChangeReview-Go: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
[Action("PrBreakingChangeReview-Python")]
public async Task<bool> PullRequestPythonSdkBreakingChangeReview([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
string prLink = (string)entities["prLink"];
if (string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"PrBreakingChangeReview-Python: PR link:{prLink}. Query: {query}");
(string org, string repo, int prNum) = GetPrInfoFromPrLink(prLink);
string reply = "";
string action = "";
// Get pull request info
if (prNum != 0)
{
(bool isPrMerged, reply) = await IsPrMerged(org, repo, prNum);
if (reply == Constants.Message_Error_Null_PR)
{
reply += $" {prLink}";
}
else if (!isPrMerged)
{
IReadOnlyList<Label> labels = await GetPrLabels(org, repo, prNum);
// check breaking change
(bool completeBcReview, reply, action) = CheckBreakingChangeReview(labels);
if (completeBcReview || reply == "")
{
// sdk breaking change review
if (!labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python))
{
reply = Constants.Message_SDKBreakingChangeReview_NotNeeded;
}
else if (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python || x.Name == Constants.Label_SDKBreakingChange_PythonTrack2) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_Python_Approval))
{
(bool completeArmReview, reply, action) = CheckArmReview(labels);
if (!completeArmReview && reply != "")
{
// arm review isn't finished
reply = Constants.Message_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
action = Constants.Action_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
}
else
{
reply = Constants.Message_SDKBreakingChangePython_NotFinished;
action = Constants.Action_SDKBreakingChangePython_NotFinished;
}
}
else
{
reply = Constants.Message_PythonSdkReview_Finished;
action = Constants.Action_BreakingChangeReview_Finished;
}
}
}
// add support message for tool failures
reply = AddSupportMessageForTools(turnContext, reply);
}
// call QA function
(string additionalAnswer, List<string>? relevancies) = await _questionAnsweringActions.QuestionAnsweringHandler(turnContext, turnState);
// get the relevance links based on user input or PR labels to differenciate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext, org, repo, prNum);
Attachment card;
if (additionalAnswer.StartsWith("Sorry, I do not know"))
{
additionalAnswer = "";
relevancies = null;
}
card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, additionalAnswer, references, relevancies, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
Console.WriteLine("Error:" + ex);
this._logger.LogError($"PrBreakingChangeReview-Python: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
[Action("PrBreakingChangeReview-JS")]
public async Task<bool> PullRequestJSSdkBreakingChangeReview([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] AppState turnState, [ActionEntities] Dictionary<string, object> entities)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnState == null)
{
throw new ArgumentNullException(nameof(turnState));
}
string prLink = (string)entities["prLink"];
if (string.IsNullOrEmpty(prLink) || !prLink.Contains("https://"))
{
await _questionAnsweringActions.QuestionAnswering(turnContext, turnState);
return false;
}
// redact query to less than 60 characters
string query = turnContext.Activity.Text;
if (query.Length > 60)
{
query = turnContext.Activity.Text.Substring(0, 57) + "...";
}
this._logger.LogInformation($"PrBreakingChangeReview-JS: PR link:{prLink}. Query: {query}");
(string org, string repo, int prNum) = GetPrInfoFromPrLink(prLink);
string reply = "";
string action = "";
// Get pull request info
if (prNum != 0)
{
(bool isPrMerged, reply) = await IsPrMerged(org, repo, prNum);
if (reply == Constants.Message_Error_Null_PR)
{
reply += $" {prLink}";
}
else if (!isPrMerged)
{
IReadOnlyList<Label> labels = await GetPrLabels(org, repo, prNum);
// check breaking change
(bool completeBcReview, reply, action) = CheckBreakingChangeReview(labels);
if (completeBcReview || reply == "")
{
// sdk breaking change review
if (!labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript))
{
reply = Constants.Message_SDKBreakingChangeReview_NotNeeded;
}
else if (labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript) && !labels.Any(x => x.Name == Constants.Label_SDKBreakingChange_JavaScript_Approval))
{
(bool completeArmReview, reply, action) = CheckArmReview(labels);
if (!completeArmReview && reply != "")
{
// arm review isn't finished
reply = Constants.Message_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
action = Constants.Action_ArmReview_NotFinished_BeforeSdkBreakingChangeReview;
}
else
{
reply = Constants.Message_SDKBreakingChangeJavaScript_NotFinished;
action = Constants.Action_SDKBreakingChangeJavaScript_NotFinished;
}
}
else
{
reply = Constants.Message_JavaScriptSdkReview_Finished;
action = Constants.Action_BreakingChangeReview_Finished;
}
}
}
// add support message for tool failures
reply = AddSupportMessageForTools(turnContext, reply);
}
// call QA function
(string additionalAnswer, List<string>? relevancies) = await _questionAnsweringActions.QuestionAnsweringHandler(turnContext, turnState);
// get the relevance links based on user input or PR labels to differenciate the mgmt plane and data plane
IList<string> references = await GetRelevanceLinks(turnContext, org, repo, prNum);
Attachment card;
if (additionalAnswer.StartsWith("Sorry, I do not know"))
{
additionalAnswer = "";
relevancies = null;
}
card = await CardBuilder.NewPRAndQAAttachment(query, reply, action, additionalAnswer, references, relevancies, CancellationToken.None);
try
{
await turnContext.SendActivityAsync(MessageFactory.Attachment(card), CancellationToken.None);
}
catch (Exception ex)
{
await turnContext.TraceActivityAsync("SendActivityError", ex, "The bot received an error when send message to Teams.");
Console.WriteLine("Error:" + ex);
this._logger.LogError($"PrBreakingChangeReview-JS: PR link:{prLink}. Query:{query}. Error: {ex}");
}
// End the current chain
return false;
}
private static (string org, string repo, int prNum) GetPrInfoFromQuery(string query)
{
string org = "";
string repo = "";
int prNum = 0;
if (!string.IsNullOrEmpty(query))
{
string pattern = @"(https?://[\w.-]+)";
Match match = Regex.Match(query, pattern);
if (match.Success)
{
string url = match.Groups[1].Value;
Console.WriteLine(url);
pattern = @"https://github.com/(?<org>[^/]+)/(?<repo>[^/]+)/pull/(?<pr>\d+)";
match = Regex.Match(url, pattern);
if (match.Success)
{
org = match.Groups["org"].Value;
repo = match.Groups["repo"].Value;
prNum = int.TryParse(match.Groups["pr"].Value, out int pr) ? pr : 0;
}
}
}
return (org, repo, prNum);
}
private static (string org, string repo, int prNum) GetPrInfoFromPrLink(string prLink)
{
string org = "";
string repo = "";
int prNum = 0;
if (!string.IsNullOrEmpty(prLink))
{
string pattern = @"https://github.com/(?<org>[^/]+)/(?<repo>[^/]+)/pull/(?<pr>\d+)";
Match match = Regex.Match(prLink, pattern);
if (match.Success)
{
org = match.Groups["org"].Value;
repo = match.Groups["repo"].Value;
prNum = int.TryParse(match.Groups["pr"].Value, out int pr) ? pr : 0;
}
}
return (org, repo, prNum);
}
private static (bool isComplete, string reply, string action) CheckArmReview(IReadOnlyList<Label> labels)
{
string result = "";
string action = "";
bool isComplete = false;
if (labels.Any(x => x.Name == "ARMReview"))
{
if (labels.Any(x => x.Name == "WaitForARMFeedback") && !labels.Any(x => x.Name == "ARMSignedOff"))
{
result = "This pull request has been labeled with 'WaitForARMFeedback' and is currently in the ARM review queue. The on-call ARM reviewer will review it automatically.";
action = "Wait for the ARM reviewer's feedback.";
}
else if (labels.Any(x => x.Name == "ARMChangesRequested"))
{
result = "There is blocking feedback in this pull request. You will need to address these issues before the ARM reviewer can re-evaluate the changes.";
action = "Address the review feedback then remove 'ARMChangesRequested' label.";
}
else if (labels.Any(x => x.Name == "ARMSignedOff"))
{
isComplete = true;
}
}
return (isComplete, result, action);
}
private static (bool isComplete, string reply, string action) CheckBreakingChangeReview(IReadOnlyList<Label> labels)
{
string result = "";
string action = "";
bool isComplete = false;
if (labels.Any(x => x.Name == Constants.Label_APIBreakingChange || x.Name == Constants.Label_APINewApiVersionRequired))
{
if (!labels.Any(x => x.Name == Constants.Label_APIBreakingChangeApproval))
{
result = "This pull request has been labeled with 'BreakingChangeReviewRequired' and need to get the breaking changes reviewed by the breaking change review board.";
action = Constants.Action_BreakingChangeReview;
}
else
{
isComplete = true;
}
}
return (isComplete, result, action);
}
private static (bool isComplete, string reply, string action) CheckSdkBreakingChangeReview(IReadOnlyList<Label> labels)
{
string result = "";
string action = "";
bool isComplete = false;
if (labels.Any(x => x.Name == "CI-BreakingChange-Go" || x.Name == "CI-BreakingChange-Python" || x.Name == "CI-BreakingChange-Python-Track2" || x.Name == "CI-BreakingChange-JavaScript"))
{
if ((labels.Any(x => x.Name == "CI-BreakingChange-Go") && !labels.Any(x => x.Name == "Approved-SdkBreakingChange-Go"))
|| (labels.Any(x => x.Name == "CI-BreakingChange-Python" || x.Name == "CI-BreakingChange-Python-Track2") && !labels.Any(x => x.Name == "Approved-SdkBreakingChange-Python"))
|| (labels.Any(x => x.Name == "CI-BreakingChange-JavaScript") && !labels.Any(x => x.Name == "Approved-SdkBreakingChange-JavaScript")))
{
result = "This pull request is flagged with at least one label prefixed with 'CI-BreakingChange-', you must get an approval for these breaking changes.";
action = "No action needs to take for SDK breaking change review. This PR is in the SDK breaking change review queue and the expected time of review completion is two business days.";
}
else
{
isComplete = true;
}
}
return (isComplete, result, action);
}
private async Task<IReadOnlyList<Label>> GetPrLabels(string org, string repo, int prNum)
{
if (_labels == null)
{
// Get the labels of the pull request
_labels = await _gitHubClient.Issue.Labels.GetAllForIssue(org, repo, prNum);
}
return _labels;
}
private async Task<IOrderedEnumerable<CheckRun>> GetCheckRuns(string org, string repo, int prNum)
{
PullRequest pr = await GetPullRequest(org, repo, prNum);
// Get check runs and validate the requirement check is success
var checkRuns = await _gitHubClient.Check.Run.GetAllForReference(org, repo, pr.Head.Sha);
return checkRuns.CheckRuns.OrderByDescending(cr => cr.CompletedAt);
}
private async Task<(bool, string)> IsPrMerged(string org, string repo, int prNum)
{
bool isMerged = false;
string reply = "";
PullRequest pr = await GetPullRequest(org, repo, prNum);
if (pr == null)
{
reply = Constants.Message_Error_Null_PR;
}
else if (pr.Merged || pr.State == "Closed")
{
isMerged = true;
reply = Constants.Message_PrIsMerged;
}
return (isMerged, reply);
}
private async Task<(string, string)> CheckPrStatus(string org, string repo, int prNum)
{
string result = "";
string action = "";
// Get the labels of the pull request
var labels = await GetPrLabels(org, repo, prNum);
// 1. breaking change review
(bool completeBcReview, string reply, action) = CheckBreakingChangeReview(labels);
if (!completeBcReview && reply != "")
{
result = reply;
}
else
{
if (labels.Any(x => x.Name == Constants.Label_ResourceManager))
{
// 2. arm review
(bool completeArmReview, reply, action) = CheckArmReview(labels);
if (!completeArmReview)
{
result = reply;
}
else
{
// 3. sdk breaking change review
(bool completesdkBcReview, reply, action) = CheckSdkBreakingChangeReview(labels);
if (!completesdkBcReview && reply != "")
{
result = reply;
}
}
}
}
// 4. required CI checks
(string checkResult, string checkAction) = await CheckRequiredCIChecks(org, repo, prNum);
if (checkResult != "")
{
// append the result and action
result = result + checkResult;
action = action + checkAction;
}
if (result == "" && action == "")
{
result = Constants.Message_Review_Finished;
if (labels.Any(x => x.Name == Constants.Label_DataPlane))
{
action = Constants.Action_RequestMerge_DataPlane;
}
else
{
action = Constants.Action_RequestMerge_MgmtPlane;
}
}
return (result, action);
}
private async Task<PullRequest> GetPullRequest(string org, string repo, int prNum)
{
if (_pullRequest == null)
{
try
{
_pullRequest = await _gitHubClient.PullRequest.Get(org, repo, prNum);
}
catch (Exception ex)
{
Console.WriteLine($"GetPullRequest with org:{org}, repo:{repo}, pull request number:{prNum}. Errors:{ex}");
this._logger.LogError($"GetPullRequest: org:{org}, repo:{repo}, pull request number:{prNum} Error: {ex}");
}
}
return _pullRequest;
}
private async Task<(string, string)> CheckRequiredCIChecks(string org, string repo, int prNum)
{
string result = "";
string action = "";
// Get check runs
IOrderedEnumerable<CheckRun> checkRuns = await GetCheckRuns(org, repo, prNum);
foreach (CheckRun checkRun in checkRuns)
{
if (checkRun.Name == Constants.CheckName_MergeRequirement)
{
if (checkRun.Conclusion == "Failure")
{
result = $"The CI check for '{Constants.CheckName_MergeRequirement}' does not succeed, it must be passed to proceed the merge.";
action = $"Please add the comment '/azp run' to the PR to trigger a re-run of the pipeline. If the re-run still doesn't work, please reach out to **Konrad Jamrozik** for assistance.";
if (checkRun.Output.Text.Contains("required checks are failing"))
{
result = "This PR has required CI check failures, it must be fixed to proceed the merge.";
action = $"In addition, please see [the check result]({checkRun.HtmlUrl}) to fix the required check failures.";
}
}
else if (checkRun.Conclusion == null)
{
result = $"The CI check for '{Constants.CheckName_MergeRequirement}' is not completing. You can view [the results page]({checkRun.HtmlUrl}).";
action = $"Please add the comment '/azp run' to the PR to trigger a re-run of the pipeline. If the re-run still doesn't work, please reach out to **Konrad Jamrozik** for assistance.";
}
break;
}
}
return (result, action);
}
private async Task<(string, string)> DoPrMerge(string org, string repo, int prNum)
{
bool readyForMerge = false;
// Get check runs and validate the requirement check is success
IOrderedEnumerable<CheckRun> checkRuns = await GetCheckRuns(org, repo, prNum);
string result = "Sorry, the pull request cannot be merged due to failing the automated merging requirements.";
string action = $"Please check the '{Constants.CheckName_MergeRequirement}' check result for details.";
foreach (CheckRun checkRun in checkRuns)
{
if (checkRun.Name == Constants.CheckName_MergeRequirement)
{
if (checkRun.Conclusion == "success")
{
readyForMerge = true;
}
else
{
result = checkRun.Output.Title;
action = $"For more information, please see [the check result]({checkRun.HtmlUrl}).";
}
break;
}
}
// Check the label doesn't have 'DONOTMERGE'
if (readyForMerge)
{
// Do the PR merge - comment out below merge operation in order to ignore real merge when validate with real user queries.
/*var mergeResult = await _gitHubClient.PullRequest.Merge(org, repo, prNum, new MergePullRequest());
if (mergeResult.Merged)
{
result = "I'm pleased to inform you that this pull request has been successfully merged.";
action = "Cheer!";
}
else
{
result = $"I'm sorry to inform you that this pull request cannot be merged at this time. The reason is: {mergeResult.Message}";
action = "Retry";
}*/
result = "Testing phase - ignore merge while validate with real user queries!";
}
return (result, action);
}
public static string AddSupportMessageForTools(ITurnContext turnContext, string reply)
{
string query = turnContext.Activity.Text;
if (!string.IsNullOrEmpty(query))
{
query = query.ToLower();
if (query.Contains("avocado"))
{
reply += Constants.Message_FurtherHelp_Avocado;
}
else if (query.Contains("lintdiff") || query.Contains("lint diff") || query.Contains("lintrpaas"))
{
reply += Constants.Message_FurtherHelp_LintTool;
}
else if (query.Contains("modelvalidation") || query.Contains("model validation") || query.Contains("semanticvalidation") || query.Contains("semantic validation"))
{
reply += Constants.Message_FurtherHelp_Oav;
}
else if (query.Contains("breaking change tool") || query.Contains("openapi diff") || query.Contains("breaking change cross version"))
{
reply += Constants.Message_FurtherHelp_Oad;
}
else if (query.Contains("typespec validation") || query.Contains("type spec validation"))
{
reply += Constants.Message_FurtherHelp_TypeSpecValidation;
}
else if (query.Contains("apidocpreview") || query.Contains("api doc preview"))
{
reply += Constants.Message_FurtherHelp_ApiDocPreview;
}
else if (query.Contains("apiview") || query.Contains("api view"))
{
reply += Constants.Message_FurtherHelp_ApiView;
}
else if (query.Contains("azure-resource-manager-schemas"))
{
reply += Constants.Message_FurtherHelp_ApiView;
}
else if (query.Contains("azure-powershell"))
{
reply += Constants.Message_FurtherHelp_Powershell;
}
else if (query.Contains("azure-sdk-for-go") || query.Contains("go sdk"))
{
reply += Constants.Message_FurtherHelp_GoSdk;
}
else if (query.Contains("azure-sdk-for-python") || query.Contains("azure-sdk-for-python-track2") || query.Contains("python sdk"))
{
reply += Constants.Message_FurtherHelp_PythonSdk;
}
else if (query.Contains("azure-sdk-for-java") || query.Contains("java sdk"))
{
reply += Constants.Message_FurtherHelp_JavaSdk;
}
else if (query.Contains("azure-sdk-for-js") || query.Contains("js sdk"))
{
reply += Constants.Message_FurtherHelp_JsSdk;
}
else if (query.Contains("azure-sdk-for-net-track2") || query.Contains("azure-sdk-for-net") || query.Contains("dotnet sdk") || query.Contains(".net sdk"))
{
reply += Constants.Message_FurtherHelp_DotnetSdk;
}
}
return reply;
}
private async Task<IList<string>> GetRelevanceLinks(ITurnContext turnContext, string org = "", string repo = "", int prNum = 0)
{
// default value set to mgmt plane relevant links
IList<string> relevanceLinks = _referencesMgmtPlane;
if (!string.IsNullOrEmpty(org) && !string.IsNullOrEmpty(repo) && prNum != 0)
{
IReadOnlyList<Label> labels = await GetPrLabels(org, repo, prNum);
if (labels.Any(x => x.Name == Constants.Label_DataPlane))
{
// check labels in the PR
relevanceLinks = _referencesDataPlane;
}
}
else
{
// check user input
string input = GetUserQueryFromContext(turnContext).ToLower();
if (input.Contains("data plane") || input.Contains("data-plane"))
{
relevanceLinks = _referencesDataPlane;
}
}
return relevanceLinks;
}
public static string GetUserQueryFromContext(ITurnContext turnContext)
{
string input = turnContext.Activity.Text; ;
if (turnContext.Activity.Attachments.Count > 0)
{
foreach (Attachment? attachment in turnContext.Activity.Attachments)
{
if (attachment.ContentType == "text/html")
{
input = (string)attachment.Content;
break;
}
}
}
return input;
}
}
}