Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
45b93d6
GitHub actions to automate creating release plan
praveenkuttappan Jun 20, 2026
a4a8d37
Update to use latest API version
praveenkuttappan Jun 20, 2026
4495f80
Move script from yaml to js
praveenkuttappan Jun 22, 2026
c401ee2
Create a ADO pipeline that gets automatically triggered to generate a…
praveenkuttappan Jun 24, 2026
a4f9578
Merge branch 'main' into automate_release_plan_create
praveenkuttappan Jun 24, 2026
901820b
split test files
praveenkuttappan Jun 25, 2026
14b4a65
Fix workflow formatting
praveenkuttappan Jun 25, 2026
0135495
Fix lint error
praveenkuttappan Jun 25, 2026
6db7b12
Fix lint errors
praveenkuttappan Jun 26, 2026
9ce356d
Fix tests
praveenkuttappan Jun 26, 2026
0721e53
Use relative path to TypeSpec project
praveenkuttappan Jun 26, 2026
241e3da
Apply suggestion from @praveenkuttappan
praveenkuttappan Jun 29, 2026
7324b97
Update to import js file
praveenkuttappan Jun 29, 2026
e8ebf62
Add optional PR number param
praveenkuttappan Jun 29, 2026
a08a1fa
Prettier update
praveenkuttappan Jun 29, 2026
eb2bb69
Merge branch 'main' into automate_release_plan_create
mikeharder Jun 30, 2026
32da2e2
Updated as per review suggestions
praveenkuttappan Jun 30, 2026
75457d4
Merge branch 'automate_release_plan_create' of https://github.com/Azu…
praveenkuttappan Jun 30, 2026
b591be5
Updated node version to match
praveenkuttappan Jun 30, 2026
1d6fdf7
Sort sections in package.json
mikeharder Jun 30, 2026
7c14a30
Merge branch 'main' into automate_release_plan_create
mikeharder Jun 30, 2026
7f0d823
Update eng/tools/release-plan/package.json
praveenkuttappan Jun 30, 2026
a31b51e
Removed node install and working directory param
praveenkuttappan Jun 30, 2026
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
27 changes: 27 additions & 0 deletions .github/workflows/release-plan-test.yaml
Comment thread
mikeharder marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: release-plan - Test

