private async Task <string?> GetCorrelatedQueueNotificationUrlAsync()
        {
            // In the case where the publish build was queued by AutoBuilder, this finds the GitHub issue associated
            // with that queued build.

            GitHubClient           gitHubClient = new(new ProductHeaderValue("dotnet"));
            Credentials            token        = new(Options.GitOptions.AuthToken);
            RepositoryIssueRequest issueRequest = new()
            {
                Filter = IssueFilter.All,
                Since  = DateTimeOffset.Now - TimeSpan.FromDays(2)
            };

            issueRequest.Labels.Add(NotificationLabels.AutoBuilder);
            issueRequest.Labels.Add(NotificationLabels.GetRepoLocationLabel(Options.SourceRepo, Options.SourceBranch));

            gitHubClient.Credentials = token;
            IReadOnlyList <Octokit.Issue> issues = await gitHubClient.Issue.GetAllForRepository(
                Options.GitOptions.Owner, Options.GitOptions.Repo, issueRequest);

            foreach (Octokit.Issue issue in issues)
            {
                // Get the metadata embedded within the issue body, if any
                QueueInfo?queueInfo = NotificationHelper.GetNotificationMetadata <QueueInfo>(issue.Body);
                if (queueInfo?.BuildId == Options.BuildId)
                {
                    return(issue.HtmlUrl);
                }
            }

            return(null);
        }
        private async Task LogAndNotifyResultsAsync(
            Subscription subscription, IEnumerable <string> pathsToRebuild, WebApi.Build?queuedBuild, Exception?exception,
            IEnumerable <string>?inProgressBuilds, IEnumerable <string>?recentFailedBuilds)
        {
            StringBuilder notificationMarkdown = new();

            notificationMarkdown.AppendLine($"Subscription: {subscription}");
            notificationMarkdown.AppendLine("Paths to rebuild:");
            notificationMarkdown.AppendLine();

            foreach (string path in pathsToRebuild.OrderBy(path => path))
            {
                notificationMarkdown.AppendLine($"* `{path}`");
            }

            notificationMarkdown.AppendLine();

            string?category = null;

            if (queuedBuild is not null)
            {
                category = "Queued";
                string webLink = queuedBuild.GetWebLink();
                _loggerService.WriteMessage($"Queued build {webLink}");
                notificationMarkdown.AppendLine($"[Build Link]({webLink})");
            }
            else if (recentFailedBuilds is not null)
            {
                category = "Failed";

                StringBuilder builder = new();
                builder.AppendLine(
                    $"Due to recent failures of the following builds, a build will not be queued again for subscription '{subscription}':");
                builder.AppendLine();
                foreach (string buildUri in recentFailedBuilds)
                {
                    builder.AppendLine($"* {buildUri}");
                }

                builder.AppendLine();
                builder.AppendLine(
                    $"Please investigate the cause of the failures, resolve the issue, and manually queue a build for the Dockerfile paths listed above. You must manually tag the build with a tag named '{AzdoTags.AutoBuilder}' in order for AutoBuilder to recognize that a successful build has occurred.");

                string message = builder.ToString();

                _loggerService.WriteMessage(message);
                notificationMarkdown.AppendLine(message);
            }
            else if (inProgressBuilds is not null)
            {
                category = "Skipped";

                StringBuilder builder = new();
                builder.AppendLine($"The following in-progress builds were detected on the pipeline for subscription '{subscription}':");
                foreach (string buildUri in inProgressBuilds)
                {
                    builder.AppendLine(buildUri);
                }

                builder.AppendLine();
                builder.AppendLine("Queueing the build will be skipped.");

                string message = builder.ToString();

                _loggerService.WriteMessage(message);
                notificationMarkdown.AppendLine(message);
            }
            else if (exception != null)
            {
                category = "Failed";
                notificationMarkdown.AppendLine("An exception was thrown when attempting to queue the build:");
                notificationMarkdown.AppendLine();
                notificationMarkdown.AppendLine("```");
                notificationMarkdown.AppendLine(exception.ToString());
                notificationMarkdown.AppendLine("```");
            }
            else
            {
                throw new NotSupportedException("Unknown state");
            }

            string header = $"AutoBuilder - {category}";

            notificationMarkdown.Insert(0, $"# {header}{Environment.NewLine}{Environment.NewLine}");

            // Add metadata to the issue so it can be used programmatically
            QueueInfo queueInfo = new()
            {
                BuildId = queuedBuild?.Id
            };

            notificationMarkdown.AppendLine();
            notificationMarkdown.AppendLine(NotificationHelper.FormatNotificationMetadata(queueInfo));

            if (Options.GitOptions.AuthToken == string.Empty ||
                Options.GitOptions.Owner == string.Empty ||
                Options.GitOptions.Repo == string.Empty)
            {
                _loggerService.WriteMessage(
                    "Skipping posting of notification because GitHub auth token, owner, and repo options were not provided.");
            }
            else
            {
                await _notificationService.PostAsync($"{header} - {subscription}", notificationMarkdown.ToString(),
                                                     new string[]
                {
                    NotificationLabels.AutoBuilder,
                    NotificationLabels.GetRepoLocationLabel(subscription.Manifest.Repo, subscription.Manifest.Branch)
                }.AppendIf(NotificationLabels.Failure, () => exception is not null),
                                                     Options.GitOptions.GetRepoUrl().ToString(), Options.GitOptions.AuthToken, Options.IsDryRun);
            }
        }
        public override async Task ExecuteAsync()
        {
            StringBuilder notificationMarkdown           = new();
            string        buildUrl                       = string.Empty;
            Dictionary <string, TaskResult?> taskResults = Options.TaskNames
                                                           .ToDictionary(name => name, name => (TaskResult?)null);
            Dictionary <string, string> buildParameters = new();
            BuildResult overallResult = BuildResult.Succeeded;
            BuildReason buildReason   = BuildReason.None;
            string?     correlatedQueueNotificationUrl = null;

            if (!Options.IsDryRun)
            {
                (Uri baseUrl, VssCredentials credentials) = Options.AzdoOptions.GetConnectionDetails();
                using (IVssConnection connection = _connectionFactory.Create(baseUrl, credentials))
                    using (IProjectHttpClient projectHttpClient = connection.GetProjectHttpClient())
                        using (IBuildHttpClient buildClient = connection.GetBuildHttpClient())
                        {
                            TeamProject project = await projectHttpClient.GetProjectAsync(Options.AzdoOptions.Project);

                            TeamFoundation.Build.WebApi.Build build = await buildClient.GetBuildAsync(project.Id, Options.BuildId);

                            buildUrl    = build.GetWebLink();
                            buildReason = build.Reason;

                            // Get the build's queue-time parameters
                            if (build.Parameters is not null)
                            {
                                JObject parametersJson = JsonConvert.DeserializeObject <JObject>(build.Parameters);
                                foreach (KeyValuePair <string, JToken?> pair in parametersJson)
                                {
                                    buildParameters.Add(pair.Key, pair.Value?.ToString() ?? string.Empty);
                                }
                            }

                            overallResult = await GetBuildTaskResultsAsync(taskResults, buildClient, project);

                            correlatedQueueNotificationUrl = await GetCorrelatedQueueNotificationUrlAsync();
                        }
            }

            notificationMarkdown.AppendLine($"# Publish Results");
            notificationMarkdown.AppendLine();

            WriteSummaryMarkdown(notificationMarkdown, buildUrl, overallResult, buildReason, correlatedQueueNotificationUrl);
            notificationMarkdown.AppendLine();

            WriteTaskStatusesMarkdown(taskResults, notificationMarkdown);
            notificationMarkdown.AppendLine();

            WriteBuildParameters(buildParameters, notificationMarkdown);
            notificationMarkdown.AppendLine();

            WriteImagesMarkdown(notificationMarkdown);

            await _notificationService.PostAsync(
                $"Publish Result - {Options.SourceRepo}/{Options.SourceBranch}",
                notificationMarkdown.ToString(),
                new string[]
            {
                NotificationLabels.Publish,
                NotificationLabels.GetRepoLocationLabel(Options.SourceRepo, Options.SourceBranch)
            }.AppendIf(NotificationLabels.Failure, () => overallResult == BuildResult.Failed),
                Options.GitOptions.GetRepoUrl().ToString(),
                Options.GitOptions.AuthToken,
                Options.IsDryRun);
        }