Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,7 @@ Follow these steps in order to create or manage a release plan for an API specif
- If no pull request is available, prompt the user to provide the API spec pull request link
- Validate that the provided pull request link is accessible and valid

## Step 2: Check Existing Release Plan
- Use `azsdk_get_release_plan_for_spec_pr` to check if a release plan already exists for the API spec pull request
- If a release plan exists:
- Display the existing release plan details to the user
- Skip to Step 5 (Link SDK Pull Requests)
- If no release plan exists, proceed to Step 3

## Step 3: Gather Release Plan Information
## Step 2: Gather Release Plan Information
Collect the following required information from the user. Do not create a release plan with temporary values. Confirm the values with the user before proceeding to create the release plan.
If any details are missing, prompt the user accordingly:

Expand All @@ -31,25 +24,34 @@ If any details are missing, prompt the user accordingly:
- "beta" for preview API versions
- "stable" for GA API versions

## Step 4: Create Release Plan
## Step 3: Create Release Plan
- If the user doesn't know the required details, direct them to create a release plan using the release planner
- Provide this resource: [Release Plan Creation Guide](https://eng.ms/docs/products/azure-developer-experience/plan/release-plan-create)
- Once all information is gathered, use `azsdk_create_release_plan` to create the release plan
- If existing release plans are found, follow the instructions under Step 3a - Handle Existing Release Plans
- Display the newly created release plan details to the user for confirmation
- Refer to #file:sdk-details-in-release-plan.instructions.md to identify languages configured in the TypeSpec project and add them to the release plan

## Step 5: Update SDK Details in Release Plan
### Step 3a: Handle Existing Release Plans
- When `azsdk_create_release_plan` returns existing release plans.
- Extract and display key information: Release Plan ID, status, associated languages, SDK PRs
- Present the three options:
1. **Work with the existing release plan** - Use the current release plan and make any needed updates
2. **Force create a new release plan** - Create a completely new release plan even though one already exists
3. **Cancel** - Don't proceed with release plan creation

## Step 4: Update SDK Details in Release Plan
- Refer to #file:sdk-details-in-release-plan.instructions.md to add languages and package names to the release plan
- If the TypeSpec project is for a management plane, refer to #file:verify-namespace-approval.instructions.md if this is first release of SDK.

## Step 6: Link SDK Pull Requests (if applicable)
## Step 5: Link SDK Pull Requests (if applicable)
- Ask the user if they have already created SDK pull requests locally for any programming language
- If SDK pull requests exist:
- Collect the pull request links from the user
- Use `azsdk_link_sdk_pull_request_to_release_plan` to link each SDK pull request to the release plan
- Confirm successful linking for each SDK pull request

## Step 7: Summary
## Step 6: Summary
- Display a summary of the completed actions:
- Release plan status (created or existing)
- Linked SDK pull requests (if any)
Expand Down
35 changes: 34 additions & 1 deletion tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public interface IDevOpsService
public Task<ReleasePlanDetails> GetReleasePlanAsync(int releasePlanId);
public Task<ReleasePlanDetails> GetReleasePlanForWorkItemAsync(int workItemId);
public Task<ReleasePlanDetails> GetReleasePlanAsync(string pullRequestUrl);
public Task<List<ReleasePlanDetails>> GetReleasePlansForProductAsync(string productTreeId, string specApiVersion);
public Task<WorkItem> CreateReleasePlanWorkItemAsync(ReleasePlanDetails releasePlan);
public Task<Build> RunSDKGenerationPipelineAsync(string apiSpecBranchRef, string typespecProjectRoot, string apiVersion, string sdkReleaseType, string language, int workItemId, string sdkRepoBranch = "");
public Task<Build> GetPipelineRunAsync(int buildId);
Expand Down Expand Up @@ -136,6 +137,38 @@ public async Task<ReleasePlanDetails> GetReleasePlanAsync(int releasePlanId)
return await MapWorkItemToReleasePlanAsync(releasePlanWorkItems[0]);
}

