Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Maestro.Data.Migrations
{
/// <inheritdoc />
public partial class AddSubscriptionOutcomePrUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PrUrl",
table: "SubscriptionOutcomes",
type: "nvarchar(max)",
nullable: true);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PrUrl",
table: "SubscriptionOutcomes");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128);

SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
Expand Down Expand Up @@ -581,6 +581,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<string>("Message")
.HasColumnType("nvarchar(max)");

b.Property<string>("PrUrl")
.HasColumnType("nvarchar(max)");

b.Property<Guid>("SubscriptionId")
.HasColumnType("uniqueidentifier");

Expand Down
2 changes: 2 additions & 0 deletions src/Maestro/Maestro.Data/Models/SubscriptionOutcome.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class SubscriptionOutcome
public string Message { get; set; }

public SubscriptionOutcomeType Type { get; set; }

public string PrUrl { get; set; }
}

public enum SubscriptionOutcomeType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.DotNet.ProductConstructionService.Client.Models
{
public partial class SubscriptionTriggerOutcome
{
public SubscriptionTriggerOutcome(Guid subscriptionId, int buildId, DateTimeOffset date, Models.OutcomeType type, string operationId, string message, string sourceRepository, string targetRepository, string targetBranch)
public SubscriptionTriggerOutcome(Guid subscriptionId, int buildId, DateTimeOffset date, Models.OutcomeType type, string operationId, string message, string sourceRepository, string targetRepository, string targetBranch, string prUrl)
{
SubscriptionId = subscriptionId;
BuildId = buildId;
Expand All @@ -19,6 +19,7 @@ public SubscriptionTriggerOutcome(Guid subscriptionId, int buildId, DateTimeOffs
SourceRepository = sourceRepository;
TargetRepository = targetRepository;
TargetBranch = targetBranch;
PrUrl = prUrl;
}

[JsonProperty("operationId")]
Expand Down Expand Up @@ -48,6 +49,9 @@ public SubscriptionTriggerOutcome(Guid subscriptionId, int buildId, DateTimeOffs
[JsonProperty("targetBranch")]
public string TargetBranch { get; }

[JsonProperty("prUrl")]
public string PrUrl { get; }

[JsonIgnore]
public bool IsValid
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public SubscriptionTriggerOutcome(Maestro.Data.Models.SubscriptionOutcome other,
SourceRepository = sourceRepository;
TargetRepository = targetRepository;
TargetBranch = targetBranch;
PrUrl = other.PrUrl;
}

public string OperationId { get; }
Expand All @@ -44,6 +45,8 @@ public SubscriptionTriggerOutcome(Maestro.Data.Models.SubscriptionOutcome other,
public string TargetRepository { get; }

public string TargetBranch { get; }

public string PrUrl { get; }
}

public enum OutcomeType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@
<span class="status-dot @GetStatusClass(context.Type)"></span>@GetStatusText(context.Type)
</FluentLabel>
</TemplateColumn>
<TemplateColumn Title="Pull Request" Width="200px" Align="Align.Start">
<FluentLabel>
@if (!string.IsNullOrEmpty(context.PrUrl))
{
<FluentAnchor Href="@context.PrUrl" Target="_blank" Appearance="Appearance.Hypertext">View Pull Request</FluentAnchor>
Comment thread
adamzip marked this conversation as resolved.
Outdated
}
</FluentLabel>
</TemplateColumn>
<TemplateColumn Title="Message" Width="2fr" Align="Align.Start">
<span class="outcome-message" title="@context.Message">@foreach (var segment in StringExtensions.SplitIntoTextAndUrls(context.Message))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected override async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdat
await _stateManager.SetCheckReminderAsync(pr, prInfo!, isCodeFlow: true);
await _stateManager.UnsetUpdateReminderAsync(isCodeFlow: true);
return new SubscriptionUpdateResult(
$"The existing PR ({GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}) is already up to date with source commit {update.SourceSha}",
$"The existing PR is already up to date with source repo (commit {update.SourceSha})",
Maestro.Data.Models.SubscriptionOutcomeType.NoUpdate);
}

Expand All @@ -100,7 +100,7 @@ protected override async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdat
await _stateManager.SetCheckReminderAsync(pr, prInfo!, isCodeFlow: true);
await _stateManager.UnsetUpdateReminderAsync(isCodeFlow: true);
return new SubscriptionUpdateResult(
$"The existing codeflow PR ({GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}) was not updated because it is currently blocked from future updates.",
"The existing codeflow PR is currently blocked from future updates",
Maestro.Data.Models.SubscriptionOutcomeType.NotUpdatable);
}

Expand Down Expand Up @@ -142,8 +142,8 @@ protected override async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdat
if (!codeFlowRes.HadUpdates)
{
var msg = pr != null
? $"There were no codeflow updates for the existing PR {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}"
: "Codeflow PR not created: there were no updates for this subscription";
? "No source code updates detected"
: "Codeflow PR not created: no source code updates detected";
return new SubscriptionUpdateResult(msg, Maestro.Data.Models.SubscriptionOutcomeType.NoUpdate);
}

Expand Down Expand Up @@ -181,7 +181,7 @@ protected override async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdat
upstreamRepoDiffs,
isUnsafeFlow);
return new SubscriptionUpdateResult(
$"Conflict resolution is required by user for PR {GitRepoUrlUtils.TurnApiUrlToWebsite(prUrl)}",
"Conflict resolution is required by user",
Maestro.Data.Models.SubscriptionOutcomeType.HasConflict);
}

