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,