Document Version: 1.0
Last Updated: January 9, 2026
Author: Release Engineering Team
Status: Draft - Pending Review
- Executive Summary
- Problem Statement
- Goals and Objectives
- Scope
- Current Manual Release Process
- Proposed Automated Solution
- Detailed Requirements
- Architecture and Design
- Security Considerations
- Implementation Plan
- Testing Strategy
- Rollback Plan
- Success Metrics
- Open Questions
- Appendix
This document outlines the requirements for automating the release process for the dotnet/aspire repository. The current release process is entirely manual, requiring a release manager to perform multiple sequential tasks including NuGet package publishing, Git tagging, GitHub release creation, merge PR creation, dependency flow channel promotion, and baseline version updates.
The proposed solution splits automation between Azure DevOps (AzDO) pipelines for infrastructure-related tasks (NuGet publishing, darc channel promotion) and GitHub Actions workflows for GitHub-specific tasks (tagging, release creation, merge PR creation, baseline version update PRs).
- Manual Process Risk: The release process involves 8+ distinct manual steps, each with potential for human error.
- Single Point of Failure: Only the release manager (or a small group) knows the complete process, creating a bus factor issue.
- Time-Consuming: Each release takes significant manual effort that could be automated.
- Inconsistency: Manual processes can lead to inconsistent release artifacts (e.g., release notes formatting, tag naming).
- Security Token Management: Short-lived API tokens are generated manually for each release.
- Knowledge Transfer: Onboarding new release managers requires extensive documentation and shadowing.
- Release delays due to release manager availability
- Potential for shipping incorrect packages or missing steps
- Inefficient use of engineering time
- Difficulty scaling release operations
- Automate 90%+ of the release process while maintaining human oversight for critical decisions
- Enable any maintainer to execute a release with minimal training
- Reduce release time from hours to minutes
- Eliminate human error in repetitive tasks
- Maintain audit trail of all release actions
- Ensure idempotency - workflows/pipelines can be safely re-run on failure without duplicating work
- Document the release process publicly - create comprehensive documentation so the process is transparent and accessible
- Automating aka.ms link updates (handled separately)
- Fully unattended releases (human initiation and approval required)
- Automating release branch creation (this is a separate process)
| Task | Automation Target |
|---|---|
| Download signed packages from AzDO build | AzDO Pipeline |
| Publish packages to NuGet.org | AzDO Pipeline |
| Promote build to Aspire GA channel (darc) | AzDO Pipeline |
| Create Git tag from build commit | GitHub Actions |
| Push tag to GitHub | GitHub Actions |
| Create GitHub Release with auto-generated notes | GitHub Actions |
| Create merge PR (release → main) | GitHub Actions |
Create PR to update PackageValidationBaselineVersion |
GitHub Actions |
| Public release process documentation | docs/release-process.md |
- aka.ms link updates
- Release branch creation/management
- Pre-release validation/testing
- Communication/announcement automation
- VS Code extension marketplace publishing (if applicable)
┌─────────────────────────────────────────────────────────────────┐
│ CURRENT MANUAL PROCESS │
├─────────────────────────────────────────────────────────────────┤
│ 1. Generate short-lived NuGet API token (1 day) │
│ └─► Security: Minimize exposure window │
│ │
│ 2. Identify final build from release branch in AzDO │
│ └─► Example: Build 20260109.1 from release/13.2 │
│ │
│ 3. Download "PackageArtifacts" from selected build │
│ └─► Contains all signed .nupkg files │
│ │
│ 4. Push all packages to NuGet.org │
│ └─► Command: for /f %x in ('dir /s /b *.nupkg') │
│ do dotnet nuget push %x -k <key> -s nuget.org │
│ │
│ 5. Tag the commit with version (e.g., v13.2.0) │
│ └─► git tag v13.2.0 <commit-sha> │
│ └─► git push origin v13.2.0 │
│ │
│ 6. Create GitHub Release from tag │
│ └─► Use "Auto-generate release notes" feature │
│ │
│ 7. Create merge PR: release/13.2 → main │
│ └─► Manual review required for release-specific changes │
│ │
│ 8. Promote build to GA channel using darc │
│ └─► darc add-build-to-channel <BARBuildId> │
│ --channel 'Aspire 9.x GA' │
│ │
│ 9. Update PackageValidationBaselineVersion in PR │
│ └─► src/Directory.Build.props │
│ └─► Current value: 13.0.2 → New value: 13.2.0 │
└─────────────────────────────────────────────────────────────────┘
| Item | Location |
|---|---|
| Package Validation Baseline | src/Directory.Build.props → PackageValidationBaselineVersion |
| darc initialization | eng/common/darc-init.ps1 |
| Build pipeline | eng/pipelines/azure-pipelines.yml |
| Existing GH workflows | .github/workflows/*.yml |
┌──────────────────────────────────────────────────────────────────────────┐
│ RELEASE AUTOMATION ARCHITECTURE │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ AZURE DEVOPS PIPELINE │ │
│ │ (release-publish-nuget.yml) │ │
│ │ │ │
│ │ INPUTS: │ │
│ │ ├─ AzDO Build ID (required) │ │
│ │ ├─ Release Version (e.g., 13.2.0) (required) │ │
│ │ ├─ GA Channel Name (optional, default: "Aspire 9.x GA") │ │
│ │ ├─ Dry Run Mode (optional, default: false) │ │
│ │ ├─ Skip NuGet Publish (optional, for re-runs) │ │
│ │ └─ Skip Channel Promotion (optional, for re-runs) │ │
│ │ │ │
│ │ AUTO-EXTRACTED: │ │
│ │ └─ BAR Build ID (from build tags: "BAR ID - NNNNNN") │ │
│ │ │ │
│ │ SECRETS (from Variable Groups): │ │
│ │ ├─ NuGetApiKey (stored in secure variable group) │ │
│ │ └─ Darc: Maestro Production (Azure service connection) │ │
│ │ │ │
│ │ STEPS (all idempotent): │ │
│ │ 1. Extract BAR Build ID from build tags │ │
│ │ 2. Download PackageArtifacts from specified build │ │
│ │ 3. Validate package signatures │ │
│ │ 4. Push packages to NuGet.org (--skip-duplicate) │ │
│ │ 5. Promote build to GA channel via darc │ │
│ │ 6. Output: Commit SHA, Package list, Success/Failure status │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ GITHUB ACTIONS WORKFLOW │ │
│ │ (release-github-tasks.yml) │ │
│ │ │ │
│ │ TRIGGER: workflow_dispatch (manual) │ │
│ │ │ │
│ │ INPUTS: │ │
│ │ ├─ Release Version (e.g., 13.2.0) (required) │ │
│ │ ├─ Commit SHA to tag (required) │ │
│ │ ├─ Release Branch (e.g., release/13.2) (required) │ │
│ │ ├─ Is Prerelease (optional, default: false) │ │
│ │ ├─ Skip Tag/Release (optional, for re-runs) │ │
│ │ ├─ Skip Merge PR (optional, for re-runs) │ │
│ │ └─ Skip Baseline PR (optional, for re-runs) │ │
│ │ │ │
│ │ USES APPROVED ACTIONS ONLY: │ │
│ │ ├─ actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 │ │
│ │ ├─ dotnet/actions-create-pull-request@e8d799aa1... │ │
│ │ └─ gh CLI (pre-installed on runners) │ │
│ │ │ │
│ │ PERMISSIONS (using GITHUB_TOKEN): │ │
│ │ ├─ contents: write (for tags) │ │
│ │ └─ pull-requests: write (for PRs) │ │
│ │ │ │
│ │ JOBS (all idempotent - skip if already done): │ │
│ │ │ │
│ │ Job 1: create-tag-and-release │ │
│ │ ├─ Check if tag exists, skip if so │ │
│ │ ├─ Create annotated tag v{version} at commit │ │
│ │ ├─ Check if release exists, skip if so │ │
│ │ └─ Create GitHub Release with auto-generated notes │ │
│ │ │ │
│ │ Job 2: create-merge-pr │ │
│ │ ├─ Use dotnet/actions-create-pull-request │ │
│ │ └─ (handles existing PR gracefully) │ │
│ │ │ │
│ │ Job 3: create-baseline-version-pr │ │
│ │ ├─ Update PackageValidationBaselineVersion │ │
│ │ └─ Use dotnet/actions-create-pull-request │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ RELEASE WORKFLOW SEQUENCE │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ PHASE 1: NUGET PUBLISHING (AzDO Pipeline) │
│ ───────────────────────────────────────── │
│ │
│ Release Manager │
│ │ │
│ ├─► 1. Identify final build in AzDO (Build ID only) │
│ │ (BAR Build ID is auto-extracted from build tags) │
│ │ │
│ ├─► 2. Queue AzDO pipeline with parameters: │
│ │ • Build ID: 20260109.1 │
│ │ • Version: 13.2.0 │
│ │ • Channel: "Aspire 9.x GA" (optional, has default) │
│ │ │
│ └─► 3. Pipeline executes: │
│ ├─ Extracts BAR Build ID from build tags │
│ ├─ Downloads PackageArtifacts │
│ ├─ Validates signatures │
│ ├─ Pushes to NuGet.org (idempotent with --skip-duplicate) │
│ ├─ Promotes to GA channel (idempotent) │
│ └─ Outputs commit SHA │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ PHASE 2: GITHUB TASKS (GitHub Actions) │
│ ────────────────────────────────────── │
│ │
│ Release Manager │
│ │ │
│ ├─► 4. Trigger GitHub workflow with parameters: │
│ │ • Version: 13.2.0 │
│ │ • Commit SHA: abc123def456 │
│ │ • Release Branch: release/13.2 │
│ │ │
│ └─► 5. Workflow executes (parallel jobs): │
│ │ (All jobs are idempotent - safe to re-run) │
│ │ │
│ ├─► Job A: Create & push tag v13.2.0 │
│ │ Create GitHub Release │
│ │ (skips if already exists) │
│ │ │
│ ├─► Job B: Create merge PR (release/13.2 → main) │
│ │ (skips if PR already exists) │
│ │ │
│ └─► Job C: Create baseline version update PR │
│ (skips if PR already exists) │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ PHASE 3: MANUAL REVIEW & MERGE │
│ ────────────────────────────────────── │
│ │
│ Maintainers │
│ │ │
│ ├─► 6. Review merge PR, resolve conflicts, adjust as needed │
│ │ │
│ └─► 7. Review baseline version PR, approve and merge │
│ │
└────────────────────────────────────────────────────────────────────────┘
| Requirement | Details |
|---|---|
| ID | R1.1 |
| Name | Create release publishing pipeline |
| Priority | P0 (Critical) |
| Description | Create a new AzDO pipeline release-publish-nuget.yml in eng/pipelines/ |
1ES Pipeline Template Compliance:
The pipeline MUST extend the 1ES Pipeline Templates to be compliant with Microsoft org requirements. This is the same pattern used by the main build pipeline (eng/pipelines/azure-pipelines.yml).
resources:
repositories:
- repository: 1ESPipelineTemplates
type: git
name: 1ESPipelineTemplates/1ESPipelineTemplates
ref: refs/tags/release
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
parameters:
featureFlags:
autoEnablePREfastWithNewRuleset: false
autoEnableRoslynWithNewRuleset: false
sdl:
sourceAnalysisPool:
name: NetCore1ESPool-Internal
image: windows.vs2022preview.amd64
os: windows
stages:
# Pipeline stages defined hereThis template provides:
- SDL (Security Development Lifecycle) compliance scanning
- Proper pool configuration for internal pipelines
- Component governance integration
- Required security tooling
Pipeline Parameters:
parameters:
- name: AzdoBuildId
displayName: 'Azure DevOps Build ID'
type: string
# Required - the build containing PackageArtifacts to publish
- name: ReleaseVersion
displayName: 'Release Version (e.g., 13.2.0)'
type: string
# Required - used for logging and validation
- name: GaChannelName
displayName: 'GA Channel Name'
type: string
default: 'Aspire 9.x GA'
# Optional - defaults to current GA channel
- name: DryRun
displayName: 'Dry Run (skip actual publish)'
type: boolean
default: false
# === IDEMPOTENCY FLAGS ===
# These flags allow re-running the pipeline after partial failures
# by skipping already-completed steps
- name: SkipNuGetPublish
displayName: 'Skip NuGet Publishing (already completed)'
type: boolean
default: false
# Set to true if packages were already published and only channel promotion failed
- name: SkipChannelPromotion
displayName: 'Skip Channel Promotion (already completed)'
type: boolean
default: false
# Set to true if channel promotion was already doneAutomatic BAR Build ID Extraction:
The pipeline will automatically extract the BAR Build ID from the AzDO build tags. Each build has a tag in the format BAR ID - 293764 added during the build process. The pipeline will:
- Query the specified AzDO build for its tags
- Parse the tag matching pattern
BAR ID - (\d+) - Extract the numeric BAR Build ID
- Use this for darc channel promotion
This eliminates the need for manual BAR Build ID lookup.
# Example: Extract BAR Build ID from build tags
$buildTags = $(Build.Tags) # or via REST API
$barIdTag = $buildTags | Where-Object { $_ -match 'BAR ID - (\d+)' }
if ($barIdTag -match 'BAR ID - (\d+)') {
$barBuildId = $Matches[1]
Write-Host "Extracted BAR Build ID: $barBuildId"
}Variable Groups Required:
| Variable Group | Variables | Purpose |
|---|---|---|
Aspire-Release-Secrets (new) |
NuGetApiKey |
Long-lived NuGet.org API key |
Publish-Build-Assets (existing) |
MaestroAccessToken |
For darc operations |
| Requirement | Details |
|---|---|
| ID | R1.2 |
| Name | Download and validate packages |
| Priority | P0 (Critical) |
Steps:
- Use
DownloadPipelineArtifact@2task to downloadPackageArtifactsfrom specified build - List all
.nupkgfiles and log count - Validate that expected packages are present (configurable list or pattern)
- Verify package signatures using
dotnet nuget verify
| Requirement | Details |
|---|---|
| ID | R1.3 |
| Name | Push packages to NuGet.org |
| Priority | P0 (Critical) |
Implementation Notes:
- Use
dotnet nuget pushwith--skip-duplicateto handle retries gracefully - Implement retry logic (3 attempts with exponential backoff)
- Log each package push result
- Continue on individual package failure but report overall status
- Support dry-run mode that logs what would be published
Example Script Logic:
$packages = Get-ChildItem -Path $artifactsPath -Filter "*.nupkg" -Recurse
foreach ($package in $packages) {
$retryCount = 0
$maxRetries = 3
do {
$result = dotnet nuget push $package.FullName `
--api-key $env:NUGET_API_KEY `
--source https://api.nuget.org/v3/index.json `
--skip-duplicate
if ($LASTEXITCODE -eq 0) { break }
$retryCount++
Start-Sleep -Seconds ([Math]::Pow(2, $retryCount))
} while ($retryCount -lt $maxRetries)
}| Requirement | Details |
|---|---|
| ID | R1.4 |
| Name | Promote build to GA channel |
| Priority | P0 (Critical) |
Required Permissions and Infrastructure:
Based on the existing eng/common/core-templates/post-build/post-build.yml template, channel promotion requires:
-
Azure Service Connection:
Darc: Maestro Production- This provides authentication to the Maestro/BAR API
- Uses
AzureCLI@2task with this subscription
-
Pool:
NetCore1ESPool-Publishing-Internal(for dnceng projects)- Image:
windows.vs2019.amd64
- Image:
-
Variable Groups:
Publish-Build-Assets(provides MaestroAccessToken if needed)
-
darc Installation: Via
eng/common/darc-init.ps1
Steps:
- Use
AzureCLI@2task withazureSubscription: "Darc: Maestro Production" - Install darc using
eng/common/darc-init.ps1 - Execute promotion command:
$darc = Get-Darc & $darc add-build-to-channel ` --id $BarBuildId ` --channel '$GaChannelName' ` --ci
- Verify promotion succeeded
Idempotency:
- Running
add-build-to-channelfor a build already in the channel is a no-op - The command will succeed without error if build is already promoted
Reference Implementation: See eng/common/post-build/publish-using-darc.ps1
| Requirement | Details |
|---|---|
| ID | R2.1 |
| Name | Create GitHub release workflow |
| Priority | P0 (Critical) |
| Description | Create .github/workflows/release-github-tasks.yml |
Workflow Inputs:
on:
workflow_dispatch:
inputs:
release_version:
description: 'Release version (e.g., 13.2.0)'
required: true
type: string
commit_sha:
description: 'Commit SHA to tag'
required: true
type: string
release_branch:
description: 'Release branch (e.g., release/13.2)'
required: true
type: string
is_prerelease:
description: 'Mark as pre-release'
required: false
type: boolean
default: false
# === IDEMPOTENCY FLAGS ===
# These flags allow re-running the workflow after partial failures
# by skipping already-completed steps
skip_tag_and_release:
description: 'Skip tag and release creation (already completed)'
required: false
type: boolean
default: false
skip_merge_pr:
description: 'Skip merge PR creation (already completed)'
required: false
type: boolean
default: false
skip_baseline_pr:
description: 'Skip baseline version PR creation (already completed)'
required: false
type: boolean
default: falseRequired Permissions:
permissions:
contents: write
pull-requests: write| Requirement | Details |
|---|---|
| ID | R2.2 |
| Name | Create Git tag and GitHub Release |
| Priority | P0 (Critical) |
Implementation:
- Check if tag already exists - if so, skip tag creation (idempotent)
- Create annotated tag
v{version}at specified commit - Push tag to origin
- Check if release already exists - if so, skip release creation (idempotent)
- Use
ghCLI (pre-installed on GitHub runners) to create release with auto-generated notes - Mark as pre-release if specified
Idempotency Handling:
- Tag already exists → Log warning, skip tag creation, continue to release
- Release already exists → Log warning, skip release creation, job succeeds
- This allows safe re-runs without manual cleanup
Example:
# Check if tag exists
if git ls-remote --tags origin | grep -q "refs/tags/v$VERSION"; then
echo "Tag v$VERSION already exists, skipping tag creation"
else
git tag -a "v$VERSION" "$COMMIT_SHA" -m "Release $VERSION"
git push origin "v$VERSION"
fi
# Check if release exists
if gh release view "v$VERSION" &>/dev/null; then
echo "Release v$VERSION already exists, skipping release creation"
else
gh release create "v$VERSION" \
--title "$VERSION" \
--generate-notes \
--target "$COMMIT_SHA" \
$([[ "$IS_PRERELEASE" == "true" ]] && echo "--prerelease")
fi| Requirement | Details |
|---|---|
| ID | R2.3 |
| Name | Create merge PR from release branch to main |
| Priority | P0 (Critical) |
Implementation:
- Check if a merge PR already exists for this branch → main (idempotent)
- Use the approved
dotnet/actions-create-pull-requestaction (already vetted and used in repo) - Set appropriate title:
Merge {release_branch} to main after {version} release - Add body with context about release
- Add labels:
area-infrastructure - Do NOT auto-merge (requires human review for release-specific changes)
Approved Action Reference:
- uses: dotnet/actions-create-pull-request@e8d799aa1f8b17f324f9513832811b0a62f1e0b1
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ inputs.release_branch }}
base: main
# ... other parametersIdempotency Handling:
- If PR already exists → Log the existing PR number, skip creation, job succeeds
- The
dotnet/actions-create-pull-requestaction handles this gracefully
PR Body Template:
## Merge Release Branch to Main
This PR merges the `{release_branch}` branch back to `main` after the `{version}` release.
### ⚠️ Manual Review Required
Some changes in the release branch may be release-specific and should NOT be merged to main:
- Version pinning changes
- Release-specific configuration
Please review carefully and adjust as needed before merging.
### Checklist
- [ ] Reviewed diff for release-specific changes
- [ ] Resolved any merge conflicts
- [ ] CI passing| Requirement | Details |
|---|---|
| ID | R2.4 |
| Name | Create PR to update PackageValidationBaselineVersion |
| Priority | P1 (High) |
Implementation:
- Check if branch
update-baseline-version-{version}already exists (idempotent) - Check if PR already exists for this update (idempotent)
- Create new branch
update-baseline-version-{version} - Update
src/Directory.Build.props:- Change
PackageValidationBaselineVersionfrom current to new version
- Change
- Commit with message:
Update PackageValidationBaselineVersion to {version} - Use the approved
dotnet/actions-create-pull-requestaction to create PR tomain
Approved Action Reference:
- uses: dotnet/actions-create-pull-request@e8d799aa1f8b17f324f9513832811b0a62f1e0b1
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: update-baseline-version-${{ inputs.release_version }}
base: main
commit-message: "Update PackageValidationBaselineVersion to ${{ inputs.release_version }}"
title: "Update PackageValidationBaselineVersion to ${{ inputs.release_version }}"
# ... other parametersIdempotency Handling:
- If branch exists with same changes → Action updates existing branch/PR
- If PR already exists → Log the existing PR number, skip creation
File to Modify: src/Directory.Build.props
<PackageValidationBaselineVersion>13.2.0</PackageValidationBaselineVersion>Both the AzDO pipeline and GitHub workflow are designed to be idempotent - they can be safely re-run after partial failures without duplicating work or causing errors.
Two-Level Idempotency:
- Automatic Idempotency: Steps that automatically detect and skip already-completed work
- Manual Skip Flags: Boolean parameters to explicitly skip steps that are known to be complete
| Scenario | Automatic Handling | Manual Recovery |
|---|---|---|
| Build artifact not found | Fail with clear error message | Fix build ID and re-run |
| Package signature validation fails | Fail pipeline, log affected packages | Investigate and re-run |
| NuGet push fails (network) | Retry 3x with exponential backoff | Re-run pipeline |
| Package already on NuGet | --skip-duplicate handles gracefully |
No action needed |
| NuGet push partially succeeds | Continue with remaining, report failures | Re-run (duplicates skipped) |
| darc promotion fails | Fail pipeline with error details | Re-run with SkipNuGetPublish=true |
| Build already in channel | darc handles gracefully (no-op) | No action needed |
| Scenario | Automatic Handling | Manual Recovery |
|---|---|---|
| Tag already exists | Skip tag creation, log warning, continue | No action needed |
| Release already exists | Skip release creation, log warning | No action needed |
| PR creation fails | Fail job with API error details | Re-run with skip_tag_and_release=true |
| Merge PR already exists | dotnet/actions-create-pull-request handles |
No action needed |
| Baseline branch exists | Action updates existing | No action needed |
| Baseline PR exists | Log existing PR, continue | No action needed |
Scenario A: AzDO pipeline fails during channel promotion
Re-run pipeline with:
SkipNuGetPublish: true
SkipChannelPromotion: false
Scenario B: GitHub workflow fails during merge PR creation
Re-run workflow with:
skip_tag_and_release: true
skip_merge_pr: false
skip_baseline_pr: false
Scenario C: Need to re-run entire workflow (safe)
Re-run with all defaults - idempotent steps will skip automatically
| Requirement | Details |
|---|---|
| ID | R4.1 |
| Name | Create public release process documentation |
| Priority | P1 (High) |
| Location | docs/release-process.md |
Purpose:
- Provide a single source of truth for the release process
- Enable any maintainer to execute a release
- Increase transparency for the community
- Reduce bus factor for release operations
Documentation Structure:
# Aspire Release Process
## Overview
- What gets released (NuGet packages, GitHub release, etc.)
- Release cadence and versioning scheme
## Prerequisites
- Required permissions (AzDO, GitHub)
- Access to variable groups (who to contact)
## Release Checklist
- [ ] Pre-release verification steps
- [ ] AzDO pipeline execution
- [ ] GitHub workflow execution
- [ ] Post-release verification
- [ ] Manual follow-up tasks (aka.ms links, announcements)
## Step-by-Step Guide
### Phase 1: Publish to NuGet (AzDO Pipeline)
1. Identify the build to release
2. Navigate to the release pipeline
3. Queue with required parameters
4. Monitor and verify completion
### Phase 2: GitHub Tasks (GitHub Workflow)
1. Trigger the workflow
2. Provide required inputs
3. Verify tag, release, and PRs created
### Phase 3: Post-Release
1. Review and merge the merge PR
2. Review and merge the baseline version PR
3. Update aka.ms links (separate process)
4. Announcements (if applicable)
## Troubleshooting
- Common issues and solutions
- How to re-run after failures
- Who to contact for help
## Appendix
- Link to this PRD
- Link to pipeline/workflow definitions| Aspect | Approach |
|---|---|
| Updates | Documentation updated alongside pipeline/workflow changes |
| Review | Part of PR review for release automation changes |
| Versioning | Lives in main branch, applies to all release branches |
All automation must use only pre-approved actions and dependencies that have been vetted by the security team. This avoids introducing new dependencies that require additional security review.
| Action | Version/SHA | Usage |
|---|---|---|
actions/checkout |
@11bd71901bbe5b1630ceea73d27597364c9af683 (v4.2.2) |
Repository checkout |
dotnet/actions-create-pull-request |
@e8d799aa1f8b17f324f9513832811b0a62f1e0b1 |
PR creation |
actions/github-script |
@60a0d83039c74a4aee543508d2ffcb1c3799cdea (v7.0.1) |
GitHub API calls |
actions/setup-dotnet |
@v4 |
.NET SDK setup |
Note: The gh CLI is pre-installed on all GitHub-hosted runners and does not require a separate action.
| Task | Version | Usage |
|---|---|---|
DownloadPipelineArtifact@2 |
Built-in | Download build artifacts |
PowerShell@2 |
Built-in | Script execution |
AzureCLI@2 |
Built-in | Azure/Maestro authentication |
NuGetAuthenticate@1 |
Built-in | NuGet feed auth |
UseDotNet@2 |
Built-in | .NET SDK setup |
The pipeline leverages existing Arcade infrastructure:
eng/common/darc-init.ps1- darc installationeng/common/tools.ps1- Common tooling (Get-Darc function)eng/common/post-build/publish-using-darc.ps1- Reference implementationeng/common/core-templates/post-build/setup-maestro-vars.yml- Maestro variable setup
| Consideration | Implementation |
|---|---|
| Key Storage | Azure DevOps secure variable group (encrypted at rest) |
| Key Scope | Scoped to dotnet-aspire namespace only |
| Key Rotation | Annual rotation, documented procedure |
| Key Expiration | 1 year validity, calendar reminder for renewal |
| Access Control | Variable group restricted to release pipeline and authorized users |
| Token | Scope | Notes |
|---|---|---|
GITHUB_TOKEN |
Automatic | Used for tag, release, PR operations |
| Permissions | Minimal required | contents: write, pull-requests: write |
| No PAT Required | ✓ | Using built-in token avoids long-lived credential storage |
- All pipeline runs are logged in AzDO with full parameter visibility
- All GitHub Actions runs are logged with inputs
- Both systems provide who triggered the workflow
Since the pipeline definitions, workflow files, and documentation will be checked into the public repository, we must ensure no security-sensitive information is exposed.
| Item | Reason |
|---|---|
Variable group names (e.g., Aspire-Release-Secrets) |
Names don't expose values; access is controlled by AzDO permissions |
Service connection names (e.g., Darc: Maestro Production) |
Names don't expose credentials; connections are managed by AzDO |
Pool names (e.g., NetCore1ESPool-Publishing-Internal) |
Infrastructure names are not sensitive |
Channel names (e.g., Aspire 9.x GA) |
Public information about release channels |
| NuGet.org source URL | Public endpoint |
| Build ID formats and tag patterns | Operational information, not credentials |
| Workflow/pipeline parameter names | Configuration, not secrets |
| GitHub Actions used (with SHAs) | Public actions, pinned for security |
| AzDO task names and versions | Built-in Microsoft tasks |
| File paths in the repository | Public repository structure |
| Process steps and procedures | Transparency helps community understanding |
| Item | Where Stored | Access Control |
|---|---|---|
| NuGet API Key | AzDO Variable Group (encrypted) | Pipeline + authorized users only |
| Maestro/BAR credentials | Azure Service Connection | Managed by AzDO, accessed via AzureCLI@2 |
GITHUB_TOKEN |
GitHub-provided at runtime | Automatic, scoped to workflow run |
- No API keys, tokens, or passwords in pipeline/workflow YAML
- No connection strings or endpoints that aren't public
- No internal URLs that shouldn't be exposed
- Variable group names reference only (values are secret)
- Service connection names reference only (credentials managed by AzDO)
- Documentation describes process, not credentials
- All GitHub Actions pinned to SHA (prevents supply chain attacks)
-
Execution requires permissions: Even with full knowledge of the process, only authorized users can:
- Trigger the AzDO pipeline (requires AzDO project permissions)
- Trigger the GitHub workflow (requires repo write access)
- Access the variable groups (requires explicit AzDO grant)
- Use the service connections (requires explicit AzDO grant)
-
Secrets are never in code: All sensitive values are stored in:
- AzDO secure variable groups (encrypted at rest)
- Azure service connections (managed credentials)
- GitHub's runtime token injection
-
Transparency benefits outweigh risks:
- Community can understand and contribute to the process
- Other .NET repos can learn from/adopt similar patterns
- Reduces bus factor by making process discoverable
This section provides a holistic threat model for the release automation system, identifying potential threats and their mitigations.
┌─────────────────────────────────────────────────────────────────────────────┐
│ RELEASE AUTOMATION ATTACK SURFACE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ AZURE DEVOPS PIPELINE │ │
│ │ │ │
│ │ Access Model: PRIVATE (Internal AzDO project) │ │
│ │ ├─ Trigger: Only authenticated users with pipeline permissions │ │
│ │ ├─ Logs: Visible only to project members │ │
│ │ └─ Secrets: Encrypted variable groups, service connections │ │
│ │ │ │
│ │ Assets Protected: │ │
│ │ ├─ NuGet API Key (publishes to nuget.org) │ │
│ │ ├─ Maestro/BAR credentials (darc channel promotion) │ │
│ │ └─ Signed package artifacts │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ GITHUB ACTIONS WORKFLOW │ │
│ │ │ │
│ │ Access Model: PUBLIC repo, RESTRICTED trigger │ │
│ │ ├─ Trigger: workflow_dispatch requires write access │ │
│ │ ├─ Logs: PUBLIC (anyone can view workflow run logs) │ │
│ │ └─ Secrets: GITHUB_TOKEN only (no custom secrets) │ │
│ │ │ │
│ │ Assets Protected: │ │
│ │ ├─ Git tags (version history integrity) │ │
│ │ ├─ GitHub Releases (official release artifacts) │ │
│ │ └─ Pull requests (code changes to main branch) │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| ID | Threat | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| T1.1 | Malicious insider publishes tampered packages | Low | Critical | • Only signed packages from official build can be published • Pipeline downloads from specific build ID, not arbitrary source • Audit trail of who triggered pipeline and with what parameters • Package signatures verified via dotnet nuget verify before push |
| T1.2 | Compromised NuGet API key | Low | Critical | • Key stored in encrypted AzDO variable group • Key scoped to dotnet-aspire namespace only (cannot publish other packages)• Annual key rotation • Key access limited to pipeline and specific users • --skip-duplicate prevents republishing existing versions |
| T1.3 | Supply chain attack on AzDO tasks | Very Low | High | • Only built-in Microsoft AzDO tasks used • 1ES Pipeline Templates provide SDL compliance • No third-party marketplace tasks |
| T1.4 | Unauthorized pipeline trigger | Very Low | High | • AzDO project is internal (requires Microsoft auth) • Pipeline trigger requires explicit permission grant • All runs logged with triggering user identity |
| T1.5 | Build artifact tampering | Very Low | Critical | • Artifacts downloaded from AzDO using authenticated API • Package signatures verified before push (fails if tampered) • Build ID explicitly specified (no "latest" ambiguity) |
| T1.6 | Promotion to wrong channel | Low | Medium | • Channel name is a parameter with sensible default • darc promotion is idempotent (re-running is safe) • Channel list is public, mistakes are visible and reversible |
| T1.7 | Secrets leaked in logs | Low | Critical | • AzDO automatically masks variable group secrets in logs • No Write-Host of sensitive values• Logs only visible to project members |
| ID | Threat | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| T2.1 | Unauthorized workflow trigger | Very Low | Medium | • workflow_dispatch requires write permission to repo• Additional check: workflow verifies actor has admin or maintain permission• Only org members/collaborators have write access • Trigger logged with user identity in workflow run |
| T2.2 | Tag pointing to malicious commit | Low | High | • Commit SHA is explicit input (not "latest") • Commit must exist in repo (can't reference external code) • Tag creation is visible in repo history • GitHub Release points to specific SHA |
| T2.3 | Secrets leaked in public logs | Medium | Low | • No custom secrets used - only GITHUB_TOKEN• GITHUB_TOKEN is automatically masked• Workflow inputs (version, SHA, branch) are not sensitive • No API keys or credentials in GitHub workflow |
| T2.4 | Malicious PR modifies workflow then triggers it | Very Low | Medium | • workflow_dispatch runs from default branch only• PR changes to workflow don't affect dispatch runs until merged • Workflow file changes require PR review and approval |
| T2.5 | GITHUB_TOKEN abuse | Low | Medium | • Token scoped to minimal permissions (contents: write, pull-requests: write)• Token expires when workflow completes • Cannot push to protected branches without approval • Cannot access other repositories |
| T2.6 | Creating release for wrong version | Low | Low | • Version format validated (SemVer regex) • Release creation is idempotent (can't duplicate) • Releases are public and visible for review • Can delete and recreate if needed |
| T2.7 | PR to main with malicious code | Low | Medium | • PRs require review before merge (not auto-merged) • Branch protection rules enforced • CI must pass before merge • Human approval required |
| T2.8 | Information disclosure via logs | Medium | Low | • Only non-sensitive info logged (version, branch, SHA) • No internal URLs, tokens, or keys exposed • Process documentation is intentionally public |
| ID | Threat | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| T3.1 | Mismatch between AzDO and GitHub operations | Medium | Low | • Both systems use same version parameter • Commit SHA links the two workflows • Idempotent design allows re-running to fix mismatches |
| T3.2 | Race condition in parallel execution | Low | Low | • GitHub workflow has concurrency group (only one runs at a time)• AzDO pipeline should be manually sequenced • Operations are idempotent |
| T3.3 | Social engineering to trigger release | Low | High | • Requires AzDO access (Microsoft auth) for NuGet publishing • Requires GitHub write access for tagging • Two separate systems = two separate compromise needed |
Public Information (visible to anyone):
- Workflow YAML file contents (process steps, action versions)
- Workflow run logs (inputs, step outputs, timing)
- Created tags and releases
- Created PRs and their contents
- Documentation of the release process
Private Information (requires authentication):
- AzDO pipeline definition (internal project)
- AzDO pipeline logs and parameters
- NuGet API key value
- Maestro/BAR credentials
- Who triggered AzDO pipeline
Security Implication: An attacker with knowledge of the process but without credentials cannot:
- Trigger either workflow (requires auth)
- Access the NuGet API key
- Publish packages to NuGet.org
- Promote builds in Maestro/BAR
Since GitHub Actions logs are public, the workflow is designed to avoid logging sensitive information:
# ✅ SAFE to log (non-sensitive):
echo "Release Version: ${{ inputs.release_version }}"
echo "Commit SHA: ${{ inputs.commit_sha }}"
echo "Release Branch: ${{ inputs.release_branch }}"
echo "Tag created: v$VERSION"
echo "PR created: #123"
# ❌ NEVER logged (but doesn't exist in GH workflow anyway):
# - API keys
# - Tokens (GITHUB_TOKEN is auto-masked)
# - Internal URLs
# - Credentials┌─────────────────────────────────────────────────────────────────┐
│ workflow_dispatch TRIGGER SECURITY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Who can trigger (GitHub level)? │
│ ├─ ✅ Org members with write access │
│ ├─ ✅ Outside collaborators with write access │
│ ├─ ❌ Read-only collaborators │
│ ├─ ❌ Public users (no access) │
│ └─ ❌ Forks (cannot trigger on upstream) │
│ │
│ Additional authorization check (implemented in workflow): │
│ └─ First job checks github.actor has 'admin' or 'maintain' │
│ permission via GitHub API - fails fast if not authorized │
│ │
│ What workflow code runs? │
│ └─ Workflow YAML always read from DEFAULT BRANCH (main) │
│ ├─ PR changes to workflow don't affect dispatch │
│ └─ Must be merged to main before taking effect │
│ NOTE: You CAN specify a different ref to run against, │
│ but the workflow DEFINITION is always from main │
│ │
│ Fork behavior: │
│ ├─ Cannot trigger upstream repo's workflow from a fork │
│ ├─ Fork has no access to upstream's GITHUB_TOKEN │
│ └─ Fork's workflow only affects the fork itself │
│ │
│ What's logged publicly? │
│ ├─ Workflow inputs (version, SHA, branch) │
│ ├─ Step outputs and timing │
│ ├─ Who triggered (GitHub username) │
│ ├─ Authorization check result │
│ └─ Success/failure status │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ NUGET PUBLISHING DEFENSE IN DEPTH │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Access Control │
│ └─ Only authorized users can trigger AzDO pipeline │
│ │
│ Layer 2: Source Verification │
│ └─ Packages come from specific build ID (not "latest") │
│ │
│ Layer 3: Package Integrity │
│ └─ `dotnet nuget verify` validates signatures before push │
│ (pipeline fails if any package fails verification) │
│ │
│ Layer 4: Key Scoping │
│ └─ API key can only publish to dotnet-aspire namespace │
│ │
│ Layer 5: Idempotency │
│ └─ --skip-duplicate prevents version republishing │
│ │
│ Layer 6: Audit Trail │
│ └─ All publishes logged in AzDO and NuGet.org │
│ │
│ Layer 7: Reversibility │
│ └─ Packages can be unlisted (not deleted) if compromised │
│ │
└─────────────────────────────────────────────────────────────────┘
Background: Git supports cryptographically signed tags using GPG or SSH keys. Signed tags display a "Verified" badge on GitHub and provide cryptographic proof of who created the tag. This is a recognized security best practice for release integrity.
Our Decision: We opted to use GitHub Tag Protection Rules instead of signed tags.
Rationale:
| Consideration | Signed Tags | Tag Protection Rules |
|---|---|---|
| Complexity | Requires GPG/SSH key management, key rotation, secure key storage as GitHub secrets | Configuration in repository settings only |
| Automation | Workflow needs access to signing key, adds secret management burden | No additional secrets required |
| Protection | Proves who created tag | Prevents unauthorized tag creation, deletion, or modification |
| Scope | Per-tag verification | Pattern-based rules (e.g., v*) |
| Recovery | If key is compromised, past signatures may be questioned | Protection is administrative, no cryptographic key to compromise |
Implemented Mitigation:
GitHub Tag Protection Rules are configured for the v* pattern with the following restrictions:
- Only users with admin or maintain permission can create matching tags
- Tags cannot be deleted once created
- Tags cannot be force-pushed (modified to point to a different commit)
Configuration Location: Repository Settings → Tags → Tag protection rules
Rule: v*
Restrictions:
├─ Only admins/maintainers can create
├─ Prevent tag deletion
└─ Prevent force-push to tags
Threat Coverage:
| Threat | Signed Tags | Tag Protection Rules |
|---|---|---|
| Unauthorized tag creation | ✅ Unsigned tags visible | ✅ Blocked at creation |
| Tag tampering (point to different commit) | ✅ Signature would be invalid | ✅ Force-push blocked |
| Tag deletion | ❌ Still possible | ✅ Blocked |
| Retroactive verification | ✅ Can verify signature | ❌ No cryptographic proof |
Acceptable Trade-off: We accept that tag protection rules do not provide cryptographic proof of authorship (which signed tags would provide). However, combined with GitHub's audit log of who created each tag and the workflow authorization check requiring admin/maintain permission, we have sufficient accountability for our threat model. The operational simplicity of not managing signing keys outweighs the additional assurance signed tags would provide.
| Risk Area | Residual Risk | Justification |
|---|---|---|
| Unauthorized package publishing | Low | Multiple auth layers, signed packages, scoped keys |
| Credential exposure | Low | No secrets in GH workflow, AzDO logs are private |
| Information disclosure | Very Low | Only non-sensitive process info is public |
| Tag/release tampering | Low | Requires write access, visible audit trail |
| Supply chain attack | Very Low | Pinned SHAs, built-in tasks only |
| Insider threat | Low | Audit trails, limited access, reversible actions |
If a security incident is suspected:
-
Immediate Actions:
- Revoke NuGet API key (generate new one)
- Review AzDO pipeline audit logs
- Review GitHub workflow run history
- Check NuGet.org for unexpected publishes
-
For Compromised Packages:
- Unlist affected package versions on NuGet.org
- Publish advisory/announcement
- Release patched versions with incremented version numbers
-
For Compromised Tags/Releases:
- Delete malicious GitHub release
- Delete malicious tag:
git push --delete origin v<version> - Recreate with correct commit
-
For Compromised PRs:
- Close malicious PR without merging
- Review branch protection settings
- Audit recent merges to main
| Task | Owner | Est. Hours |
|---|---|---|
Create AzDO variable group Aspire-Release-Secrets |
Release Manager | 1 |
| Generate and store long-lived NuGet API key | Release Manager | 1 |
Create release-publish-nuget.yml pipeline skeleton |
Dev | 4 |
| Implement package download and listing | Dev | 2 |
| Task | Owner | Est. Hours |
|---|---|---|
| Implement NuGet push with retry logic | Dev | 4 |
| Implement darc channel promotion | Dev | 3 |
| Add dry-run mode | Dev | 2 |
| Testing with test packages/channel | Dev | 4 |
| Task | Owner | Est. Hours |
|---|---|---|
Create release-github-tasks.yml workflow |
Dev | 4 |
| Implement tag and release creation | Dev | 3 |
| Implement merge PR creation | Dev | 3 |
| Implement baseline version PR | Dev | 3 |
| Task | Owner | Est. Hours |
|---|---|---|
Create docs/release-process.md documentation |
Dev | 4 |
| End-to-end testing with real release | Dev + RM | 8 |
| Security review of public artifacts | Dev + Security | 2 |
| Knowledge transfer to additional maintainers | RM | 4 |
- Dry Run Testing: Use dry-run mode to validate all steps without publishing
- Test Package Testing: Create test packages with
-testsuffix to validate push logic - Channel Testing: Test with a non-production channel first
- Fork Testing: Test workflow in a fork before merging
- Draft Release: Test release creation in draft mode first
- Branch Protection: Ensure PR creation respects branch protection rules
- Full Release Simulation: Execute complete process with test artifacts
- Failure Recovery: Test error scenarios and recovery procedures
- Unlist packages via NuGet.org UI (cannot delete)
- Ship corrected packages with incremented patch version
- Update release notes to reflect the issue
- Delete the release via GitHub UI or API
- Delete the tag:
git push --delete origin v{version} - Re-run workflow with correct parameters
- Close the PR without merging
- Delete the branch if applicable
- Re-run workflow or create manually
| Metric | Target | Measurement |
|---|---|---|
| Release time | < 30 minutes | From pipeline start to release completion |
| Error rate | < 5% | Releases requiring manual intervention |
| Adoption | 100% | All releases using automation within 2 releases |
| Documentation coverage | 100% | All steps documented in runbook |
-
Q: Should we support preview/RC releases differently?
- Proposed: Use
is_prereleaseflag for GitHub releases; same pipeline for NuGet - Status: Open
- Proposed: Use
-
Q: Should the baseline version PR target
mainor the next release branch?- Proposed: Target
mainsince baseline validation is for breaking changes detection - Status: Open
- Proposed: Target
-
Q: Do we need approval gates in the AzDO pipeline?
- Proposed: No automated approval gates, but consider adding manual approval stage
- Status: Open
-
Q: Should we automatically assign reviewers to the merge PR?
- Proposed: Assign to
dotnet/aspire-maintainersteam if available - Status: Open
- Proposed: Assign to
-
Q: What happens if packages are already published to NuGet.org?- ✅ Resolved: Use
--skip-duplicateflag to handle gracefully (idempotent)
- ✅ Resolved: Use
-
Q: Should we validate the release version format?
- Proposed: Yes, validate SemVer format (major.minor.patch with optional prerelease)
- Status: Open
-
Q: Which GA channel name should be used?- ✅ Resolved: Default to
Aspire 9.x GAbut make it a configurable parameter
- ✅ Resolved: Default to
-
Q: How do we get the BAR Build ID?- ✅ Resolved: Automatically extract from AzDO build tags (format:
BAR ID - NNNNNN)
- ✅ Resolved: Automatically extract from AzDO build tags (format:
-
Q: What approved actions can we use for PR creation?- ✅ Resolved: Use
dotnet/actions-create-pull-request@e8d799aa1f8b17f324f9513832811b0a62f1e0b1(already vetted)
- ✅ Resolved: Use
-
Q: What permissions are needed for darc channel promotion?- ✅ Resolved: Requires
AzureCLI@2withazureSubscription: "Darc: Maestro Production"service connection
- ✅ Resolved: Requires
| Purpose | Path |
|---|---|
| Release Process Documentation (new) | docs/release-process.md |
| AzDO Pipeline (new) | eng/pipelines/release-publish-nuget.yml |
| GitHub Workflow (new) | .github/workflows/release-github-tasks.yml |
| Baseline Version | src/Directory.Build.props |
| darc init script | eng/common/darc-init.ps1 |
| Existing main pipeline | eng/pipelines/azure-pipelines.yml |
| Common variables | eng/pipelines/common-variables.yml |
| This PRD | docs/specs/release-automation-prd.md |
| Group | Contains | Usage |
|---|---|---|
Publish-Build-Assets |
MaestroAccessToken |
darc operations |
DotNet-HelixApi-Access |
HelixApiAccessToken |
Test infrastructure |
SDL_Settings |
Security settings | Build security |
AzDO Pipeline - Normal Run:
Pipeline: release-publish-nuget
Parameters:
AzdoBuildId: 20260109.1
ReleaseVersion: 13.2.0
GaChannelName: Aspire 9.x GA # Optional, this is the default
DryRun: false
SkipNuGetPublish: false
SkipChannelPromotion: false
AzDO Pipeline - Re-run after NuGet success but channel promotion failure:
Pipeline: release-publish-nuget
Parameters:
AzdoBuildId: 20260109.1
ReleaseVersion: 13.2.0
SkipNuGetPublish: true # Skip - already completed
SkipChannelPromotion: false # Retry this step
GitHub Actions - Normal Run:
Workflow: release-github-tasks
Inputs:
release_version: 13.2.0
commit_sha: abc123def456789
release_branch: release/13.2
is_prerelease: false
skip_tag_and_release: false
skip_merge_pr: false
skip_baseline_pr: false
GitHub Actions - Re-run after tag/release success but PR creation failure:
Workflow: release-github-tasks
Inputs:
release_version: 13.2.0
commit_sha: abc123def456789
release_branch: release/13.2
skip_tag_and_release: true # Skip - already completed
skip_merge_pr: false # Retry this step
skip_baseline_pr: false # Retry this step
- Arcade Publishing Documentation
- darc Documentation
- NuGet CLI Reference
- GitHub CLI Release Documentation
| Role | Name | Date | Status |
|---|---|---|---|
| Release Manager | Pending | ||
| Engineering Lead | Pending | ||
| Security Review | Pending |
This document is a living specification and will be updated as requirements evolve.