-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Automate release plan create #44137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automate release plan create #44137
Changes from all commits
45b93d6
a4a8d37
4495f80
c401ee2
a4f9578
901820b
14b4a65
0135495
6db7b12
9ce356d
0721e53
241e3da
7324b97
e8ebf62
a08a1fa
eb2bb69
32da2e2
75457d4
b591be5
1d6fdf7
7c14a30
7f0d823
a31b51e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| 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" |
| 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(); |
| 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, | ||
| }); |
| 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" | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.