public async Task<List<ReleasePlanDetails>> GetReleasePlansForProductAsync(string productTreeId, string specApiVersion)
{
try
{
var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}' AND [Custom.ProductServiceTreeID] = '{productTreeId}' AND [System.WorkItemType] = 'Release Plan' AND [System.State] IN ('New','Not Started','In Progress')";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should skip the test release plans created with Tag Release Planner App Test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also check whether existing release plan is for same SDK release type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh interesting. This is a great find!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should skip the test release plans created with Tag Release Planner App Test

with the condition that if the mcp is running under test mode, then we do want to make sure we only list the work items that have that tag :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. So we should avoid the release plans with test app tag when test mode env variable is present.

var releasePlanWorkItems = await FetchWorkItemsAsync(query);
if (releasePlanWorkItems.Count == 0)
{
logger.LogInformation($"Release plan does not exist for the given product id {productTreeId}");
return null;
}

var releasePlans = new List<ReleasePlanDetails>();

foreach (var workItem in releasePlanWorkItems)
{
var releasePlan = await MapWorkItemToReleasePlanAsync(workItem);
if (releasePlan.SpecAPIVersion == specApiVersion)
{
releasePlans.Add(releasePlan);
}
}

return releasePlans;
}
catch (Exception ex)
{
logger.LogError($"Failed to get release plans for product id {productTreeId}. Error: {ex.Message}");
throw new Exception($"Failed to get release plans for product id {productTreeId}. Error: {ex.Message}");
}
}

