diff --git a/eng/pipelines/report-unreleased-sdks.yml b/eng/pipelines/report-unreleased-sdks.yml new file mode 100644 index 00000000000..df1e31529a5 --- /dev/null +++ b/eng/pipelines/report-unreleased-sdks.yml @@ -0,0 +1,31 @@ +trigger: none +pr: none + +variables: + - template: /eng/pipelines/templates/variables/image.yml + +pool: + name: $(LINUXPOOL) + demands: ImageOverride -equals $(LINUXVMIMAGE) + +jobs: + - job: ReportUnreleasedSdks + steps: + - checkout: self + + - task: PowerShell@2 + displayName: 'Install Azure SDK MCP' + inputs: + targetType: 'inline' + script: './eng/common/mcp/azure-sdk-mcp.ps1 -InstallDirectory $(System.DefaultWorkingDirectory)' + pwsh: true + workingDirectory: '$(System.DefaultWorkingDirectory)' + + - task: AzureCLI@2 + displayName: Email product owners about overdue SDK release plans + inputs: + azureSubscription: opensource-api-connection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + & "$(System.DefaultWorkingDirectory)/azsdk" release-plan list-overdue --notify-owners true --emailer-uri "$(AzureSDKEmailerSasURL)" \ No newline at end of file diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanManualTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanManualTests.cs index 1c4cca92e45..a81d0af630b 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanManualTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanManualTests.cs @@ -26,6 +26,7 @@ internal class ReleasePlanManualTests private IEnvironmentHelper environmentHelper; private readonly IGitHelper gitHelper; private IInputSanitizer inputSanitizer; + private HttpClient httpClient; public ReleasePlanManualTests() { @@ -37,6 +38,7 @@ public ReleasePlanManualTests() logger = new TestLogger(); gitHubService = new Mock().Object; inputSanitizer = new InputSanitizer(); + httpClient = new Mock().Object; var typeSpecHelperMock = new Mock(); typeSpecHelperMock.Setup(x => x.IsRepoPathForPublicSpecRepo(It.IsAny())).Returns(true); @@ -54,7 +56,7 @@ public ReleasePlanManualTests() gitHelperMock.Setup(x => x.GetBranchName(It.IsAny())).Returns("testBranch"); gitHelper = gitHelperMock.Object; - releasePlan = new ReleasePlanTool(devOpsService, gitHelper, typeSpecHelper, logger, userHelper, gitHubService, environmentHelper, inputSanitizer); + releasePlan = new ReleasePlanTool(devOpsService, gitHelper, typeSpecHelper, logger, userHelper, gitHubService, environmentHelper, inputSanitizer, httpClient); } [Test] // disabled by default because it makes real API calls diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanToolTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanToolTests.cs index f577099f6ff..38cab7ee2db 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanToolTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/ReleasePlan/ReleasePlanToolTests.cs @@ -1,4 +1,5 @@ using Moq; +using Moq.Protected; using Azure.Sdk.Tools.Cli.Helpers; using Azure.Sdk.Tools.Cli.Services; using Azure.Sdk.Tools.Cli.Tests.Mocks.Services; @@ -21,6 +22,7 @@ internal class ReleasePlanToolTests private IEnvironmentHelper environmentHelper; private ReleasePlanTool releasePlanTool; private IInputSanitizer inputSanitizer; + private HttpClient httpClient; [SetUp] public void Setup() @@ -30,6 +32,7 @@ public void Setup() devOpsService = new MockDevOpsService(); gitHubService = new MockGitHubService(); inputSanitizer = new InputSanitizer(); + httpClient = new Mock().Object; var userHelperMock = new Mock(); userHelperMock.Setup(x => x.GetUserEmail()).ReturnsAsync("test@example.com"); @@ -57,7 +60,8 @@ public void Setup() userHelper, gitHubService, environmentHelper, - inputSanitizer); + inputSanitizer, + httpClient); } [Test] @@ -150,7 +154,8 @@ public async Task Test_Create_releasePlan_with_AZSDKTOOLS_AGENT_TESTING_true_cre userHelper, gitHubService, environmentHelperMock.Object, - inputSanitizer); + inputSanitizer, + httpClient); var testCodeFilePath = "TypeSpecTestData/specification/testcontoso/Contoso.Management"; @@ -191,7 +196,8 @@ public async Task Test_Create_releasePlan_with_AZSDKTOOLS_AGENT_TESTING_false_re userHelper, gitHubService, environmentHelperMock.Object, - inputSanitizer); + inputSanitizer, + httpClient); var testCodeFilePath = "TypeSpecTestData/specification/testcontoso/Contoso.Management"; @@ -419,5 +425,200 @@ public async Task Test_update_spec_pull_request_with_non_specs_repo() Assert.That(response.ResponseError, Does.Contain("Invalid spec pull request URL")); Assert.That(response.ResponseError, Does.Contain("azure-rest-api-specs")); } + + [Test] + public async Task Test_list_overdue_release_plans_notify_without_emailer_uri() + { + var response = await releasePlanTool.ListOverdueReleasePlans(notifyOwners: true, emailerUri: ""); + Assert.IsNotNull(response); + Assert.IsNotNull(response.ResponseError); + Assert.That(response.ResponseError, Does.Contain("Emailer URI is required")); + } + + [Test] + public async Task Test_notification_includes_correct_missing_sdks() + { + var mockDevOps = new Mock(); + var plan = new ReleasePlanWorkItem + { + WorkItemId = 200, + Owner = "Test Owner", + ReleasePlanSubmittedByEmail = "valid@example.com", + IsManagementPlane = true, + IsDataPlane = false, + SDKReleaseMonth = "January 2026", + ReleasePlanLink = "https://example.com/releaseplan/200", + SDKInfo = + [ + new SDKInfo { Language = "Java", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" }, + new SDKInfo { Language = "Python", ReleaseStatus = "Released", ReleaseExclusionStatus = "Not applicable" }, + new SDKInfo { Language = ".NET", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" } + ] + }; + mockDevOps.Setup(x => x.ListOverdueReleasePlansAsync()).ReturnsAsync([plan]); + + var mockHttpMessageHandler = new Mock(); + var capturedBody = ""; + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + var content = request.Content?.ReadAsStringAsync().Result ?? ""; + var payload = JsonSerializer.Deserialize(content); + capturedBody = payload.GetProperty("Body").GetString() ?? ""; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + + var testHttpClient = new HttpClient(mockHttpMessageHandler.Object); + var tool = new ReleasePlanTool(mockDevOps.Object, gitHelper, typeSpecHelper, logger, userHelper, gitHubService, environmentHelper, inputSanitizer, testHttpClient); + + await tool.ListOverdueReleasePlans(notifyOwners: true, emailerUri: "https://test.com/email"); + + Assert.That(capturedBody, Does.Contain("Java")); + Assert.That(capturedBody, Does.Contain(".NET")); + Assert.That(capturedBody, Does.Not.Contain("Python")); // Released, should not be in missing list + } + + [Test] + public async Task Test_notification_excludes_approved_and_requested_languages() + { + var mockDevOps = new Mock(); + var plan = new ReleasePlanWorkItem + { + WorkItemId = 201, + Owner = "Test Owner", + ReleasePlanSubmittedByEmail = "valid@example.com", + IsManagementPlane = true, + IsDataPlane = false, + SDKReleaseMonth = "January 2026", + ReleasePlanLink = "https://example.com/releaseplan/201", + SDKInfo = + [ + new SDKInfo { Language = "Java", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" }, + new SDKInfo { Language = "Python", ReleaseStatus = "", ReleaseExclusionStatus = "Approved" }, + new SDKInfo { Language = ".NET", ReleaseStatus = "", ReleaseExclusionStatus = "Requested" } + ] + }; + mockDevOps.Setup(x => x.ListOverdueReleasePlansAsync()).ReturnsAsync([plan]); + + var mockHttpMessageHandler = new Mock(); + var capturedBody = ""; + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + var content = request.Content?.ReadAsStringAsync().Result ?? ""; + var payload = JsonSerializer.Deserialize(content); + capturedBody = payload.GetProperty("Body").GetString() ?? ""; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + + var testHttpClient = new HttpClient(mockHttpMessageHandler.Object); + var tool = new ReleasePlanTool(mockDevOps.Object, gitHelper, typeSpecHelper, logger, userHelper, gitHubService, environmentHelper, inputSanitizer, testHttpClient); + + await tool.ListOverdueReleasePlans(notifyOwners: true, emailerUri: "https://test.com/email"); + + Assert.That(capturedBody, Does.Contain("Java")); + Assert.That(capturedBody, Does.Not.Contain("Python")); // Approved exclusion + Assert.That(capturedBody, Does.Not.Contain(".NET")); // Requested exclusion + } + + [Test] + public async Task Test_notification_excludes_go_for_dataplane() + { + var mockDevOps = new Mock(); + var plan = new ReleasePlanWorkItem + { + WorkItemId = 202, + Owner = "Test Owner", + ReleasePlanSubmittedByEmail = "valid@example.com", + IsDataPlane = true, + IsManagementPlane = false, + SDKReleaseMonth = "January 2026", + ReleasePlanLink = "https://example.com/releaseplan/202", + SDKInfo = + [ + new SDKInfo { Language = "Java", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" }, + new SDKInfo { Language = "Go", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" } + ] + }; + mockDevOps.Setup(x => x.ListOverdueReleasePlansAsync()).ReturnsAsync([plan]); + + var mockHttpMessageHandler = new Mock(); + var capturedBody = ""; + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + var content = request.Content?.ReadAsStringAsync().Result ?? ""; + var payload = JsonSerializer.Deserialize(content); + capturedBody = payload.GetProperty("Body").GetString() ?? ""; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + + var testHttpClient = new HttpClient(mockHttpMessageHandler.Object); + var tool = new ReleasePlanTool(mockDevOps.Object, gitHelper, typeSpecHelper, logger, userHelper, gitHubService, environmentHelper, inputSanitizer, testHttpClient); + + await tool.ListOverdueReleasePlans(notifyOwners: true, emailerUri: "https://test.com/email"); + + Assert.That(capturedBody, Does.Contain("Java")); + Assert.That(capturedBody, Does.Not.Contain("Go")); // Filtered for Data Plane + Assert.That(capturedBody, Does.Contain("Data Plane")); + } + + [Test] + public async Task Test_notification_includes_go_for_management_plane() + { + var mockDevOps = new Mock(); + var plan = new ReleasePlanWorkItem + { + WorkItemId = 203, + Owner = "Test Owner", + ReleasePlanSubmittedByEmail = "valid@example.com", + IsManagementPlane = true, + IsDataPlane = false, + SDKReleaseMonth = "January 2026", + ReleasePlanLink = "https://example.com/releaseplan/203", + SDKInfo = + [ + new SDKInfo { Language = "Java", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" }, + new SDKInfo { Language = "Go", ReleaseStatus = "", ReleaseExclusionStatus = "Not applicable" } + ] + }; + mockDevOps.Setup(x => x.ListOverdueReleasePlansAsync()).ReturnsAsync([plan]); + + var mockHttpMessageHandler = new Mock(); + var capturedBody = ""; + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + var content = request.Content?.ReadAsStringAsync().Result ?? ""; + var payload = JsonSerializer.Deserialize(content); + capturedBody = payload.GetProperty("Body").GetString() ?? ""; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + + var testHttpClient = new HttpClient(mockHttpMessageHandler.Object); + var tool = new ReleasePlanTool(mockDevOps.Object, gitHelper, typeSpecHelper, logger, userHelper, gitHubService, environmentHelper, inputSanitizer, testHttpClient); + + await tool.ListOverdueReleasePlans(notifyOwners: true, emailerUri: "https://test.com/email"); + + Assert.That(capturedBody, Does.Contain("Java")); + Assert.That(capturedBody, Does.Contain("Go")); // Included for Management Plane + Assert.That(capturedBody, Does.Contain("Management Plane")); + } } } diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/CHANGELOG.md b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/CHANGELOG.md index 885b4363192..4b7b78cd54c 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/CHANGELOG.md +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/CHANGELOG.md @@ -6,6 +6,7 @@ - Improved error message when GitHub authentication fails to include GitHub CLI installation and authentication instructions - Added TypeSpecProject to the telemetry data for the `azsdk_package_generate_code` tool +- Added email notification support for overdue release plan owners. - Added support for GitHub URLs in TypeSpecHelper methods to accept URLs like `https://github.com/Azure/azure-rest-api-specs/blob/main/specification/...` in addition to local paths - MCP server now forwards log and subprocess output to MCP logging notifications instead of stdout - Added `APISpecProjectPath` property to Release Plan Work Item to track the TypeSpec project path in release plans diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs index a45b0f5a480..0583c7b05b9 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs @@ -254,22 +254,19 @@ private async Task MapWorkItemToReleasePlanAsync(WorkItem w var pullRequestStatus = workItem.Fields.TryGetValue($"Custom.SDKPullRequestStatusFor{lang}", out value) ? value?.ToString() ?? string.Empty : string.Empty; var exclusionStatus = workItem.Fields.TryGetValue($"Custom.ReleaseExclusionStatusFor{lang}", out value) ? value?.ToString() ?? string.Empty : string.Empty; - if (!string.IsNullOrEmpty(sdkGenPipelineUrl) || !string.IsNullOrEmpty(sdkPullRequestUrl) || !string.IsNullOrEmpty(packageName)) - { - releasePlan.SDKInfo.Add( - new SDKInfo() - { - Language = MapLanguageIdToName(lang), - GenerationPipelineUrl = sdkGenPipelineUrl, - SdkPullRequestUrl = sdkPullRequestUrl, - GenerationStatus = generationStatus, - ReleaseStatus = releaseStatus, - PullRequestStatus = pullRequestStatus, - PackageName = packageName, - ReleaseExclusionStatus = exclusionStatus - } - ); - } + releasePlan.SDKInfo.Add( + new SDKInfo() + { + Language = MapLanguageIdToName(lang), + GenerationPipelineUrl = sdkGenPipelineUrl, + SdkPullRequestUrl = sdkPullRequestUrl, + GenerationStatus = generationStatus, + ReleaseStatus = releaseStatus, + PullRequestStatus = pullRequestStatus, + PackageName = packageName, + ReleaseExclusionStatus = exclusionStatus + } + ); } // Get details from API spec work item diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/ReleasePlan/ReleasePlanTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/ReleasePlan/ReleasePlanTool.cs index 7e3034c02df..9983be9c6ac 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/ReleasePlan/ReleasePlanTool.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/ReleasePlan/ReleasePlanTool.cs @@ -28,7 +28,8 @@ public partial class ReleasePlanTool( // partial class required due to source g IUserHelper userHelper, IGitHubService githubService, IEnvironmentHelper environmentHelper, - IInputSanitizer inputSanitizer + IInputSanitizer inputSanitizer, + HttpClient httpClient ) : MCPMultiCommandTool { public override CommandGroup[] CommandHierarchy { get; set; } = [new("release-plan", "Manage release plans in AzureDevops")]; @@ -145,7 +146,20 @@ IInputSanitizer inputSanitizer Required = false, }; + private readonly Option notifyOwnersOpt = new("--notify-owners") + { + Description = "Send email notification to owners of overdue release plans", + Required = false, + }; + + private readonly Option azureSDKEmailerUriOpt = new("--emailer-uri") + { + Description = "The Uri of the app used to send email notifications", + Required = false, + }; + private const string sdkBotEmail = "azuresdk@microsoft.com"; + private const string sdkApexEmail = "azsdkapex@microsoft.com"; private static readonly string DEFAULT_BRANCH = "main"; private static readonly string PUBLIC_SPECS_REPO = "azure-rest-api-specs"; private static readonly string NAMESPACE_APPROVAL_REPO = "azure-sdk"; @@ -197,7 +211,7 @@ protected override List GetCommands() => new McpCommand(linkNamespaceApprovalIssueCommandName, "Link namespace approval issue to release plan", LinkNamespaceApprovalToolName) { workItemIdOpt, namespaceApprovalIssueOpt, }, new McpCommand(checkApiReadinessCommandName, "Check if API spec is ready to generate SDK", CheckApiSpecReadyToolName) { typeSpecProjectPathOpt, pullRequestNumberOpt, workItemIdOpt, }, new McpCommand(linkSdkPrCommandName, "Link SDK pull request to release plan", LinkSdkPullRequestToolName) { languageOpt, pullRequestOpt, workItemIdOpt, releasePlanNumberOpt, }, - new McpCommand(listOverdueReleasePlansCommandName, "List in-progress release plans that are past their SDK release deadline"), + new McpCommand(listOverdueReleasePlansCommandName, "List in-progress release plans that are past their SDK release deadline") { notifyOwnersOpt, azureSDKEmailerUriOpt, }, new McpCommand(updateApiSpecPullRequestCommandName, "Update TypeSpec pull request URL in a release plan", UpdateApiSpecPullRequestToolName) { pullRequestOpt, workItemIdOpt, releasePlanNumberOpt, } ]; @@ -246,7 +260,7 @@ public override async Task HandleCommand(ParseResult parseResul return await LinkSdkPullRequestToReleasePlan(commandParser.GetValue(languageOpt), commandParser.GetValue(pullRequestOpt), workItemId: commandParser.GetValue(workItemIdOpt), releasePlanId: commandParser.GetValue(releasePlanNumberOpt)); case listOverdueReleasePlansCommandName: - return await ListOverdueReleasePlans(); + return await ListOverdueReleasePlans(commandParser.GetValue(notifyOwnersOpt), commandParser.GetValue(azureSDKEmailerUriOpt)); case updateApiSpecPullRequestCommandName: return await UpdateSpecPullRequestInReleasePlan(specPullRequestUrl: commandParser.GetValue(pullRequestOpt), workItemId: commandParser.GetValue(workItemIdOpt), releasePlanId: commandParser.GetValue(releasePlanNumberOpt)); @@ -1013,11 +1027,21 @@ private async Task UpdateSdkPullRequestDescription(ParsedSdkPullRequest parsedUr } } - public async Task ListOverdueReleasePlans() + public async Task ListOverdueReleasePlans(bool notifyOwners = false, string emailerUri = "") { try { + if (notifyOwners && string.IsNullOrWhiteSpace(emailerUri)) + { + return new ReleasePlanListResponse { ResponseError = "Emailer URI is required when notify owners is enabled." }; + } var releasePlans = await devOpsService.ListOverdueReleasePlansAsync(); + + if (notifyOwners) + { + await NotifyOwnersOfOverdueReleasePlans(releasePlans, emailerUri); + } + return new ReleasePlanListResponse { Message = "List of overdue Release plans:", @@ -1031,6 +1055,93 @@ public async Task ListOverdueReleasePlans() } } + private async Task NotifyOwnersOfOverdueReleasePlans(List releasePlans, string emailerUri) + { + const string subject = "Action Required: Azure SDKs Not Yet Published for Your Release Plan"; + + foreach (var releasePlan in releasePlans) + { + var releaseOwnerEmail = releasePlan.ReleasePlanSubmittedByEmail; + + // Validate email address + if (string.IsNullOrWhiteSpace(releaseOwnerEmail) || !Regex.IsMatch(releaseOwnerEmail, @"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase)) + { + logger.LogWarning("Skipped notification for Release Plan ID {WorkItemId}: invalid email '{Email}'", + releasePlan.WorkItemId, releaseOwnerEmail); + continue; + } + + var releaseOwnerName = releasePlan.Owner; + var plane = releasePlan.IsManagementPlane ? "Management Plane" : "Data Plane"; + var releasePlanLink = releasePlan.ReleasePlanLink; + var releasePlanDate = releasePlan.SDKReleaseMonth; + + // Identify SDKs not yet released (skip Go for Data Plane and skip excluded languages) + var missingSDKs = releasePlan.SDKInfo + .Where(info => (string.IsNullOrEmpty(info.ReleaseStatus) || !string.Equals(info.ReleaseStatus, "Released", StringComparison.OrdinalIgnoreCase)) + && (releasePlan.IsManagementPlane || !string.Equals(info.Language, "Go", StringComparison.OrdinalIgnoreCase)) + && !string.Equals(info.ReleaseExclusionStatus, "Requested", StringComparison.OrdinalIgnoreCase) + && !string.Equals(info.ReleaseExclusionStatus, "Approved", StringComparison.OrdinalIgnoreCase)) + .Select(info => info.Language) + .ToList(); + + var body = $""" + + +

