diff --git a/src/Maestro/Maestro.Data/Migrations/20260615142342_AddSubscriptionOutcomePrUrl.Designer.cs b/src/Maestro/Maestro.Data/Migrations/20260615142342_AddSubscriptionOutcomePrUrl.Designer.cs
new file mode 100644
index 0000000000..046310a213
--- /dev/null
+++ b/src/Maestro/Maestro.Data/Migrations/20260615142342_AddSubscriptionOutcomePrUrl.Designer.cs
@@ -0,0 +1,1106 @@
+//
+using System;
+using Maestro.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Maestro.Data.Migrations
+{
+ [DbContext(typeof(BuildAssetRegistryContext))]
+ [Migration("20260615142342_AddSubscriptionOutcomePrUrl")]
+ partial class AddSubscriptionOutcomePrUrl
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Maestro.Data.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("FullName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LastUpdated")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Maestro.Data.ApplicationUserPersonalAccessToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ApplicationUserId")
+ .HasColumnType("int");
+
+ b.Property("Created")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("Hash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationUserId", "Name")
+ .IsUnique()
+ .HasFilter("[Name] IS NOT NULL");
+
+ b.ToTable("AspNetUserPersonalAccessTokens");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Asset", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BuildId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasMaxLength(250)
+ .HasColumnType("nvarchar(250)");
+
+ b.Property("NonShipping")
+ .HasColumnType("bit");
+
+ b.Property("Version")
+ .HasMaxLength(75)
+ .HasColumnType("nvarchar(75)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BuildId");
+
+ b.HasIndex("Name", "Version");
+
+ b.ToTable("Assets");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.AssetFilter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Filter")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SubscriptionId");
+
+ b.ToTable("AssetFilters");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.AssetLocation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AssetId")
+ .HasColumnType("int");
+
+ b.Property("Location")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssetId");
+
+ b.ToTable("AssetLocations");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Build", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AzureDevOpsAccount")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AzureDevOpsBranch")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AzureDevOpsBuildDefinitionId")
+ .HasColumnType("int");
+
+ b.Property("AzureDevOpsBuildId")
+ .HasColumnType("int");
+
+ b.Property("AzureDevOpsBuildNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AzureDevOpsProject")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AzureDevOpsRepository")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Commit")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DateProduced")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("GitHubBranch")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("GitHubRepository")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Released")
+ .HasColumnType("bit");
+
+ b.Property("Stable")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.ToTable("Builds");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.BuildChannel", b =>
+ {
+ b.Property("BuildId")
+ .HasColumnType("int");
+
+ b.Property("ChannelId")
+ .HasColumnType("int");
+
+ b.Property("DateTimeAdded")
+ .HasColumnType("datetimeoffset");
+
+ b.HasKey("BuildId", "ChannelId");
+
+ b.HasIndex("ChannelId");
+
+ b.ToTable("BuildChannels");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.BuildDependency", b =>
+ {
+ b.Property("BuildId")
+ .HasColumnType("int");
+
+ b.Property("DependentBuildId")
+ .HasColumnType("int");
+
+ b.Property("IsProduct")
+ .HasColumnType("bit");
+
+ b.Property("TimeToInclusionInMinutes")
+ .HasColumnType("float");
+
+ b.HasKey("BuildId", "DependentBuildId");
+
+ b.HasIndex("DependentBuildId");
+
+ b.ToTable("BuildDependencies");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.BuildIncoherence", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BuildId")
+ .HasColumnType("int");
+
+ b.Property("Commit")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Repository")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Version")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BuildId");
+
+ b.ToTable("BuildIncoherencies");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Channel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Classification")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("NamespaceId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.HasIndex("NamespaceId");
+
+ b.ToTable("Channels");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.DefaultChannel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Branch")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("ChannelId")
+ .HasColumnType("int");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("NamespaceId")
+ .HasColumnType("int");
+
+ b.Property("Repository")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("varchar(300)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId");
+
+ b.HasIndex("NamespaceId");
+
+ b.HasIndex("Repository", "Branch", "ChannelId")
+ .IsUnique();
+
+ b.ToTable("DefaultChannels");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.DependencyFlowEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BuildId")
+ .HasColumnType("int");
+
+ b.Property("ChannelId")
+ .HasColumnType("int");
+
+ b.Property("Event")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FlowType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Reason")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceRepository")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("TargetRepository")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("Url")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BuildId");
+
+ b.ToTable("DependencyFlowEvents");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.GoalTime", b =>
+ {
+ b.Property("DefinitionId")
+ .HasColumnType("int");
+
+ b.Property("ChannelId")
+ .HasColumnType("int");
+
+ b.Property("Minutes")
+ .HasColumnType("int");
+
+ b.HasKey("DefinitionId", "ChannelId");
+
+ b.HasIndex("ChannelId");
+
+ b.ToTable("GoalTime");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Namespace", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Namespaces");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Repository", b =>
+ {
+ b.Property("RepositoryName")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("InstallationId")
+ .HasColumnType("bigint");
+
+ b.HasKey("RepositoryName");
+
+ b.ToTable("Repositories");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.RepositoryBranch", b =>
+ {
+ b.Property("RepositoryName")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("BranchName")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("NamespaceId")
+ .HasColumnType("int");
+
+ b.Property("PolicyString")
+ .HasColumnType("nvarchar(max)")
+ .HasColumnName("Policy");
+
+ b.HasKey("RepositoryName", "BranchName");
+
+ b.HasIndex("NamespaceId");
+
+ b.ToTable("RepositoryBranches");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Subscription", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ChannelId")
+ .HasColumnType("int");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("LastAppliedBuildId")
+ .HasColumnType("int");
+
+ b.Property("NamespaceId")
+ .HasColumnType("int");
+
+ b.Property("PolicyString")
+ .HasColumnType("nvarchar(max)")
+ .HasColumnName("Policy");
+
+ b.Property("PullRequestFailureNotificationTags")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceDirectory")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceEnabled")
+ .HasColumnType("bit");
+
+ b.Property("SourceRepository")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TargetBranch")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TargetDirectory")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TargetRepository")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId");
+
+ b.HasIndex("LastAppliedBuildId");
+
+ b.HasIndex("NamespaceId");
+
+ b.ToTable("Subscriptions");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.SubscriptionOutcome", b =>
+ {
+ b.Property("OperationId")
+ .HasMaxLength(32)
+ .HasColumnType("nchar(32)")
+ .IsFixedLength();
+
+ b.Property("BuildId")
+ .HasColumnType("int");
+
+ b.Property("Date")
+ .HasColumnType("datetime2");
+
+ b.Property("Message")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PrUrl")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("OperationId");
+
+ b.HasIndex("BuildId");
+
+ b.HasIndex("Date")
+ .IsDescending();
+
+ b.HasIndex("SubscriptionId", "Date")
+ .IsDescending(false, true);
+
+ b.ToTable("SubscriptionOutcomes");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.SubscriptionUpdate", b =>
+ {
+ b.Property("SubscriptionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Action")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Arguments")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Method")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Success")
+ .HasColumnType("bit");
+
+ b.Property("SysEndTime")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("SysEndTime");
+
+ b.Property("SysStartTime")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("SysStartTime");
+
+ b.HasKey("SubscriptionId");
+
+ b.ToTable("SubscriptionUpdates");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("SubscriptionUpdateHistory");
+ ttb
+ .HasPeriodStart("SysStartTime")
+ .HasColumnName("SysStartTime");
+ ttb
+ .HasPeriodEnd("SysEndTime")
+ .HasColumnName("SysEndTime");
+ }));
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.SubscriptionUpdateHistory", b =>
+ {
+ b.Property("Action")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Arguments")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Method")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Success")
+ .HasColumnType("bit");
+
+ b.Property("SysEndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("SysStartTime")
+ .HasColumnType("datetime2");
+
+ b.HasIndex("SysEndTime", "SysStartTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("SysEndTime", "SysStartTime"));
+
+ b.HasIndex("SubscriptionId", "SysEndTime", "SysStartTime");
+
+ b.ToTable("SubscriptionUpdateHistory", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Maestro.Data.ApplicationUserPersonalAccessToken", b =>
+ {
+ b.HasOne("Maestro.Data.ApplicationUser", "ApplicationUser")
+ .WithMany("PersonalAccessTokens")
+ .HasForeignKey("ApplicationUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ApplicationUser");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Asset", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Build", null)
+ .WithMany("Assets")
+ .HasForeignKey("BuildId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.AssetFilter", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Subscription", null)
+ .WithMany("ExcludedAssets")
+ .HasForeignKey("SubscriptionId");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.AssetLocation", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Asset", null)
+ .WithMany("Locations")
+ .HasForeignKey("AssetId");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.BuildChannel", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Build", "Build")
+ .WithMany("BuildChannels")
+ .HasForeignKey("BuildId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Maestro.Data.Models.Channel", "Channel")
+ .WithMany("BuildChannels")
+ .HasForeignKey("ChannelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Build");
+
+ b.Navigation("Channel");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.BuildDependency", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Build", "Build")
+ .WithMany()
+ .HasForeignKey("BuildId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Maestro.Data.Models.Build", "DependentBuild")
+ .WithMany()
+ .HasForeignKey("DependentBuildId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Build");
+
+ b.Navigation("DependentBuild");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.BuildIncoherence", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Build", null)
+ .WithMany("Incoherencies")
+ .HasForeignKey("BuildId");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Channel", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Namespace", "Namespace")
+ .WithMany("Channels")
+ .HasForeignKey("NamespaceId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Namespace");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.DefaultChannel", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Channel", "Channel")
+ .WithMany("DefaultChannels")
+ .HasForeignKey("ChannelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Maestro.Data.Models.Namespace", "Namespace")
+ .WithMany("DefaultChannels")
+ .HasForeignKey("NamespaceId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Channel");
+
+ b.Navigation("Namespace");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.DependencyFlowEvent", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Build", "Build")
+ .WithMany()
+ .HasForeignKey("BuildId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Build");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.GoalTime", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Channel", "Channel")
+ .WithMany()
+ .HasForeignKey("ChannelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Channel");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.RepositoryBranch", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Namespace", "Namespace")
+ .WithMany("RepositoryBranches")
+ .HasForeignKey("NamespaceId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("Maestro.Data.Models.Repository", "Repository")
+ .WithMany("Branches")
+ .HasForeignKey("RepositoryName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Namespace");
+
+ b.Navigation("Repository");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Subscription", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Channel", "Channel")
+ .WithMany()
+ .HasForeignKey("ChannelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Maestro.Data.Models.Build", "LastAppliedBuild")
+ .WithMany()
+ .HasForeignKey("LastAppliedBuildId");
+
+ b.HasOne("Maestro.Data.Models.Namespace", "Namespace")
+ .WithMany("Subscriptions")
+ .HasForeignKey("NamespaceId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Channel");
+
+ b.Navigation("LastAppliedBuild");
+
+ b.Navigation("Namespace");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.SubscriptionUpdate", b =>
+ {
+ b.HasOne("Maestro.Data.Models.Subscription", "Subscription")
+ .WithOne()
+ .HasForeignKey("Maestro.Data.Models.SubscriptionUpdate", "SubscriptionId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Subscription");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Maestro.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Maestro.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Maestro.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Maestro.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Maestro.Data.ApplicationUser", b =>
+ {
+ b.Navigation("PersonalAccessTokens");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Asset", b =>
+ {
+ b.Navigation("Locations");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Build", b =>
+ {
+ b.Navigation("Assets");
+
+ b.Navigation("BuildChannels");
+
+ b.Navigation("Incoherencies");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Channel", b =>
+ {
+ b.Navigation("BuildChannels");
+
+ b.Navigation("DefaultChannels");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Namespace", b =>
+ {
+ b.Navigation("Channels");
+
+ b.Navigation("DefaultChannels");
+
+ b.Navigation("RepositoryBranches");
+
+ b.Navigation("Subscriptions");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Repository", b =>
+ {
+ b.Navigation("Branches");
+ });
+
+ modelBuilder.Entity("Maestro.Data.Models.Subscription", b =>
+ {
+ b.Navigation("ExcludedAssets");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Maestro/Maestro.Data/Migrations/20260615142342_AddSubscriptionOutcomePrUrl.cs b/src/Maestro/Maestro.Data/Migrations/20260615142342_AddSubscriptionOutcomePrUrl.cs
new file mode 100644
index 0000000000..2c11299681
--- /dev/null
+++ b/src/Maestro/Maestro.Data/Migrations/20260615142342_AddSubscriptionOutcomePrUrl.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Maestro.Data.Migrations
+{
+ ///
+ public partial class AddSubscriptionOutcomePrUrl : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "PrUrl",
+ table: "SubscriptionOutcomes",
+ type: "nvarchar(max)",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "PrUrl",
+ table: "SubscriptionOutcomes");
+ }
+ }
+}
diff --git a/src/Maestro/Maestro.Data/Migrations/BuildAssetRegistryContextModelSnapshot.cs b/src/Maestro/Maestro.Data/Migrations/BuildAssetRegistryContextModelSnapshot.cs
index 2706912f9d..547d83a3bb 100644
--- a/src/Maestro/Maestro.Data/Migrations/BuildAssetRegistryContextModelSnapshot.cs
+++ b/src/Maestro/Maestro.Data/Migrations/BuildAssetRegistryContextModelSnapshot.cs
@@ -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);
@@ -581,6 +581,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Message")
.HasColumnType("nvarchar(max)");
+ b.Property("PrUrl")
+ .HasColumnType("nvarchar(max)");
+
b.Property("SubscriptionId")
.HasColumnType("uniqueidentifier");
diff --git a/src/Maestro/Maestro.Data/Models/SubscriptionOutcome.cs b/src/Maestro/Maestro.Data/Models/SubscriptionOutcome.cs
index 8914567765..420671fb15 100644
--- a/src/Maestro/Maestro.Data/Models/SubscriptionOutcome.cs
+++ b/src/Maestro/Maestro.Data/Models/SubscriptionOutcome.cs
@@ -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
diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/TriggerSubscriptionsOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/TriggerSubscriptionsOperation.cs
index 994614952c..258616cbe3 100644
--- a/src/Microsoft.DotNet.Darc/Darc/Operations/TriggerSubscriptionsOperation.cs
+++ b/src/Microsoft.DotNet.Darc/Darc/Operations/TriggerSubscriptionsOperation.cs
@@ -244,6 +244,10 @@ private static void PrintOutcome(Subscription subscription, SubscriptionTriggerO
{
Console.WriteLine($" {UxHelpers.GetSubscriptionDescription(subscription)}");
Console.WriteLine($" Outcome: {outcome.Type} (build {outcome.BuildId})");
+ if (!string.IsNullOrEmpty(outcome.PrUrl))
+ {
+ Console.WriteLine($" PR URL: {outcome.PrUrl}");
+ }
if (!string.IsNullOrWhiteSpace(outcome.Message))
{
Console.WriteLine($" {outcome.Message}");
diff --git a/src/ProductConstructionService/Microsoft.DotNet.ProductConstructionService.Client/Generated/Models/SubscriptionTriggerOutcome.cs b/src/ProductConstructionService/Microsoft.DotNet.ProductConstructionService.Client/Generated/Models/SubscriptionTriggerOutcome.cs
index f95acd3f1e..9017436b7f 100644
--- a/src/ProductConstructionService/Microsoft.DotNet.ProductConstructionService.Client/Generated/Models/SubscriptionTriggerOutcome.cs
+++ b/src/ProductConstructionService/Microsoft.DotNet.ProductConstructionService.Client/Generated/Models/SubscriptionTriggerOutcome.cs
@@ -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;
@@ -19,6 +19,7 @@ public SubscriptionTriggerOutcome(Guid subscriptionId, int buildId, DateTimeOffs
SourceRepository = sourceRepository;
TargetRepository = targetRepository;
TargetBranch = targetBranch;
+ PrUrl = prUrl;
}
[JsonProperty("operationId")]
@@ -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
{
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Models/SubscriptionTriggerOutcome.cs b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Models/SubscriptionTriggerOutcome.cs
index 3178edfb85..9daa8ab2fb 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Models/SubscriptionTriggerOutcome.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Models/SubscriptionTriggerOutcome.cs
@@ -25,6 +25,7 @@ public SubscriptionTriggerOutcome(Maestro.Data.Models.SubscriptionOutcome other,
SourceRepository = sourceRepository;
TargetRepository = targetRepository;
TargetBranch = targetBranch;
+ PrUrl = other.PrUrl;
}
public string OperationId { get; }
@@ -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
diff --git a/src/ProductConstructionService/ProductConstructionService.BarViz/Code/Helpers/RepoUrlConverter.cs b/src/ProductConstructionService/ProductConstructionService.BarViz/Code/Helpers/RepoUrlConverter.cs
index df34fe0028..502fbaa590 100644
--- a/src/ProductConstructionService/ProductConstructionService.BarViz/Code/Helpers/RepoUrlConverter.cs
+++ b/src/ProductConstructionService/ProductConstructionService.BarViz/Code/Helpers/RepoUrlConverter.cs
@@ -60,4 +60,18 @@ public static class RepoUrlConverter
return null;
}
+
+ public static string GetPrNumber(string prUrl)
+ {
+ if (prUrl != null && Uri.TryCreate(prUrl, UriKind.Absolute, out var uri))
+ {
+ var lastSegment = uri.Segments[^1].TrimEnd('/');
+ if (int.TryParse(lastSegment, out _))
+ {
+ return lastSegment;
+ }
+ }
+
+ return "?";
+ }
}
diff --git a/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/Codeflows.razor b/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/Codeflows.razor
index b2aee405fb..91eb3b7415 100644
--- a/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/Codeflows.razor
+++ b/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/Codeflows.razor
@@ -137,7 +137,7 @@
{
- #@GetPrNumber(context.ForwardFlow.ActivePullRequest) (@GetPrAge(context.ForwardFlow.ActivePullRequest))
+ #@RepoUrlConverter.GetPrNumber(context.ForwardFlow.ActivePullRequest.Url) (@GetPrAge(context.ForwardFlow.ActivePullRequest))
}
else
@@ -178,7 +178,7 @@
{
- #@GetPrNumber(context.Backflow.ActivePullRequest) (@GetPrAge(context.Backflow.ActivePullRequest))
+ #@RepoUrlConverter.GetPrNumber(context.Backflow.ActivePullRequest.Url) (@GetPrAge(context.Backflow.ActivePullRequest))
}
else
@@ -408,20 +408,6 @@
ForwardflowPr: forwardPr);
}
- static string GetPrNumber(TrackedPullRequest pr)
- {
- if (pr?.Url != null && Uri.TryCreate(pr.Url, UriKind.Absolute, out var uri))
- {
- var lastSegment = uri.Segments[^1].TrimEnd('/');
- if (int.TryParse(lastSegment, out _))
- {
- return lastSegment;
- }
- }
-
- return "?";
- }
-
static string GetPrAge(TrackedPullRequest pr)
{
if (pr == null || pr.CreationDate <= DateTimeOffset.UnixEpoch)
diff --git a/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/SubscriptionTriggerHistory.razor b/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/SubscriptionTriggerHistory.razor
index e0706a4038..e8f07fc0f9 100644
--- a/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/SubscriptionTriggerHistory.razor
+++ b/src/ProductConstructionService/ProductConstructionService.BarViz/Pages/SubscriptionTriggerHistory.razor
@@ -171,6 +171,14 @@
@GetStatusText(context.Type)
+
+
+ @if (!string.IsNullOrEmpty(context.PrUrl))
+ {
+ #@RepoUrlConverter.GetPrNumber(context.PrUrl)
+ }
+
+
@foreach (var segment in StringExtensions.SplitIntoTextAndUrls(context.Message))
{
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/DependencyFlowConfiguration.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/DependencyFlowConfiguration.cs
index a63486090e..ea8895063b 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/DependencyFlowConfiguration.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/DependencyFlowConfiguration.cs
@@ -30,7 +30,7 @@ public static void AddDependencyFlowProcessors(this IServiceCollection services)
services.TryAddScoped();
services.TryAddTransient();
services.TryAddTransient();
- services.TryAddTransient();
+ services.TryAddScoped();
services.AddWorkItemProcessor();
services.AddWorkItemProcessor();
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/CodeFlowPullRequestUpdater.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/CodeFlowPullRequestUpdater.cs
index 811d5851e6..70212bb90c 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/CodeFlowPullRequestUpdater.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/CodeFlowPullRequestUpdater.cs
@@ -34,6 +34,7 @@ internal class CodeFlowPullRequestUpdater : PullRequestUpdater
private readonly ICommentCollector _commentCollector;
private readonly IPullRequestStateManager _stateManager;
private readonly ISubscriptionEventRecorder _subscriptionEventRecorder;
+ private readonly ISubscriptionUpdateOutcomeRecorder _outcomeRecorder;
private readonly IPullRequestTarget _target;
private readonly ILogger _logger;
@@ -52,8 +53,9 @@ public CodeFlowPullRequestUpdater(
IPullRequestCommenter pullRequestCommenter,
IPullRequestStateManager stateManager,
ISubscriptionEventRecorder subscriptionEventRecorder,
+ ISubscriptionUpdateOutcomeRecorder outcomeRecorder,
ILogger logger)
- : base(target, mergePolicyEvaluator, remoteFactory, sqlClient, pullRequestCommenter, stateManager, subscriptionEventRecorder, logger)
+ : base(target, mergePolicyEvaluator, remoteFactory, sqlClient, pullRequestCommenter, stateManager, subscriptionEventRecorder, outcomeRecorder, logger)
{
_vmrInfo = vmrInfo;
_vmrForwardFlower = vmrForwardFlower;
@@ -67,6 +69,7 @@ public CodeFlowPullRequestUpdater(
_logger = logger;
_stateManager = stateManager;
_subscriptionEventRecorder = subscriptionEventRecorder;
+ _outcomeRecorder = outcomeRecorder;
_target = target;
}
@@ -87,7 +90,7 @@ protected override async Task 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);
}
@@ -100,7 +103,7 @@ protected override async Task 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);
}
@@ -142,8 +145,8 @@ protected override async Task 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);
}
@@ -181,7 +184,7 @@ protected override async Task 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);
}
@@ -190,6 +193,7 @@ protected override async Task ProcessSubscriptionUpdat
{
oldPrUrl = pr.Url;
await _stateManager.ClearAllStateAsync(isCodeFlow: true, clearPendingUpdates: true);
+ _outcomeRecorder.SetPullRequestUrl(null);
pr = null;
prInfo = null;
}
@@ -211,7 +215,7 @@ protected override async Task ProcessSubscriptionUpdat
}
return new SubscriptionUpdateResult(
- $"New codeflow PR created: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}.",
+ "New codeflow PR created",
Maestro.Data.Models.SubscriptionOutcomeType.Updated);
}
else
@@ -231,7 +235,7 @@ await UpdateCodeFlowPullRequestAsync(
upstreamRepoDiffs);
return new SubscriptionUpdateResult(
- $"Existing codeflow PR has been updated: {GitRepoUrlUtils.TurnApiUrlToWebsite(pr.Url)}.",
+ string.Empty,
Maestro.Data.Models.SubscriptionOutcomeType.Updated);
}
}
@@ -471,6 +475,7 @@ private async Task HandleConflictsAsync(
{
oldPrUrl = pr.Url;
await _stateManager.ClearAllStateAsync(isCodeFlow: true, clearPendingUpdates: true);
+ _outcomeRecorder.SetPullRequestUrl(null);
pr = null;
}
}
@@ -687,6 +692,7 @@ await _subscriptionEventRecorder.AddDependencyFlowEventsAsync(
inProgressPr.LastUpdate = DateTime.UtcNow;
await _stateManager.SetCheckReminderAsync(inProgressPr, pr, isCodeFlow: true);
await _stateManager.UnsetUpdateReminderAsync(isCodeFlow: true);
+ _outcomeRecorder.SetPullRequestUrl(inProgressPr.Url);
_logger.LogInformation("Code flow pull request created: {prUrl}", pr.Url);
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/DependencyPullRequestUpdater.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/DependencyPullRequestUpdater.cs
index 59e7c71b41..60c7b557b7 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/DependencyPullRequestUpdater.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/DependencyPullRequestUpdater.cs
@@ -26,6 +26,7 @@ internal class DependencyPullRequestUpdater : PullRequestUpdater
private readonly ISqlBarClient _sqlClient;
private readonly IPullRequestStateManager _stateManager;
private readonly ISubscriptionEventRecorder _subscriptionEventRecorder;
+ private readonly ISubscriptionUpdateOutcomeRecorder _outcomeRecorder;
private readonly ILogger _logger;
public DependencyPullRequestUpdater(
@@ -38,8 +39,9 @@ public DependencyPullRequestUpdater(
ISqlBarClient sqlClient,
ILogger logger,
IPullRequestCommenter pullRequestCommenter,
- ISubscriptionEventRecorder subscriptionEventRecorder)
- : base(target, mergePolicyEvaluator, remoteFactory, sqlClient, pullRequestCommenter, stateManager, subscriptionEventRecorder, logger)
+ ISubscriptionEventRecorder subscriptionEventRecorder,
+ ISubscriptionUpdateOutcomeRecorder outcomeRecorder)
+ : base(target, mergePolicyEvaluator, remoteFactory, sqlClient, pullRequestCommenter, stateManager, subscriptionEventRecorder, outcomeRecorder, logger)
{
_target = target;
_coherencyUpdateResolver = coherencyUpdateResolver;
@@ -49,6 +51,7 @@ public DependencyPullRequestUpdater(
_stateManager = stateManager;
_logger = logger;
_subscriptionEventRecorder = subscriptionEventRecorder;
+ _outcomeRecorder = outcomeRecorder;
}
protected override async Task ProcessSubscriptionUpdateAsync(
@@ -73,7 +76,7 @@ protected override async Task ProcessSubscriptionUpdat
await _stateManager.UnsetUpdateReminderAsync(isCodeFlow: false);
return new SubscriptionUpdateResult(
- "",
+ string.Empty,
SubscriptionOutcomeType.NoUpdate);
}
else
@@ -81,7 +84,7 @@ protected override async Task ProcessSubscriptionUpdat
_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);
}
}
@@ -108,7 +111,7 @@ private async Task 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(
@@ -123,7 +126,7 @@ private async Task 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);
@@ -186,7 +189,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);
}
@@ -265,6 +268,7 @@ await _subscriptionEventRecorder.AddDependencyFlowEventsAsync(
};
await _stateManager.SetCheckReminderAsync(inProgressPr, pr, isCodeFlow);
+ _outcomeRecorder.SetPullRequestUrl(inProgressPr.Url);
return pr;
}
@@ -329,6 +333,7 @@ await _subscriptionEventRecorder.AddDependencyFlowEventsAsync(
pr.Url);
await _stateManager.SetCheckReminderAsync(inProgressPr, pr, isCodeFlow);
+ _outcomeRecorder.SetPullRequestUrl(inProgressPr.Url);
return pr;
}
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/PullRequestUpdater.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/PullRequestUpdater.cs
index 23ba1989b7..5fa98e3886 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/PullRequestUpdater.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdaters/PullRequestUpdater.cs
@@ -2,12 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
-using Maestro.Common;
using Maestro.Data.Models;
using Maestro.DataProviders;
using Maestro.MergePolicies;
using Maestro.MergePolicyEvaluation;
-using Maestro.WorkItems;
using Microsoft.DotNet.DarcLib;
using Microsoft.DotNet.DarcLib.Models;
using Microsoft.Extensions.Logging;
@@ -35,6 +33,7 @@ internal abstract class PullRequestUpdater : IPullRequestUpdater
private readonly IMergePolicyEvaluator _mergePolicyEvaluator;
private readonly IRemoteFactory _remoteFactory;
private readonly ISubscriptionEventRecorder _subscriptionEventRecorder;
+ private readonly ISubscriptionUpdateOutcomeRecorder _outcomeRecorder;
private readonly ILogger _logger;
public PullRequestUpdater(
@@ -45,6 +44,7 @@ public PullRequestUpdater(
IPullRequestCommenter pullRequestCommenter,
IPullRequestStateManager stateManager,
ISubscriptionEventRecorder subscriptionEventRecorder,
+ ISubscriptionUpdateOutcomeRecorder outcomeRecorder,
ILogger logger)
{
_target = target;
@@ -54,6 +54,7 @@ public PullRequestUpdater(
_pullRequestCommenter = pullRequestCommenter;
_stateManager = stateManager;
_subscriptionEventRecorder = subscriptionEventRecorder;
+ _outcomeRecorder = outcomeRecorder;
_logger = logger;
}
@@ -69,6 +70,7 @@ protected abstract Task ProcessSubscriptionUpdateAsync
public async Task CheckPullRequestAsync(PullRequestCheck pullRequestCheck)
{
var inProgressPr = await _stateManager.GetInProgressPullRequestAsync();
+ _outcomeRecorder.SetPullRequestUrl(inProgressPr?.Url);
if (inProgressPr == null)
{
@@ -82,6 +84,7 @@ public async Task CheckPullRequestAsync(PullRequestCheck pullRequestCheck)
{
await _stateManager.ClearAllStateAsync(isCodeFlow: true, clearPendingUpdates: true);
await _stateManager.ClearAllStateAsync(isCodeFlow: false, clearPendingUpdates: true);
+ _outcomeRecorder.SetPullRequestUrl(null);
// Return true for test PRs to avoid reporting failure for deleted subscriptions during E2E tests
return inProgressPr.Url?.Contains("maestro-auth-test") ?? false;
}
@@ -146,6 +149,7 @@ await _subscriptionEventRecorder.AddDependencyFlowEventsAsync(
// If the PR we just merged was in conflict with an update we previously tried to apply, we shouldn't delete the reminder for the update
await _stateManager.ClearAllStateAsync(isCodeFlow, false);
+ _outcomeRecorder.SetPullRequestUrl(null);
return (PullRequestStatus.Completed, prInfo);
case MergePolicyCheckResult.FailedPolicies:
@@ -192,6 +196,7 @@ await _subscriptionEventRecorder.AddDependencyFlowEventsAsync(
_logger.LogInformation("PR {url} has been manually {action}. Stopping tracking it", pr.Url, prInfo.Status.ToString().ToLowerInvariant());
await _stateManager.ClearAllStateAsync(isCodeFlow, clearPendingUpdates: false);
+ _outcomeRecorder.SetPullRequestUrl(null);
// Also try to clean up the PR branch.
try
@@ -394,6 +399,7 @@ public async Task ProcessPendingUpdatesAsync(Subscript
_logger.LogInformation("Processing pending updates for subscription {subscriptionId} with build {buildId}", update.SubscriptionId, build.Id);
bool isCodeFlow = update.SubscriptionType == SubscriptionType.DependenciesAndSources;
InProgressPullRequest? pr = await _stateManager.GetInProgressPullRequestAsync();
+ _outcomeRecorder.SetPullRequestUrl(pr?.Url);
PullRequest? prInfo;
if (pr == null)
@@ -414,8 +420,7 @@ public async Task 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);
}
@@ -443,7 +448,7 @@ public async Task 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}");
@@ -453,6 +458,7 @@ public async Task ProcessPendingUpdatesAsync(Subscript
var result = await ProcessSubscriptionUpdateAsync(update, pr, prInfo, build, forceUpdate);
pr = await _stateManager.GetInProgressPullRequestAsync();
+ _outcomeRecorder.SetPullRequestUrl(pr?.Url);
if (pr != null)
{
await _pullRequestCommenter.PostCollectedCommentsAsync(
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/SubscriptionUpdateOutcomeRecorder.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/SubscriptionUpdateOutcomeRecorder.cs
index 9c117a40de..d79156a27b 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/SubscriptionUpdateOutcomeRecorder.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/SubscriptionUpdateOutcomeRecorder.cs
@@ -2,6 +2,7 @@
// 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.WorkItems;
@@ -13,6 +14,11 @@ namespace ProductConstructionService.DependencyFlow;
public interface ISubscriptionUpdateOutcomeRecorder
{
+ ///
+ /// Register the relevant PR URL of the subscription update, to be used when recording the outcome
+ ///
+ void SetPullRequestUrl(string? url);
+
Task RunUpdateWithOutcomePersistenceAsync(
SubscriptionUpdateWorkItem workItem,
Func> processAsync);
@@ -29,6 +35,20 @@ public class SubscriptionUpdateOutcomeRecorder(
private readonly BuildAssetRegistryContext _context = context;
private readonly ILogger _logger = logger;
+ private string? _pullRequestUrl;
+
+ public void SetPullRequestUrl(string? url)
+ {
+ if (url != null)
+ {
+ _pullRequestUrl = GitRepoUrlUtils.TurnApiUrlToWebsite(url);
+ }
+ else
+ {
+ _pullRequestUrl = null;
+ }
+ }
+
public Task RunUpdateWithOutcomePersistenceAsync(
SubscriptionUpdateWorkItem workItem,
Func> processAsync) =>
@@ -91,7 +111,7 @@ private async Task RecordSubscriptionUpdateAsync(
Guid subscriptionId,
int? buildId)
{
- // Fall back to a generated id if there's no current Activity (e.g. tests).
+ // Fall back to a generated id if there's no current Activity
var operationId = Activity.Current?.RootId ?? Guid.NewGuid().ToString("N");
await _context.SubscriptionOutcomes.AddAsync(new SubscriptionOutcome
@@ -101,8 +121,10 @@ await _context.SubscriptionOutcomes.AddAsync(new SubscriptionOutcome
SubscriptionId = subscriptionId,
BuildId = buildId ?? -1,
Type = type,
- Date = DateTime.UtcNow
+ Date = DateTime.UtcNow,
+ PrUrl = _pullRequestUrl,
});
+
await _context.SaveChangesAsync();
}
}
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionTriggerProcessor.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionTriggerProcessor.cs
index bead3be7c8..140c5f22d9 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionTriggerProcessor.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionTriggerProcessor.cs
@@ -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);
}
@@ -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);
}
diff --git a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionUpdateProcessor.cs b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionUpdateProcessor.cs
index c39f144367..b29cef8831 100644
--- a/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionUpdateProcessor.cs
+++ b/src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionUpdateProcessor.cs
@@ -37,9 +37,6 @@ private async Task 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.");
-
var updater = _updaterFactory.CreatePullRequestUpdater(
PullRequestUpdaterId.Parse(
workItem.UpdaterId,