private async Task<ReleasePlanDetails> MapWorkItemToReleasePlanAsync(WorkItem workItem)
{
var releasePlan = new ReleasePlanDetails()
Expand Down Expand Up @@ -178,7 +211,7 @@ private async Task<ReleasePlanDetails> MapWorkItemToReleasePlanAsync(WorkItem wo
{
Language = MapLanguageIdToName(lang),
GenerationPipelineUrl = sdkGenPipelineUrl,
SdkPullRequestUrl = sdkPullRequestUrl,
SdkPullRequestUrl = sdkPullRequestUrl,
ReleaseStatus = releaseStatus,
PackageName = packageName,
ReleaseExclusionStatus = exclusionStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ IEnvironmentHelper environmentHelper
private readonly Option<bool> isTestReleasePlanOpt = new(["--test-release"], () => false, "Create release plan in test environment") { IsRequired = false };
private readonly Option<string> userEmailOpt = new(["--user-email"], "User email for release plan creation") { IsRequired = false };
private readonly Option<string> namespaceApprovalIssueOpt = new Option<string>(["--namespace-approval-issue"], "Namespace approval issue URL") { IsRequired = true };
private readonly Option<bool> forceCreateReleasePlanOpt = new(["--force-create-release"], () => false, "Force creation of release plan even if one already exists") { IsRequired = false };

//Namespace approval repo details
private const string namespaceApprovalRepoName = "azure-sdk";
Expand All @@ -73,7 +74,7 @@ IEnvironmentHelper environmentHelper
protected override List<Command> GetCommands() =>
[
new(getReleasePlanDetailsCommandName, "Get release plan details") {workItemIdOpt, releasePlanNumberOpt},
new(createReleasePlanCommandName, "Create a release plan") { typeSpecProjectPathOpt, targetReleaseOpt, serviceTreeIdOpt, productTreeIdOpt, apiVersionOpt, pullRequestOpt, sdkReleaseTypeOpt, userEmailOpt, isTestReleasePlanOpt },
new(createReleasePlanCommandName, "Create a release plan") { typeSpecProjectPathOpt, targetReleaseOpt, serviceTreeIdOpt, productTreeIdOpt, apiVersionOpt, pullRequestOpt, sdkReleaseTypeOpt, userEmailOpt, isTestReleasePlanOpt, forceCreateReleasePlanOpt },
new(linkNamespaceApprovalIssueCommandName, "Link namespace approval issue to release plan") { workItemIdOpt, namespaceApprovalIssueOpt }
];

Expand All @@ -98,7 +99,8 @@ public override async Task<CommandResponse> HandleCommand(InvocationContext ctx,
var sdkReleaseType = commandParser.GetValueForOption(sdkReleaseTypeOpt);
var isTestReleasePlan = commandParser.GetValueForOption(isTestReleasePlanOpt);
var userEmail = commandParser.GetValueForOption(userEmailOpt);
return await CreateReleasePlan(typeSpecProjectPath, targetReleaseMonthYear, serviceTreeId, productTreeId, specApiVersion, specPullRequestUrl, sdkReleaseType, userEmail: userEmail, isTestReleasePlan: isTestReleasePlan);
var forceCreateReleasePlan = commandParser.GetValueForOption(forceCreateReleasePlanOpt);
return await CreateReleasePlan(typeSpecProjectPath, targetReleaseMonthYear, serviceTreeId, productTreeId, specApiVersion, specPullRequestUrl, sdkReleaseType, userEmail: userEmail, isTestReleasePlan: isTestReleasePlan, forceCreateReleasePlan);

case linkNamespaceApprovalIssueCommandName:
return await LinkNamespaceApprovalIssue(commandParser.GetValueForOption(workItemIdOpt), commandParser.GetValueForOption(namespaceApprovalIssueOpt));
Expand Down Expand Up @@ -221,7 +223,7 @@ Please create a pull request in the public Azure/azure-rest-api-specs repository
}

[McpServerTool(Name = "azsdk_create_release_plan"), Description("Create Release Plan")]
public async Task<ObjectCommandResponse> CreateReleasePlan(string typeSpecProjectPath, string targetReleaseMonthYear, string serviceTreeId, string productTreeId, string specApiVersion, string specPullRequestUrl, string sdkReleaseType, string userEmail = "", bool isTestReleasePlan = false)
public async Task<ObjectCommandResponse> CreateReleasePlan(string typeSpecProjectPath, string targetReleaseMonthYear, string serviceTreeId, string productTreeId, string specApiVersion, string specPullRequestUrl, string sdkReleaseType, string userEmail = "", bool isTestReleasePlan = false, bool forceCreateReleasePlan = false)
{
try
{
Expand All @@ -235,21 +237,37 @@ public async Task<ObjectCommandResponse> CreateReleasePlan(string typeSpecProjec
{
sdkReleaseType = mappedType;
}

ValidateCreateReleasePlanInputAsync(typeSpecProjectPath, serviceTreeId, productTreeId, specPullRequestUrl, sdkReleaseType, specApiVersion);

// Check for existing release plan for the given pull request URL.
logger.LogInformation("Checking for existing release plan for pull request URL: {specPullRequestUrl}", specPullRequestUrl);
var existingReleasePlan = await devOpsService.GetReleasePlanAsync(specPullRequestUrl);
if (existingReleasePlan != null && existingReleasePlan.WorkItemId > 0)
ValidateCreateReleasePlanInputAsync(typeSpecProjectPath, serviceTreeId, productTreeId, specPullRequestUrl, sdkReleaseType, specApiVersion);

if (!forceCreateReleasePlan)
{
return new ObjectCommandResponse
// Check for existing release plan for the given pull request URL.
logger.LogInformation("Checking for existing release plan for pull request URL: {specPullRequestUrl}", specPullRequestUrl);
var existingReleasePlan = await devOpsService.GetReleasePlanAsync(specPullRequestUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there are multiples Release plans with the same API spec PR, wouldn't this return a list of release plans?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think service method fetches only latest release plan. @smw-ms can you please check and confirm this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one release plan is returned. looking at the code there is no sorting happening, it just returns the first linked release plan it comes across, so there is no guarantee that it is the latest release plan

if (existingReleasePlan != null && existingReleasePlan.WorkItemId > 0)
{
Message = $"Release plan already exists for the pull request: {specPullRequestUrl}. Release plan link: {existingReleasePlan.ReleasePlanLink}",
Result = existingReleasePlan
};
}
return new ObjectCommandResponse
{
Message = $"Release plan not created. Release plan already exists for the pull request: {specPullRequestUrl}. Release plan link: {existingReleasePlan.ReleasePlanLink}",
Result = existingReleasePlan
};
}

logger.LogInformation("Checking for existing release plans for product: {productTreeId}", productTreeId);
var existingReleasePlans = await devOpsService.GetReleasePlansForProductAsync(productTreeId, specApiVersion);
existingReleasePlans = existingReleasePlans.Where(releasePlan => releasePlan.WorkItemId > 0).ToList();
if (existingReleasePlans.Any())
{
return new ObjectCommandResponse
{
Message = $" Release plan not created. Release plan(s) already exist for the product: {productTreeId}."
+ $"Release plan link(s): {string.Join("\n ", existingReleasePlans.Select(p => p.ReleasePlanLink))}",
Result = existingReleasePlans
};
}
}

// Check environment variable to determine if this should be a test release plan
var isAgentTesting = environmentHelper.GetBooleanVariable("AZSDKTOOLS_AGENT_TESTING", false);
if (isAgentTesting)
Expand Down
Loading