Hello {releaseOwnerName},

+

Our automation has detected that one or more Azure SDKs generated for your release plan have not yet been published to the required language package managers.

+
    +
  • Azure SDK Type: {plane}
  • +
  • SDKs not yet published: {string.Join(", ", missingSDKs)}
  • +
  • Release Plan: {releasePlanLink}
  • +
  • Release Plan Target Release Date: {releasePlanDate}
  • +
+

Per Azure SDK release requirements, all Tier 1 language SDKs must be published to their respective package managers before a release plan can be marked as complete.

+

Until the missing SDKs are published:

+
    +
  • The release plan cannot be completed in Release Planner.
  • +
  • If this release is in scope for CPEX, Cloud Lifecycle phase KPIs for Public Preview or GA will remain incomplete.
  • +
+

Required actions:

+
    +
  1. Publish the missing SDKs to their respective package managers, or
  2. +
  3. Update the target release date in the release plan, or
  4. +
  5. If publication is not intended, file an approved exception: https://eng.ms/docs/products/azure-developer-experience/onboard/request-exception
  6. +
+

Once publication is complete, this status will clear automatically. Thank you for helping maintain consistent, complete Azure SDK releases across all mandatory Tier 1 languages.

+

Best regards,

+

Azure SDK PM Team

+ + + """; + + await SendEmailNotification(emailerUri, releaseOwnerEmail, sdkApexEmail, subject, body); + } + } + + private async Task SendEmailNotification(string emailerUri, string to, string cc, string subject, string body) + { + var emailPayload = new + { + EmailTo = to, + CC = cc, + Subject = subject, + Body = body + }; + + var jsonContent = JsonSerializer.Serialize(emailPayload); + + using (var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json")) + { + logger.LogInformation("Sending Email - To: {To}, CC: {CC}, Subject: {Subject}", to, cc, subject); + + var response = await httpClient.PostAsync(emailerUri, httpContent); + response.EnsureSuccessStatusCode(); + + logger.LogInformation("Successfully sent email - To: {To}, CC: {CC}, Subject: {Subject}", to, cc, subject); + } + } + [McpServerTool(Name = UpdateApiSpecPullRequestToolName), Description("Update TypeSpec pull request URL in a release plan using work item id or release plan id.")] public async Task UpdateSpecPullRequestInReleasePlan(string specPullRequestUrl, int workItemId = 0, int releasePlanId = 0) {