Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ Task<ReleasePlanDetails> IDevOpsService.GetReleasePlanAsync(string pullRequestUr
return Task.FromResult(releasePlan);
}

Task<List<ReleasePlanDetails>> IDevOpsService.GetReleasePlansForProductAsync(string productTreeId, string specApiVersion)
{
var releasePlans = new List<ReleasePlanDetails>();
return Task.FromResult(releasePlans);
}

Task<ReleasePlanDetails> IDevOpsService.GetReleasePlanForWorkItemAsync(int workItemId)
{
var releasePlan = new ReleasePlanDetails
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')";
var releasePlanWorkItems = await FetchWorkItemsAsync(query);
if (releasePlanWorkItems.Count == 0)
{
logger.LogInformation($"Release plan does not exist for the given product id {productTreeId}");
return new List<ReleasePlanDetails>();
}

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 @@ -108,6 +108,12 @@ IEnvironmentHelper environmentHelper
Required = true,
};

private readonly Option<bool> forceCreateReleasePlanOpt = new("--force-create-release")
{
Description = "Force creation of release plan even if one already exists",
Required = true,
};

//Namespace approval repo details
private const string namespaceApprovalRepoName = "azure-sdk";
private const string namespaceApprovalRepoOwner = "Azure";
Expand All @@ -132,7 +138,7 @@ IEnvironmentHelper environmentHelper

protected override List<Command> GetCommands() =>
[
new(getReleasePlanDetailsCommandName, "Get release plan details") { workItemIdOpt, releasePlanNumberOpt },
new(getReleasePlanDetailsCommandName, "Get release plan details") {workItemIdOpt, releasePlanNumberOpt},
new(createReleasePlanCommandName, "Create a release plan")
{
typeSpecProjectPathOpt,
Expand All @@ -144,6 +150,7 @@ protected override List<Command> GetCommands() =>
sdkReleaseTypeOpt,
userEmailOpt,
isTestReleasePlanOpt,
forceCreateReleasePlanOpt,
},
new(linkNamespaceApprovalIssueCommandName, "Link namespace approval issue to release plan") { workItemIdOpt, namespaceApprovalIssueOpt }
];
Expand All @@ -169,6 +176,7 @@ public override async Task<CommandResponse> HandleCommand(ParseResult parseResul
var sdkReleaseType = commandParser.GetValue(sdkReleaseTypeOpt);
var isTestReleasePlan = commandParser.GetValue(isTestReleasePlanOpt);
var userEmail = commandParser.GetValue(userEmailOpt);
var forceCreateReleasePlan = commandParser.GetValue(forceCreateReleasePlanOpt);
return await CreateReleasePlan(
typeSpecProjectPath,
targetReleaseMonthYear,
Expand All @@ -178,7 +186,8 @@ public override async Task<CommandResponse> HandleCommand(ParseResult parseResul
specPullRequestUrl,
sdkReleaseType,
userEmail: userEmail,
isTestReleasePlan: isTestReleasePlan
isTestReleasePlan: isTestReleasePlan,
forceCreateReleasePlan: forceCreateReleasePlan
);

case linkNamespaceApprovalIssueCommandName:
Expand Down Expand Up @@ -302,7 +311,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 @@ -316,21 +325,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);
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 = $"An active release plan already exists 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