Skip to content

Commit 491ca87

Browse files
Automate release plan create (#44137)
* ADO pipeline to automate creating release plan
1 parent 8329aa9 commit 491ca87

20 files changed

Lines changed: 2453 additions & 0 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: release-plan - Test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
paths:
9+
- package-lock.json
10+
- package.json
11+
- tsconfig.json
12+
- .github/workflows/_reusable-eng-tools-test.yaml
13+
- .github/workflows/release-plan-tests.yaml
14+
- eng/tools/package.json
15+
- eng/tools/tsconfig.json
16+
- eng/tools/release-plan/**
17+
workflow_dispatch:
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
release-plan:
24+
uses: ./.github/workflows/_reusable-eng-tools-test.yaml
25+
with:
26+
package: release-plan
27+
lint: true
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Release Plan Auto-Generation
2+
3+
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.
4+
5+
## Overview
6+
7+
The release-plan pipeline is a post-merge automation for specification updates.
8+
9+
High-level lifecycle:
10+
11+
1. A spec PR merges into `main` in `Azure/azure-rest-api-specs` or `Azure/azure-rest-api-specs-pr`.
12+
2. The pipeline is triggered by changes under `specification/**`.
13+
3. Stage (`ReleasePlanCreation`) runs and resolves the commit SHA to analyze.
14+
4. The tool tries to map the commit to an associated PR and inspects TypeSpec changes.
15+
5. The tool first attempts to find an existing release plan.
16+
6. The tool creates a release plan only when creation is allowed by label policy and if a release plan does not exists.
17+
7. The pipeline always emits a JSON result artifact for downstream stages/jobs.
18+
19+
From pipeline execution perspective, the release-plan tool operates in this order:
20+
21+
1. Resolve associated PR from commit SHA (if any).
22+
2. Detect TypeSpec project and API version from changed spec files.
23+
3. Check whether the associated PR has the `new-api-version` label.
24+
4. Find an existing release plan first.
25+
5. Create a new release plan only when allowed.
26+
6. Publish JSON output as a pipeline artifact.
27+
28+
## Trigger Model (When This Runs)
29+
30+
Pipeline trigger behavior in `eng/pipelines/release-plan.yml`:
31+
32+
- `trigger.paths.include: specification/**`
33+
- `pr: none`
34+
35+
Meaning:
36+
37+
- This pipeline does not run as a PR-validation pipeline.
38+
- It runs on branch updates (CI trigger) when files under `specification/**` change.
39+
- In the common path, this happens automatically after a spec PR is merged to `main`.
40+
41+
Commit SHA source:
42+
43+
1. Primary: `$(Build.SourceVersion)`
44+
2. Fallback: repository `HEAD` via `git rev-parse HEAD` when `Build.SourceVersion` is empty
45+
46+
So after a merge to `main`, the pipeline analyzes the latest merged commit (or latest `HEAD` if fallback is needed).
47+
48+
## Inputs
49+
50+
The tool is invoked with:
51+
52+
- `--commit-sha`: commit to analyze
53+
- `--repo`: repository in `owner/repo` format
54+
- `--workspace`: local repo path
55+
- `--azsdk-path`: azsdk CLI path
56+
- `--output-file`: optional JSON output path for artifact publishing
57+
58+
## How TypeSpec Project Is Determined
59+
60+
Changed files are inspected under `specification/**`.
61+
62+
Project detection logic:
63+
64+
1. Prefer changed `tspconfig.yaml` paths.
65+
2. Otherwise, walk up from changed files to find nearest `tspconfig.yaml`.
66+
3. If no project is found: no release plan action.
67+
4. If multiple projects are found: no automatic create (manual handling required).
68+
69+
## How API Version Is Determined
70+
71+
API version is selected from detected project changes in this order:
72+
73+
1. Version-like strings found in changed file paths (`YYYY-MM-DD` or `YYYY-MM-DD-preview`).
74+
2. If not found, scan project `main.tsp` for version-like strings.
75+
3. Sort versions descending and pick the first one.
76+
77+
Sorting rules:
78+
79+
- Newer date wins.
80+
- For same date, GA wins over `-preview`.
81+
82+
Result:
83+
84+
- Selected value is used as `apiVersion` in release-plan metadata.
85+
- Preview detection (`-preview`) drives SDK release type and API release type mapping.
86+
87+
## Create vs No-Create Scenarios
88+
89+
### Scenario A: Associated PR has `new-api-version` label
90+
91+
Behavior:
92+
93+
1. Try `release-plan get --pull-request` (if PR URL exists).
94+
2. Try `release-plan get --typespec-path --api-release-type`.
95+
3. If still not found, run `release-plan create`.
96+
97+
Outcome:
98+
99+
- Existing plan is returned if found.
100+
- New plan is created only in this scenario.
101+
102+
### Scenario B: Associated PR does NOT have `new-api-version` label
103+
104+
Behavior:
105+
106+
1. Try `release-plan get --pull-request` (if PR URL exists).
107+
2. Try `release-plan get --typespec-path --api-release-type`.
108+
3. Do NOT create a new release plan.
109+
110+
Outcome:
111+
112+
- Existing plan is returned if present.
113+
- If no existing plan is found, result is `not_found`.
114+
115+
### Scenario C: No PR associated with commit SHA
116+
117+
Behavior:
118+
119+
- Project/version detection falls back to commit-level file changes.
120+
- PR URL is unavailable.
121+
- Existing-by-path lookup still runs.
122+
- Create is not possible without PR URL and is skipped unless a PR is resolved.
123+
124+
Outcome:
125+
126+
- Existing plan may still be returned by path.
127+
- If none found and create is not allowed, result is `not_found`.
128+
129+
## API Release Type and SDK Release Type
130+
131+
It uses selected API version status and repo name:
132+
133+
- Repo `azure-rest-api-specs-pr` -> API release type: `Private Preview`
134+
- Repo `azure-rest-api-specs`:
135+
- preview API version -> `Public Preview`
136+
- stable API version -> `GA`
137+
138+
SDK release type:
139+
140+
- preview API version -> `preview`
141+
- stable API version -> `stable`
142+
143+
## PR Comment Behavior
144+
145+
PR comment is posted only when:
146+
147+
- A new release plan is created, and
148+
- An associated PR number is available.
149+
150+
## Pipeline Artifact Output
151+
152+
The pipeline writes release-plan result JSON to:
153+
154+
- `$(Build.ArtifactStagingDirectory)/release-plan/release-plan.json`
155+
156+
It publishes artifact:
157+
158+
- `release-plan-details`
159+
160+
If no file is generated, pipeline creates a fallback JSON:
161+
162+
- `{"outcome":"not_found","releasePlan":null,"details":{}}`
163+
164+
## Stage Layout
165+
166+
Current pipeline stage:
167+
168+
- Stage 1: `ReleasePlanCreation`
169+
170+
This stage:
171+
172+
- Authenticates npm
173+
- Logs into GitHub
174+
- Installs azsdk CLI
175+
- Runs release-plan tool
176+
- Publishes release-plan details artifact
177+
178+
179+
## Future enhancement
180+
181+
Run SDK generation for the release plan as stage 2 using release plan details identified in stage 1.

eng/pipelines/release-plan.yml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
parameters:
2+
- name: Test-release-Plan
3+
type: boolean
4+
default: false
5+
- name: PrNumber
6+
type: number
7+
default: 0
8+
9+
trigger:
10+
paths:
11+
include:
12+
- specification/**
13+
14+
pr: none
15+
16+
stages:
17+
- stage: ReleasePlanCreation
18+
displayName: "Stage : Release Plan Creation"
19+
jobs:
20+
- job: ReleasePlan
21+
displayName: Create or Find Release Plan
22+
pool:
23+
name: $(LINUXPOOL)
24+
vmImage: $(LINUXVMIMAGE)
25+
26+
variables:
27+
- template: /eng/pipelines/templates/variables/globals.yml
28+
- template: /eng/pipelines/templates/variables/image.yml
29+
30+
steps:
31+
- checkout: self
32+
fetchDepth: 2
33+
34+
- template: /eng/common/pipelines/templates/steps/login-to-github.yml
35+
36+
- template: /eng/pipelines/templates/steps/npm-install.yml
37+
38+
- template: /eng/common/pipelines/templates/steps/install-azsdk-cli.yml
39+
40+
- task: AzureCLI@2
41+
displayName: "Get Release Plan details"
42+
condition: succeeded()
43+
continueOnError: true
44+
env:
45+
GH_TOKEN: $(GH_TOKEN)
46+
inputs:
47+
azureSubscription: opensource-api-connection
48+
scriptType: pscore
49+
scriptLocation: inlineScript
50+
inlineScript: |
51+
$ErrorActionPreference = "Stop"
52+
53+
$prNumber = ${{ parameters.PrNumber }}
54+
$outputPath = "$(Build.ArtifactStagingDirectory)/release-plan/release-plan.json"
55+
56+
$nodeArgs = @(
57+
"--repo", "$(Build.Repository.Name)",
58+
"--workspace", "$(Build.SourcesDirectory)",
59+
"--azsdk-path", "$(AZSDK)",
60+
"--test-release-plan", "${{ parameters['Test-release-Plan'] }}",
61+
"--output-file", $outputPath
62+
)
63+
64+
if ($prNumber -gt 0) {
65+
Write-Host "Using PR number: $prNumber"
66+
$nodeArgs += @("--pr-number", $prNumber)
67+
} else {
68+
$commitSha = "$(Build.SourceVersion)"
69+
if ([string]::IsNullOrWhiteSpace($commitSha)) {
70+
Write-Host "Build.SourceVersion is empty. Falling back to repo HEAD commit."
71+
$commitSha = (git -C "$(Build.SourcesDirectory)" rev-parse HEAD).Trim()
72+
}
73+
74+
if ([string]::IsNullOrWhiteSpace($commitSha)) {
75+
Write-Host "##vso[task.logissue type=error]Unable to determine commit SHA from Build.SourceVersion or repository HEAD."
76+
exit 1
77+
}
78+
79+
Write-Host "Using commit SHA: $commitSha"
80+
$nodeArgs += @("--commit-sha", $commitSha)
81+
}
82+
83+
npm exec --no -- release-plan @nodeArgs
84+
85+
- pwsh: |
86+
$outputPath = "$(Build.ArtifactStagingDirectory)/release-plan/release-plan.json"
87+
if (!(Test-Path $outputPath)) {
88+
New-Item -ItemType Directory -Force -Path (Split-Path $outputPath -Parent) | Out-Null
89+
'{"outcome":"not_found","releasePlan":null,"details":{}}' | Out-File -FilePath $outputPath -Encoding utf8
90+
}
91+
displayName: "Ensure release plan artifact file exists"
92+
condition: succeededOrFailed()
93+
94+
- task: PublishPipelineArtifact@1
95+
displayName: "Publish release plan details artifact"
96+
condition: succeededOrFailed()
97+
inputs:
98+
targetPath: "$(Build.ArtifactStagingDirectory)/release-plan"
99+
artifactName: "release-plan-details"
100+
publishLocation: "pipeline"

eng/tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"devDependencies": {
66
"@azure-tools/lint-diff": "file:lint-diff",
77
"@azure-tools/oav-runner": "file:oav-runner",
8+
"@azure-tools/release-plan": "file:release-plan",
89
"@azure-tools/sdk-suppressions": "file:sdk-suppressions",
910
"@azure-tools/openapi-diff-runner": "file:openapi-diff-runner",
1011
"@azure-tools/spec-gen-sdk-runner": "file:spec-gen-sdk-runner",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env node
2+
3+
import { main } from "../src/index.ts";
4+
5+
await main();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// @ts-check
2+
3+
import { defineBaseConfig } from "../eslint.base.config.js";
4+
5+
export default defineBaseConfig({
6+
tsconfigRootDir: import.meta.dirname,
7+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@azure-tools/release-plan",
3+
"private": true,
4+
"type": "module",
5+
"main": "src/index.ts",
6+
"bin": {
7+
"release-plan": "cmd/release-plan.js"
8+
},
9+
"dependencies": {
10+
"@octokit/rest": "^22.0.0",
11+
"@azure-tools/specs-shared": "file:../../../.github/shared"
12+
},
13+
"devDependencies": {
14+
"@eslint/js": "^10.0.0",
15+
"@types/node": "^20.0.0",
16+
"@vitest/coverage-v8": "^4.1.0",
17+
"cross-env": "^10.1.0",
18+
"eslint": "^10.0.0",
19+
"prettier": "3.8.3",
20+
"prettier-plugin-organize-imports": "^4.2.0",
21+
"typescript": "~6.0.2",
22+
"typescript-eslint": "^8.58.0",
23+
"vitest": "^4.1.0"
24+
},
25+
"scripts": {
26+
"build": "tsc --noEmit",
27+
"check": "npm run build && npm run lint && npm run format:check && npm run test:ci",
28+
"format": "prettier . --ignore-path ../.prettierignore --write",
29+
"format:check": "prettier . --ignore-path ../.prettierignore --check",
30+
"format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug",
31+
"lint": "cross-env DEBUG=eslint:eslint,eslint:linter eslint",
32+
"test": "vitest",
33+
"test:ci": "vitest run --coverage --reporter=verbose"
34+
},
35+
"engines": {
36+
"node": ">=24.14.1"
37+
}
38+
}

0 commit comments

Comments
 (0)