Expand Down Expand Up @@ -211,7 +211,7 @@ protected override async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdat
}

return new SubscriptionUpdateResult(
$"New codeflow PR created: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}.",
"New codeflow PR created",
Maestro.Data.Models.SubscriptionOutcomeType.Updated);
}
else
Expand All @@ -231,7 +231,7 @@ await UpdateCodeFlowPullRequestAsync(
upstreamRepoDiffs);

return new SubscriptionUpdateResult(
$"Existing codeflow PR has been updated: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}.",
string.Empty,
Maestro.Data.Models.SubscriptionOutcomeType.Updated);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ protected override async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdat

await _stateManager.UnsetUpdateReminderAsync(isCodeFlow: false);
return new SubscriptionUpdateResult(
"",
string.Empty,
SubscriptionOutcomeType.NoUpdate);
}
else
{
_logger.LogInformation("Pull request '{url}' for subscription {subscriptionId} created", newPr.Url, update.SubscriptionId);
await _stateManager.UnsetUpdateReminderAsync(isCodeFlow: false);
return new SubscriptionUpdateResult(
$"Pull request created successfully: {GitRepoUrlUtils.TurnApiUrlToWebsite(newPr.Url)}",
"New pull request created",
SubscriptionOutcomeType.Updated);
}
}
Expand All @@ -108,7 +108,7 @@ private async Task<SubscriptionUpdateResult> UpdatePullRequestAsync(
|| update.CoherencyUpdates.Count == 0)))
{
_logger.LogInformation("No updates found for pull request {url}", pr.Url);
return new SubscriptionUpdateResult($"No new updates found for existing PR: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}", SubscriptionOutcomeType.NoUpdate);
return new SubscriptionUpdateResult("No new dependency updates", SubscriptionOutcomeType.NoUpdate);
}

