diff --git a/eng/common/instructions/azsdk-tools/check-package-readiness.instructions.md b/eng/common/instructions/azsdk-tools/check-package-readiness.instructions.md index bb4334dc586..486d59436be 100644 --- a/eng/common/instructions/azsdk-tools/check-package-readiness.instructions.md +++ b/eng/common/instructions/azsdk-tools/check-package-readiness.instructions.md @@ -15,7 +15,7 @@ Check the release readiness of an SDK package by collecting the required informa - Go 2. **Execute Readiness Check**: - - Use the `azsdk_check_package_release_readiness` tool with the provided package name and selected language + - Use the `azsdk_release_sdk` tool with the provided package name, selected language, and set checkReady to true. - Do not check for existing pull requests to run this step. - Do not ask the user to create a release plan to run this step. diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/SdkReleaseToolTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/SdkReleaseToolTests.cs index 400b8274f10..b806d7ce468 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/SdkReleaseToolTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/SdkReleaseToolTests.cs @@ -67,12 +67,9 @@ public void Setup() }); devOpsService = mockDevOpsService.Object; - var releaseReadinessToolLogger = new TestLogger(); - sdkReleaseTool = new SdkReleaseTool( devOpsService, logger, - releaseReadinessToolLogger, new InputSanitizer()); } @@ -108,6 +105,24 @@ public async Task TestRunReleaseWithInvalidLanguage() }); } + [Test] + public async Task TestRunReleaseWithCheckReady() + { + var packageName = "azure-template"; + var language = "Python"; + var result = await sdkReleaseTool.ReleasePackageAsync(packageName, language, "main", checkReady: true); + + Assert.That(result, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(result.PackageName, Is.EqualTo(packageName)); + Assert.That(result.Language, Is.EqualTo(SdkLanguage.Python)); + Assert.That(result.ReleaseStatusDetails, Does.Contain("Package 'azure-template' is ready for release.")); + Assert.That(result.ReleasePipelineRunUrl, Is.EqualTo(string.Empty)); + Assert.That(result.PipelineBuildId, Is.EqualTo(0)); + }); + } + [Test] public async Task TestRunReleaseWithCsharpLanguage() { diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedOptions.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedOptions.cs index 6c84e618fa8..8c50c8c63d9 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedOptions.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedOptions.cs @@ -29,7 +29,6 @@ public static class SharedOptions typeof(SampleGeneratorTool), typeof(SampleTranslatorTool), typeof(ReleasePlanTool), - typeof(ReleaseReadinessTool), typeof(SpecWorkflowTool), typeof(SdkBuildTool), typeof(SdkGenerationTool), diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/ReleaseReadinessTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/ReleaseReadinessTool.cs deleted file mode 100644 index f0b3510472e..00000000000 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/ReleaseReadinessTool.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.CommandLine; -using System.CommandLine.Parsing; -using System.ComponentModel; -using Microsoft.TeamFoundation.Build.WebApi; -using ModelContextProtocol.Server; -using Azure.Sdk.Tools.Cli.Commands; -using Azure.Sdk.Tools.Cli.Helpers; -using Azure.Sdk.Tools.Cli.Models; -using Azure.Sdk.Tools.Cli.Services; -using Azure.Sdk.Tools.Cli.Models.Responses.Package; - -namespace Azure.Sdk.Tools.Cli.Tools.Package -{ - [Description("This class contains an MCP tool that checks the release readiness status of a package")] - [McpServerToolType] - public class ReleaseReadinessTool( - IDevOpsService devopsService, - ILogger logger - ) : MCPTool - { - private const string CheckPackageReleaseReadinessToolName = "azsdk_check_package_release_readiness"; - - public override CommandGroup[] CommandHierarchy { get; set; } = [SharedCommandGroups.Package]; - private readonly Option packageNameOpt = new("--package-name") - { - Description = "SDK package name", - Required = true, - }; - - private readonly Option languageOpt = new("--language") - { - Description = "SDK language from one of the following ['.NET', 'Python', 'Java', 'JavaScript', Go]", - Required = true, - }; - private static readonly string Pipeline_Success_Status = "Succeeded"; - - protected override Command GetCommand() => - new McpCommand("release-readiness", "Checks release readiness of a SDK package", CheckPackageReleaseReadinessToolName) { packageNameOpt, languageOpt }; - - public override async Task HandleCommand(ParseResult parseResult, CancellationToken ct) - { - var packageName = parseResult.GetValue(packageNameOpt); - var language = parseResult.GetValue(languageOpt); - logger.LogInformation("Running release readiness check for {packageName} in {language}", packageName, language); - return await CheckPackageReleaseReadinessAsync(packageName, language); - } - - [McpServerTool(Name = CheckPackageReleaseReadinessToolName), Description("Checks if SDK package is ready to release (release readiness). This includes checking pipeline status, apiview status, change log status, and namespace approval status.")] - public async Task CheckPackageReleaseReadinessAsync(string packageName, string language) - { - try - { - var package = await devopsService.GetPackageWorkItemAsync(packageName, language); - if (package == null) - { - package = new PackageWorkitemResponse - { - PackageName = packageName, - ResponseError = $"No package work item found for package '{packageName}' in language '{language}'. Please check the package name and language." - }; - package.SetLanguage(language); - return package; - } - - package.IsPackageReady = package.IsChangeLogReady; - - //Check release date for latest version in planned release - var plannedRelease = package.PlannedReleases.FirstOrDefault(r => r.Version.Equals(package.Version)) ?? package.PlannedReleases.LastOrDefault(); - package.PlannedReleaseDate = plannedRelease?.ReleaseDate ?? "Unknown"; - if (package.PlannedReleaseDate.Equals("Unknown")) - { - package.IsPackageReady = false; - package.PackageReadinessDetails = $"No planned release date found in package details for current package version {package.Version}. Please check the package version and verify that change log file is correct. "; - } - - var releaseType = plannedRelease?.ReleaseType ?? "Unknown"; - bool isPreviewRelease = releaseType.Equals("Beta"); - bool isDataPlanePackage = package.PackageType != SdkType.Management; - // Check for namespace approval if preview release for data plane - if (isDataPlanePackage && isPreviewRelease) - { - if (!package.IsPackageNameApproved) - { - package.IsPackageReady = false; - package.PackageReadinessDetails += $"Package name '{packageName}' is not approved for preview release. "; - } - // no need to add extra package name approval status if package name is approved or has at least one version already released - } - else - { - package.PackageNameStatus = "Not required"; - package.PackageNameApprovalDetails = "Package name approval is not required for GA releases of data plane packages or for non-data plane packages."; - } - - // Check if API view is approved if stable version for data plane or .NET - if ((isDataPlanePackage || language.Equals(".NET")) && !isPreviewRelease) - { - - if (!package.IsApiViewApproved) - { - package.IsPackageReady = false; - package.PackageReadinessDetails += $"API view is not approved for GA release of package '{packageName}'. "; - } - } - else - { - package.APIViewStatus = "Not required"; - package.ApiViewValidationDetails = "API view is not required for preview releases of data plane packages or for non-data plane packages."; - } - - // Check last pipeline run status for the package and verify it completed successfully - package.LatestPipelineStatus = await GetPipelineRunDetails(package.LatestPipelineRun); - bool hasPipelineWarning = string.IsNullOrEmpty(package.LatestPipelineStatus) || !package.LatestPipelineStatus.Contains(Pipeline_Success_Status); - - // Package release readiness status - if (package.IsPackageReady) - { - package.PackageReadinessDetails = $"Package '{packageName}' is ready for release. Queue a release pipeline run using the link {package.PipelineDefinitionUrl} to release the package."; - - // Add warning about pipeline status if not successful - if (hasPipelineWarning) - { - package.PackageReadinessDetails += $"\n\nWARNING: The last known CI pipeline status for this package has failed. This might cause issues when running the release pipeline if the error was not a transient failure. Please review the last pipeline run at {package.LatestPipelineRun} to verify the failure was transient before proceeding with the release."; - } - } - else - { - package.PackageReadinessDetails += $"Package '{packageName}' is not ready for release. Please address the issues mentioned above."; - } - return package; - } - catch (Exception ex) - { - var package = new PackageWorkitemResponse - { - PackageName = packageName, - IsPackageReady = false, - ResponseError = $"Failed to check package readiness for '{packageName}' in language '{language}'. Error {ex.Message}" - }; - package.SetLanguage(language); - return package; - } - } - - private async Task GetPipelineRunDetails(string pipelineRunUrl) - { - try - { - logger.LogInformation("Getting pipeline run details for URL: {pipelineRunUrl}", pipelineRunUrl); - if (!string.IsNullOrEmpty(pipelineRunUrl) && pipelineRunUrl.Contains("buildId=")) - { - var buildId = int.Parse(pipelineRunUrl.Split("buildId=").LastOrDefault()); - logger.LogInformation("Extracted build ID: {buildId}", buildId); - var pipelineRun = await devopsService.GetPipelineRunAsync(buildId); - if (pipelineRun != null) - { - logger.LogInformation( - "Pipeline status: {PipelineStatus}, Result: {PipelineResult}", - pipelineRun.Status, - pipelineRun.Result); - var status = (pipelineRun.Status == BuildStatus.Completed ? pipelineRun.Result?.ToString() : pipelineRun.Status.ToString()) ?? "Unknown"; - if (!status.Contains(Pipeline_Success_Status)) - { - status = $"Pipeline run with ID {buildId} did not succeed. Status: {status}. Please check the pipeline run details at {DevOpsService.GetPipelineUrl(buildId)} for more information."; - } - return status; - } - } - return $"Failed to get pipeline run details. The pipeline run URL '{pipelineRunUrl}' is invalid or does not contain a valid build ID."; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to get pipeline run details for URL {PipelineRunUrl}", pipelineRunUrl); - return $"Failed to get pipeline run details. Error: {ex.Message}"; - } - } - } -} diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/SdkReleaseTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/SdkReleaseTool.cs index 1a773ce388c..64ba539db98 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/SdkReleaseTool.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/SdkReleaseTool.cs @@ -1,12 +1,13 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.ComponentModel; +using Microsoft.TeamFoundation.Build.WebApi; +using ModelContextProtocol.Server; using Azure.Sdk.Tools.Cli.Commands; using Azure.Sdk.Tools.Cli.Helpers; using Azure.Sdk.Tools.Cli.Models; using Azure.Sdk.Tools.Cli.Models.Responses.Package; using Azure.Sdk.Tools.Cli.Services; -using ModelContextProtocol.Server; namespace Azure.Sdk.Tools.Cli.Tools.Package { @@ -14,10 +15,10 @@ namespace Azure.Sdk.Tools.Cli.Tools.Package public class SdkReleaseTool( IDevOpsService devopsService, ILogger logger, - ILogger releaseReadinessLogger, IInputSanitizer inputSanitizer) : MCPTool { private const string ReleaseSdkToolName = "azsdk_release_sdk"; + private const string Pipeline_Success_Status = "Succeeded"; public override CommandGroup[] CommandHierarchy { get; set; } = [SharedCommandGroups.Package]; @@ -40,12 +41,19 @@ public class SdkReleaseTool( Required = false, DefaultValueFactory = _ => "main", }; + + private readonly Option checkReadyOpt = new("--check-ready") + { + Description = "Verify package release readiness without triggering the release pipeline", + Required = false, + }; + public static readonly string[] ValidLanguages = [".NET", "Go", "Java", "JavaScript", "Python"]; protected override Command GetCommand() => new McpCommand(commandName, "Run the release pipeline for the package", ReleaseSdkToolName) { - packageNameOpt, languageOpt, branchOpt, + packageNameOpt, languageOpt, branchOpt, checkReadyOpt }; public override async Task HandleCommand(ParseResult parseResult, CancellationToken ct) @@ -53,11 +61,12 @@ public override async Task HandleCommand(ParseResult parseResul var packageName = parseResult.GetValue(packageNameOpt); var language = parseResult.GetValue(languageOpt); var branch = parseResult.GetValue(branchOpt); - return await ReleasePackageAsync(packageName, language, branch); + var checkReady = parseResult.GetValue(checkReadyOpt); + return await ReleasePackageAsync(packageName, language, branch, checkReady); } - [McpServerTool(Name = ReleaseSdkToolName), Description("Releases the specified SDK package for a language. This includes checking if the package is ready for release and triggering the release pipeline. This tool calls CheckPackageReleaseReadiness")] - public async Task ReleasePackageAsync(string packageName, string language, string branch = "main") + [McpServerTool(Name = ReleaseSdkToolName), Description("Releases the specified SDK package for a language. This includes checking if the package is ready for release and triggering the release pipeline. To ONLY check package release readiness pass checkReady as true.")] + public async Task ReleasePackageAsync(string packageName, string language, string branch = "main", bool checkReady = false) { try { @@ -116,8 +125,7 @@ public async Task ReleasePackageAsync(string packageName, st } // Check if the package is ready for release - var releaseReadinessTool = new ReleaseReadinessTool(devopsService, releaseReadinessLogger); - var releaseReadiness = await releaseReadinessTool.CheckPackageReleaseReadinessAsync(packageName, language); + var releaseReadiness = await CheckPackageReleaseReadinessAsync(packageName, language); if (!releaseReadiness.IsPackageReady) { response.ReleaseStatusDetails = $"Package is not ready for release. {releaseReadiness.PackageReadinessDetails}"; @@ -126,6 +134,14 @@ public async Task ReleasePackageAsync(string packageName, st return response; } + // If check-ready mode, return readiness check results without triggering release + if (checkReady) + { + response.ReleaseStatusDetails = releaseReadiness.PackageReadinessDetails; + logger.LogInformation("[CHECK READY] Package readiness check completed for {packageName} in {language}.", packageName, language); + return response; + } + var buildDefinitionId = package?.PipelineDefinitionUrl?.Split('=')?.LastOrDefault(); logger.LogInformation("Package {packageName} is ready for release in {language}.", packageName, language); logger.LogInformation("Release pipeline: {pipelineUrl}", package?.PipelineDefinitionUrl); @@ -172,5 +188,134 @@ public async Task ReleasePackageAsync(string packageName, st return response; } } + + private async Task CheckPackageReleaseReadinessAsync(string packageName, string language) + { + try + { + var package = await devopsService.GetPackageWorkItemAsync(packageName, language); + if (package == null) + { + package = new PackageWorkitemResponse + { + PackageName = packageName, + ResponseError = $"No package work item found for package '{packageName}' in language '{language}'. Please check the package name and language." + }; + package.SetLanguage(language); + return package; + } + + package.IsPackageReady = package.IsChangeLogReady; + + //Check release date for latest version in planned release + var plannedRelease = package.PlannedReleases.FirstOrDefault(r => r.Version.Equals(package.Version)) ?? package.PlannedReleases.LastOrDefault(); + package.PlannedReleaseDate = plannedRelease?.ReleaseDate ?? "Unknown"; + if (package.PlannedReleaseDate.Equals("Unknown")) + { + package.IsPackageReady = false; + package.PackageReadinessDetails = $"No planned release date found in package details for current package version {package.Version}. Please check the package version and verify that change log file is correct. "; + } + + var releaseType = plannedRelease?.ReleaseType ?? "Unknown"; + bool isPreviewRelease = releaseType.Equals("Beta"); + bool isDataPlanePackage = package.PackageType != SdkType.Management; + // Check for namespace approval if preview release for data plane + if (isDataPlanePackage && isPreviewRelease) + { + if (!package.IsPackageNameApproved) + { + package.IsPackageReady = false; + package.PackageReadinessDetails += $"Package name '{packageName}' is not approved for preview release. "; + } + // no need to add extra package name approval status if package name is approved or has at least one version already released + } + else + { + package.PackageNameStatus = "Not required"; + package.PackageNameApprovalDetails = "Package name approval is not required for GA releases of data plane packages or for non-data plane packages."; + } + + // Check if API view is approved if stable version for data plane or .NET + if ((isDataPlanePackage || language.Equals(".NET")) && !isPreviewRelease) + { + + if (!package.IsApiViewApproved) + { + package.IsPackageReady = false; + package.PackageReadinessDetails += $"API view is not approved for GA release of package '{packageName}'. "; + } + } + else + { + package.APIViewStatus = "Not required"; + package.ApiViewValidationDetails = "API view is not required for preview releases of data plane packages or for non-data plane packages."; + } + + // Check last pipeline run status for the package and verify it completed successfully + package.LatestPipelineStatus = await GetPipelineRunDetails(package.LatestPipelineRun); + bool hasPipelineWarning = string.IsNullOrEmpty(package.LatestPipelineStatus) || !package.LatestPipelineStatus.Contains(Pipeline_Success_Status); + + // Package release readiness status + if (package.IsPackageReady) + { + package.PackageReadinessDetails = $"Package '{packageName}' is ready for release. Queue a release pipeline run using the link {package.PipelineDefinitionUrl} to release the package."; + + // Add warning about pipeline status if not successful + if (hasPipelineWarning) + { + package.PackageReadinessDetails += $"\n\nWARNING: The last known CI pipeline status for this package has failed. This might cause issues when running the release pipeline if the error was not a transient failure. Please review the last pipeline run at {package.LatestPipelineRun} to verify the failure was transient before proceeding with the release."; + } + } + else + { + package.PackageReadinessDetails += $"Package '{packageName}' is not ready for release. Please address the issues mentioned above."; + } + return package; + } + catch (Exception ex) + { + var package = new PackageWorkitemResponse + { + PackageName = packageName, + IsPackageReady = false, + ResponseError = $"Failed to check package readiness for '{packageName}' in language '{language}'. Error {ex.Message}" + }; + package.SetLanguage(language); + return package; + } + } + + private async Task GetPipelineRunDetails(string pipelineRunUrl) + { + try + { + logger.LogInformation("Getting pipeline run details for URL: {pipelineRunUrl}", pipelineRunUrl); + if (!string.IsNullOrEmpty(pipelineRunUrl) && pipelineRunUrl.Contains("buildId=")) + { + var buildId = int.Parse(pipelineRunUrl.Split("buildId=").LastOrDefault()); + logger.LogInformation("Extracted build ID: {buildId}", buildId); + var pipelineRun = await devopsService.GetPipelineRunAsync(buildId); + if (pipelineRun != null) + { + logger.LogInformation( + "Pipeline status: {PipelineStatus}, Result: {PipelineResult}", + pipelineRun.Status, + pipelineRun.Result); + var status = (pipelineRun.Status == BuildStatus.Completed ? pipelineRun.Result?.ToString() : pipelineRun.Status.ToString()) ?? "Unknown"; + if (!status.Contains(Pipeline_Success_Status)) + { + status = $"Pipeline run with ID {buildId} did not succeed. Status: {status}. Please check the pipeline run details at {DevOpsService.GetPipelineUrl(buildId)} for more information."; + } + return status; + } + } + return $"Failed to get pipeline run details. The pipeline run URL '{pipelineRunUrl}' is invalid or does not contain a valid build ID."; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get pipeline run details for URL {PipelineRunUrl}", pipelineRunUrl); + return $"Failed to get pipeline run details. Error: {ex.Message}"; + } + } } }