on:
push:
branches:
- main
pull_request:
paths:
- package-lock.json
- package.json
- tsconfig.json
- .github/workflows/_reusable-eng-tools-test.yaml
- .github/workflows/release-plan-tests.yaml
- eng/tools/package.json
- eng/tools/tsconfig.json
- eng/tools/release-plan/**
workflow_dispatch:

permissions:
contents: read

jobs:
release-plan:
uses: ./.github/workflows/_reusable-eng-tools-test.yaml
with:
package: release-plan
lint: true
181 changes: 181 additions & 0 deletions documentation/release-plan-auto-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Release Plan Auto-Generation

This document explains how release plans are automatically discovered or created by the `eng/tools/release-plan` tool and the `eng/pipelines/release-plan.yml` pipeline.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you help to describe the problem you plan to solve? What are the challenges we are facing today?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Currently release plan needs to be created using the agent. This can lead to agent nondeterministic behavior and also costs tokens when this can be automated without using agent at all. So, a service team does not have to worry about creating a release plan or missing release plan for their spec. Every auto generated SDK pull requests will have a release plan linked to it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can add SDK generation as the second stage of this pipeline so SDK pull request will be auto generated after a release plan is created once API spec is merged. Currently one of the concerns from a few service teams is that they need to run these steps manually using the agent. This will also ensure SDK PR has a release plan.


## Overview

The release-plan pipeline is a post-merge automation for specification updates.

High-level lifecycle:

1. A spec PR merges into `main` in `Azure/azure-rest-api-specs` or `Azure/azure-rest-api-specs-pr`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shall we only target merge spec PR only in Azure/azure-rest-api-specs repo? We could not release SDK from the spec private repo.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SDK stage will not run for private repo. It is only to create private preview release plan for KPI completion.

2. The pipeline is triggered by changes under `specification/**`.
3. Stage (`ReleasePlanCreation`) runs and resolves the commit SHA to analyze.
4. The tool tries to map the commit to an associated PR and inspects TypeSpec changes.
5. The tool first attempts to find an existing release plan.
6. The tool creates a release plan only when creation is allowed by label policy and if a release plan does not exists.
7. The pipeline always emits a JSON result artifact for downstream stages/jobs.

From pipeline execution perspective, the release-plan tool operates in this order:

1. Resolve associated PR from commit SHA (if any).
2. Detect TypeSpec project and API version from changed spec files.
3. Check whether the associated PR has the `new-api-version` label.
4. Find an existing release plan first.
5. Create a new release plan only when allowed.
6. Publish JSON output as a pipeline artifact.

## Trigger Model (When This Runs)

Pipeline trigger behavior in `eng/pipelines/release-plan.yml`:

- `trigger.paths.include: specification/**`
- `pr: none`

Meaning:

- This pipeline does not run as a PR-validation pipeline.
- It runs on branch updates (CI trigger) when files under `specification/**` change.
- In the common path, this happens automatically after a spec PR is merged to `main`.

Commit SHA source:

1. Primary: `$(Build.SourceVersion)`
2. Fallback: repository `HEAD` via `git rev-parse HEAD` when `Build.SourceVersion` is empty

So after a merge to `main`, the pipeline analyzes the latest merged commit (or latest `HEAD` if fallback is needed).

## Inputs

The tool is invoked with:

- `--commit-sha`: commit to analyze
- `--repo`: repository in `owner/repo` format
- `--workspace`: local repo path
- `--azsdk-path`: azsdk CLI path
- `--output-file`: optional JSON output path for artifact publishing

## How TypeSpec Project Is Determined

Changed files are inspected under `specification/**`.

Project detection logic:

1. Prefer changed `tspconfig.yaml` paths.
2. Otherwise, walk up from changed files to find nearest `tspconfig.yaml`.
3. If no project is found: no release plan action.
4. If multiple projects are found: no automatic create (manual handling required).

## How API Version Is Determined

API version is selected from detected project changes in this order:

1. Version-like strings found in changed file paths (`YYYY-MM-DD` or `YYYY-MM-DD-preview`).
2. If not found, scan project `main.tsp` for version-like strings.
3. Sort versions descending and pick the first one.

Sorting rules:

- Newer date wins.
- For same date, GA wins over `-preview`.

Result:

- Selected value is used as `apiVersion` in release-plan metadata.
- Preview detection (`-preview`) drives SDK release type and API release type mapping.

## Create vs No-Create Scenarios

### Scenario A: Associated PR has `new-api-version` label

Behavior:

1. Try `release-plan get --pull-request` (if PR URL exists).
2. Try `release-plan get --typespec-path --api-release-type`.
3. If still not found, run `release-plan create`.

Outcome:

- Existing plan is returned if found.
- New plan is created only in this scenario.

### Scenario B: Associated PR does NOT have `new-api-version` label

Behavior:

1. Try `release-plan get --pull-request` (if PR URL exists).
2. Try `release-plan get --typespec-path --api-release-type`.
3. Do NOT create a new release plan.

Outcome:

- Existing plan is returned if present.
- If no existing plan is found, result is `not_found`.

### Scenario C: No PR associated with commit SHA

Behavior:

- Project/version detection falls back to commit-level file changes.
- PR URL is unavailable.
- Existing-by-path lookup still runs.
- Create is not possible without PR URL and is skipped unless a PR is resolved.

Outcome:

- Existing plan may still be returned by path.
- If none found and create is not allowed, result is `not_found`.

## API Release Type and SDK Release Type

It uses selected API version status and repo name:

- Repo `azure-rest-api-specs-pr` -> API release type: `Private Preview`
- Repo `azure-rest-api-specs`:
- preview API version -> `Public Preview`
- stable API version -> `GA`

SDK release type:

- preview API version -> `preview`
- stable API version -> `stable`

## PR Comment Behavior

PR comment is posted only when:

- A new release plan is created, and
- An associated PR number is available.

## Pipeline Artifact Output

The pipeline writes release-plan result JSON to:

- `$(Build.ArtifactStagingDirectory)/release-plan/release-plan.json`

It publishes artifact:

- `release-plan-details`

If no file is generated, pipeline creates a fallback JSON:

- `{"outcome":"not_found","releasePlan":null,"details":{}}`

## Stage Layout

Current pipeline stage:

- Stage 1: `ReleasePlanCreation`

This stage:

- Authenticates npm
- Logs into GitHub
- Installs azsdk CLI
- Runs release-plan tool
- Publishes release-plan details artifact


## Future enhancement

Run SDK generation for the release plan as stage 2 using release plan details identified in stage 1.
100 changes: 100 additions & 0 deletions eng/pipelines/release-plan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
parameters:
- name: Test-release-Plan
type: boolean
default: false
- name: PrNumber
type: number
default: 0

trigger:
paths:
include:
- specification/**

pr: none

stages:
- stage: ReleasePlanCreation
displayName: "Stage : Release Plan Creation"
jobs:
- job: ReleasePlan
displayName: Create or Find Release Plan
pool:
name: $(LINUXPOOL)
vmImage: $(LINUXVMIMAGE)

variables:
- template: /eng/pipelines/templates/variables/globals.yml
- template: /eng/pipelines/templates/variables/image.yml

steps:
- checkout: self
fetchDepth: 2

- template: /eng/common/pipelines/templates/steps/login-to-github.yml

- template: /eng/pipelines/templates/steps/npm-install.yml

- template: /eng/common/pipelines/templates/steps/install-azsdk-cli.yml

- task: AzureCLI@2
displayName: "Get Release Plan details"
condition: succeeded()
continueOnError: true
env:
GH_TOKEN: $(GH_TOKEN)
inputs:
azureSubscription: opensource-api-connection
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
$ErrorActionPreference = "Stop"

$prNumber = ${{ parameters.PrNumber }}
$outputPath = "$(Build.ArtifactStagingDirectory)/release-plan/release-plan.json"

$nodeArgs = @(
"--repo", "$(Build.Repository.Name)",
"--workspace", "$(Build.SourcesDirectory)",
"--azsdk-path", "$(AZSDK)",
"--test-release-plan", "${{ parameters['Test-release-Plan'] }}",
"--output-file", $outputPath
)

if ($prNumber -gt 0) {
Write-Host "Using PR number: $prNumber"
$nodeArgs += @("--pr-number", $prNumber)
} else {
$commitSha = "$(Build.SourceVersion)"
if ([string]::IsNullOrWhiteSpace($commitSha)) {
Write-Host "Build.SourceVersion is empty. Falling back to repo HEAD commit."
$commitSha = (git -C "$(Build.SourcesDirectory)" rev-parse HEAD).Trim()
}

if ([string]::IsNullOrWhiteSpace($commitSha)) {
Write-Host "##vso[task.logissue type=error]Unable to determine commit SHA from Build.SourceVersion or repository HEAD."
exit 1
}

Write-Host "Using commit SHA: $commitSha"
$nodeArgs += @("--commit-sha", $commitSha)
}

npm exec --no -- release-plan @nodeArgs

- pwsh: |
$outputPath = "$(Build.ArtifactStagingDirectory)/release-plan/release-plan.json"
if (!(Test-Path $outputPath)) {
New-Item -ItemType Directory -Force -Path (Split-Path $outputPath -Parent) | Out-Null
'{"outcome":"not_found","releasePlan":null,"details":{}}' | Out-File -FilePath $outputPath -Encoding utf8
}
displayName: "Ensure release plan artifact file exists"
condition: succeededOrFailed()

- task: PublishPipelineArtifact@1
displayName: "Publish release plan details artifact"
condition: succeededOrFailed()
inputs:
targetPath: "$(Build.ArtifactStagingDirectory)/release-plan"
artifactName: "release-plan-details"
publishLocation: "pipeline"
1 change: 1 addition & 0 deletions eng/tools/package.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"name": "azure-rest-api-specs-eng-tools",
"private": true,
Expand All @@ -5,6 +5,7 @@
"devDependencies": {
"@azure-tools/lint-diff": "file:lint-diff",
"@azure-tools/oav-runner": "file:oav-runner",
"@azure-tools/release-plan": "file:release-plan",
"@azure-tools/sdk-suppressions": "file:sdk-suppressions",
"@azure-tools/openapi-diff-runner": "file:openapi-diff-runner",
"@azure-tools/spec-gen-sdk-runner": "file:spec-gen-sdk-runner",
Expand Down
5 changes: 5 additions & 0 deletions eng/tools/release-plan/cmd/release-plan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { main } from "../src/index.ts";

await main();
7 changes: 7 additions & 0 deletions eng/tools/release-plan/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// @ts-check

import { defineBaseConfig } from "../eslint.base.config.js";

export default defineBaseConfig({
tsconfigRootDir: import.meta.dirname,
});
38 changes: 38 additions & 0 deletions eng/tools/release-plan/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@azure-tools/release-plan",
"private": true,
"type": "module",
"main": "src/index.ts",
"bin": {
"release-plan": "cmd/release-plan.js"
},
"dependencies": {
"@octokit/rest": "^22.0.0",
"@azure-tools/specs-shared": "file:../../../.github/shared"
},
"devDependencies": {
"@eslint/js": "^10.0.0",
"@types/node": "^20.0.0",
"@vitest/coverage-v8": "^4.1.0",
"cross-env": "^10.1.0",
"eslint": "^10.0.0",
"prettier": "3.8.3",
"prettier-plugin-organize-imports": "^4.2.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vitest": "^4.1.0"
},
"scripts": {
"build": "tsc --noEmit",
"check": "npm run build && npm run lint && npm run format:check && npm run test:ci",
"format": "prettier . --ignore-path ../.prettierignore --write",
"format:check": "prettier . --ignore-path ../.prettierignore --check",
"format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug",
"lint": "cross-env DEBUG=eslint:eslint,eslint:linter eslint",
"test": "vitest",
"test:ci": "vitest run --coverage --reporter=verbose"
},
"engines": {
"node": ">=24.14.1"
}
}
Loading