pr.RequiredUpdates = MergeExistingWithIncomingUpdates(
Expand All @@ -123,7 +123,7 @@ private async Task<SubscriptionUpdateResult> UpdatePullRequestAsync(
if (pr.RequiredUpdates.Count < 1)
{
_logger.LogInformation("No new updates found for pull request {url}", pr.Url);
return new SubscriptionUpdateResult($"No new updates found for existing PR: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}", SubscriptionOutcomeType.NoUpdate);
return new SubscriptionUpdateResult("No new dependency updates", SubscriptionOutcomeType.NoUpdate);
}

await _subscriptionEventRecorder.RegisterSubscriptionUpdateAction(SubscriptionUpdateAction.ApplyingUpdates, update.SubscriptionId);
Expand Down Expand Up @@ -186,7 +186,7 @@ await _subscriptionEventRecorder.AddDependencyFlowEventsAsync(
_logger.LogInformation("Pull request '{prUrl}' updated", pr.Url);

return new SubscriptionUpdateResult(
$"Dependencies successfully updated in an existing PR: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}",
string.Empty,
SubscriptionOutcomeType.Updated);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,7 @@ public async Task<SubscriptionUpdateResult> ProcessPendingUpdatesAsync(Subscript
pr.NextBuildsToProcess);

return new SubscriptionUpdateResult(
$"Skipping codeflow update because an update with a newer build {pr.NextBuildsToProcess} has already been queued."
+ (pr != null ? $"PR url: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}" : ""),
$"Skipping codeflow update because an update with a newer build {pr.NextBuildsToProcess.Values.First().ToString() ?? "(N/A)"} has already been queued",
SubscriptionOutcomeType.NoUpdate);
Comment on lines 422 to 424
Comment on lines 422 to 424
}

Expand Down Expand Up @@ -443,7 +442,7 @@ public async Task<SubscriptionUpdateResult> ProcessPendingUpdatesAsync(Subscript
_logger.LogInformation("PR {url} for subscription {subscriptionId} cannot be updated at this time. Deferring update..", pr.Url, update.SubscriptionId);
await _stateManager.ScheduleUpdateForLater(pr, update, isCodeFlow);
return new SubscriptionUpdateResult(
$"The existing PR cannot be updated at this time due to pending checks, and will be retried periodically. Pr url: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}",
"The existing PR cannot be updated due to pending checks - retrying periodically until success",
SubscriptionOutcomeType.Rescheduled);
default:
throw new NotImplementedException($"Unknown PR status {status}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Maestro.Common;
using Maestro.Data;
using Maestro.Data.Models;
using Maestro.Services.Common.Cache;
using Maestro.WorkItems;
using Microsoft.Extensions.Logging;
using ProductConstructionService.DependencyFlow.Model;
using ProductConstructionService.DependencyFlow.WorkItems;
using ProductConstructionService.DependencyFlow.PullRequestUpdaters;
using Microsoft.EntityFrameworkCore;

namespace ProductConstructionService.DependencyFlow;

Expand All @@ -24,9 +28,11 @@ Task<bool> RunUpdateWithOutcomePersistenceAsync(

public class SubscriptionUpdateOutcomeRecorder(
BuildAssetRegistryContext context,
IRedisCacheFactory cacheFactory,
ILogger<SubscriptionUpdateOutcomeRecorder> logger) : ISubscriptionUpdateOutcomeRecorder
{
private readonly BuildAssetRegistryContext _context = context;
private readonly IRedisCacheFactory _cacheFactory = cacheFactory;
private readonly ILogger<SubscriptionUpdateOutcomeRecorder> _logger = logger;

public Task<bool> RunUpdateWithOutcomePersistenceAsync(
Expand Down Expand Up @@ -94,15 +100,46 @@ private async Task RecordSubscriptionUpdateAsync(
// Fall back to a generated id if there's no current Activity (e.g. tests).
var operationId = Activity.Current?.RootId ?? Guid.NewGuid().ToString("N");

var prUrl = await TryGetPullRequestUrlAsync(subscriptionId);

await _context.SubscriptionOutcomes.AddAsync(new SubscriptionOutcome
{
Message = message,
OperationId = operationId,
SubscriptionId = subscriptionId,
BuildId = buildId ?? -1,
Type = type,
Date = DateTime.UtcNow
Date = DateTime.UtcNow,
PrUrl = prUrl,
});
await _context.SaveChangesAsync();
}

private async Task<string?> TryGetPullRequestUrlAsync(Guid subscriptionId)
{
try
{
var subscription = await _context.Subscriptions.FirstAsync(s => s.Id == subscriptionId);

string prRedisKey = PullRequestUpdaterId.CreateUpdaterId(subscription).Id;
Comment thread
adamzip marked this conversation as resolved.
Outdated

var pullRequest = await _cacheFactory
.Create<InProgressPullRequest>(prRedisKey)
.TryGetStateAsync();

if (pullRequest == null)
{
return null;
}

return GitRepoUrlUtils.TurnApiUrlToWebsite(pullRequest.Url);
}
catch (Exception e)
{
_logger.LogWarning(e,
"Failed to retrieve the in-progress pull request for SubscriptionId: {SubscriptionId} while recording the subscription outcome.",
subscriptionId);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ where sub.Enabled
if (subscriptionToUpdate == null)
{
return new SubscriptionUpdateResult(
"No matching subscription and build found",
"Could not find the matching subscription or latest build to apply",
SubscriptionOutcomeType.Failure);
Comment thread
adamzip marked this conversation as resolved.
}

Expand Down Expand Up @@ -119,7 +119,7 @@ where sub.Enabled
if (subscriptionToUpdate == null)
{
return new SubscriptionUpdateResult(
"No matching subscription and build found",
"Could not find the matching subscription or latest build to apply",
SubscriptionOutcomeType.Failure);
Comment thread
adamzip marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ private async Task<SubscriptionUpdateResult> ProcessSubscriptionUpdateAsync(
var build = await _sqlClient.GetBuildAsync(workItem.BuildId)
?? throw new NonRetriableException($"Build with buildId {workItem.BuildId} not found in the DB.");

var subscription = await _sqlClient.GetSubscriptionAsync(workItem.SubscriptionId)
?? throw new NonRetriableException($"Subscription with subscriptionId {workItem.SubscriptionId} not found in the DB.");

Comment thread
adamzip marked this conversation as resolved.
var updater = _updaterFactory.CreatePullRequestUpdater(
PullRequestUpdaterId.Parse(
workItem.UpdaterId,
Expand Down
Loading