Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions eng/pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
# Runtime parameters for VS Code extension publishing (shown in "Run pipeline" UI)
parameters:
- name: publishVSCodeExtension
displayName: 'Publish VS Code Extension to Marketplace'
type: boolean
default: false
- name: vscePublishPreRelease
displayName: 'Publish as Pre-Release'
type: boolean
default: false

trigger:
batch: true
branches:
Expand Down Expand Up @@ -51,10 +40,6 @@ variables:
- template: /eng/pipelines/common-variables.yml@self
- template: /eng/common/templates-official/variables/pool-providers.yml@self

# Variable group containing VscePublishToken for VS Code Marketplace publishing
- ${{ if eq(parameters.publishVSCodeExtension, true) }}:
- group: Aspire-Release-Secrets

- name: _BuildConfig
value: Release
- name: Build.Arcade.ArtifactsPath
Expand Down Expand Up @@ -288,9 +273,6 @@ extends:
targetRids:
- win-x64
- win-arm64
publishVSCodeExtension: ${{ parameters.publishVSCodeExtension }}
vscePublishToken: $(VscePublishToken)
vscePublishPreRelease: ${{ parameters.vscePublishPreRelease }}

# Extract the Aspire version from a generated nupkg filename so downstream
# stages (e.g. publish_winget) can use it. We use Aspire.Hosting.Docker
Expand Down
195 changes: 185 additions & 10 deletions eng/pipelines/release-publish-nuget.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Release Pipeline: Publish NuGet Packages and Promote to GA Channel
# Release Pipeline: Publish NuGet Packages, VS Code Extension, and Promote to GA Channel
#
# This pipeline automates the release process for dotnet/aspire:
# 1. Downloads signed packages from a specified build
# 2. Publishes packages to NuGet.org
# 3. Promotes the build to the Aspire GA channel via darc
# 4. Submits WinGet manifests and Homebrew cask PRs
# 3. Publishes VS Code extension to the Marketplace
# 4. Promotes the build to the Aspire GA channel via darc
# 5. Submits WinGet manifests and Homebrew cask PRs
#
# For full documentation, see: docs/release-process.md

Expand Down Expand Up @@ -43,10 +44,26 @@ parameters:
type: boolean
default: false

# VS Code Extension Publishing Parameters
- name: PublishVSCodeExtension
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.

Do we need this when we have SkipExtensionPublish, which follows the pattern in this pipeline?

displayName: 'Publish VS Code Extension to Marketplace'
type: boolean
default: false

- name: VSCodeExtensionPreRelease
displayName: 'Publish VS Code Extension as Pre-Release'
type: boolean
default: false

- name: SkipExtensionPublish
displayName: 'Skip VS Code Extension Publishing (set true if already completed)'
type: boolean
default: false

variables:
- template: /eng/pipelines/common-variables.yml@self
- template: /eng/common/templates-official/variables/pool-providers.yml@self
# Variable group containing secrets (VscePublishToken for future use)
# Variable group containing secrets (VscePublishToken, etc.)
# Note: NuGet publishing uses service connection 'NuGet.org - dotnet/aspire' instead of API key
- group: Aspire-Release-Secrets
# Variable group containing aspire-winget-bot-pat and aspire-homebrew-bot-pat for WinGet and Homebrew publishing
Expand Down Expand Up @@ -225,6 +242,10 @@ extends:
Write-Host "Dry Run: ${{ parameters.DryRun }}"
Write-Host "Skip NuGet Publish: ${{ parameters.SkipNuGetPublish }}"
Write-Host "Skip Channel Promotion: ${{ parameters.SkipChannelPromotion }}"
Write-Host "--- VS Code Extension ---"
Write-Host "Publish VS Code Extension: ${{ parameters.PublishVSCodeExtension }}"
Write-Host "Extension Pre-Release: ${{ parameters.VSCodeExtensionPreRelease }}"
Write-Host "Skip Extension Publish: ${{ parameters.SkipExtensionPublish }}"
Write-Host "==================================="
displayName: 'Validate Parameters'

Expand Down Expand Up @@ -454,17 +475,26 @@ extends:
Write-Host "║ GA Channel: ${{ parameters.GaChannelName }}"
Write-Host "║ Dry Run: ${{ parameters.DryRun }}"
Write-Host "╠═══════════════════════════════════════════════════════════════╣"
Write-Host "║ NuGet Publish: ${{ parameters.SkipNuGetPublish }}" -NoNewline
Write-Host "║ NuGet Publish: " -NoNewline
if ("${{ parameters.SkipNuGetPublish }}" -eq "true") {
Write-Host " (SKIPPED)"
Write-Host "SKIPPED"
} else {
Write-Host " (EXECUTED)"
Write-Host "EXECUTED"
}
Write-Host "║ Channel Promo: ${{ parameters.SkipChannelPromotion }}" -NoNewline
Write-Host "║ VS Code Ext: " -NoNewline
if ("${{ parameters.PublishVSCodeExtension }}" -eq "false") {
Write-Host "NOT REQUESTED"
} elseif ("${{ parameters.SkipExtensionPublish }}" -eq "true") {
Write-Host "SKIPPED"
} else {
$preRelease = if ("${{ parameters.VSCodeExtensionPreRelease }}" -eq "true") { " (pre-release)" } else { "" }
Write-Host "EXECUTED$preRelease"
}
Comment on lines +478 to +492
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The summary prints "NuGet Publish: EXECUTED" whenever SkipNuGetPublish is false, even when DryRun is true (in which case no publish occurs). Similarly, the VS Code extension summary prints EXECUTED even in dry-run mode. Consider updating the summary logic to account for DryRun and emit something like "DRY RUN" to avoid misleading release logs.

Copilot uses AI. Check for mistakes.
Write-Host "║ Channel Promo: " -NoNewline
if ("${{ parameters.SkipChannelPromotion }}" -eq "true") {
Write-Host " (SKIPPED)"
Write-Host "SKIPPED"
} else {
Write-Host " (EXECUTED)"
Write-Host "EXECUTED"
}
Write-Host "║ WinGet: (runs in parallel after this job)"
Write-Host "║ Homebrew: (runs in parallel after this job)"
Expand Down Expand Up @@ -528,3 +558,148 @@ extends:
caskArtifactPath: $(Pipeline.Workspace)/homebrew/homebrew-cask-stable
channel: stable
dryRun: ${{ parameters.DryRun }}

# ===== VS CODE EXTENSION PUBLISHING =====
- job: VSCodeExtensionJob
displayName: 'Publish VS Code Extension to Marketplace'
dependsOn: ReleaseJob
condition: |
and(
in(dependencies.ReleaseJob.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'),
eq('${{ parameters.PublishVSCodeExtension }}', 'true'),
eq('${{ parameters.SkipExtensionPublish }}', 'false')
)
timeoutInMinutes: 30
pool:
name: NetCore1ESPool-Internal
image: windows.vs2026preview.scout.amd64
os: windows
steps:
- checkout: none

- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '20.x'

- powershell: |
Write-Host "Installing vsce CLI..."
npm install -g @vscode/vsce@3.7.1
vsce --version
displayName: 'Install vsce'

- task: DownloadBuildArtifacts@0
displayName: 'Download VS Code Extension Artifact'
inputs:
buildType: specific
buildVersionToDownload: specific
project: internal
pipeline: dotnet-aspire
buildId: $(resources.pipeline.aspire-build.runID)
artifactName: aspire-vscode-extension
downloadPath: '$(Pipeline.Workspace)/extension'
checkDownloadedFiles: true

- powershell: |
$extensionPath = "$(Pipeline.Workspace)/extension/aspire-vscode-extension"
Write-Host "=== VS Code Extension Inventory ==="

$vsixFiles = Get-ChildItem -Path $extensionPath -Filter "*.vsix" -Recurse
Write-Host "Found $($vsixFiles.Count) .vsix files:"

foreach ($vsix in $vsixFiles) {
$sizeMB = [math]::Round($vsix.Length / 1MB, 2)
Write-Host " - $($vsix.Name) ($sizeMB MB)"
}

if ($vsixFiles.Count -eq 0) {
Write-Error "No .vsix files found in artifacts!"
exit 1
}

# Check for signature files
$manifestFiles = Get-ChildItem -Path $extensionPath -Filter "*.manifest" -Recurse
$signatureFiles = Get-ChildItem -Path $extensionPath -Filter "*.signature.p7s" -Recurse
Write-Host ""
Write-Host "Found $($manifestFiles.Count) manifest files"
Write-Host "Found $($signatureFiles.Count) signature files"

Write-Host "==========================="
displayName: 'List Extension Files'

- powershell: |
$extensionPath = "$(Pipeline.Workspace)/extension/aspire-vscode-extension"
$dryRun = [System.Convert]::ToBoolean("${{ parameters.DryRun }}")
$preRelease = [System.Convert]::ToBoolean("${{ parameters.VSCodeExtensionPreRelease }}")

$vsixFiles = Get-ChildItem -Path $extensionPath -Filter "*.vsix" -Recurse

Write-Host "=== Publishing VS Code Extension to Marketplace ==="

if ($dryRun) {
Write-Host "DRY RUN MODE - Extension will not actually be published"
}

foreach ($vsix in $vsixFiles) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($vsix.FullName)
$manifestPath = Join-Path $extensionPath "$baseName.manifest"
$signaturePath = Join-Path $extensionPath "$baseName.signature.p7s"
Comment on lines +645 to +646
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

$vsixFiles is collected with -Recurse, but the manifest/signature paths are constructed under $extensionPath (root). If the artifact ever contains nested folders, this will look in the wrong place and fail publishing. Consider either (1) not using -Recurse, matching the build artifact layout, or (2) deriving the manifest/signature paths from $vsix.DirectoryName (or searching adjacent files) so the trio stays together.

Suggested change
$manifestPath = Join-Path $extensionPath "$baseName.manifest"
$signaturePath = Join-Path $extensionPath "$baseName.signature.p7s"
$vsixDirectory = $vsix.DirectoryName
$manifestPath = Join-Path $vsixDirectory "$baseName.manifest"
$signaturePath = Join-Path $vsixDirectory "$baseName.signature.p7s"

Copilot uses AI. Check for mistakes.

# Verify required files exist
if (-not (Test-Path $manifestPath)) {
Write-Error "Manifest file not found: $manifestPath"
exit 1
}
if (-not (Test-Path $signaturePath)) {
Write-Error "Signature file not found: $signaturePath"
exit 1
}

# Verify signature file is valid PKCS#7 format
$bytes = [System.IO.File]::ReadAllBytes($signaturePath)
if ($bytes.Length -eq 0 -or $bytes[0] -ne 0x30) {
$firstByte = if ($bytes.Length -gt 0) { "0x{0:X2}" -f $bytes[0] } else { "empty" }
Write-Error "$baseName.signature.p7s does NOT appear to be signed. First byte: $firstByte (expected 0x30 for PKCS#7)"
exit 1
}
Write-Host "Signature file format verified"

if ($dryRun) {
Write-Host "[DRY RUN] Would publish: $($vsix.Name)"
if ($preRelease) {
Write-Host "[DRY RUN] Would publish as PRE-RELEASE"
}
continue
}

# Verify PAT is valid
Write-Host "Verifying VS Code Marketplace PAT..."
$publisher = "microsoft-aspire"
npx vsce verify-pat $publisher
if ($LASTEXITCODE -ne 0) {
Write-Error "PAT verification failed for publisher '$publisher'. Ensure the token has 'Marketplace: Manage' scope."
exit 1
Comment on lines +675 to +681
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Before running vsce verify-pat, consider explicitly validating that the VSCE_PAT environment variable is set/non-empty and failing with a clear message if it isn't. This makes failures easier to diagnose than relying on verify-pat's generic exit code.

Copilot uses AI. Check for mistakes.
}
Write-Host "PAT verified successfully"
Comment on lines +675 to +683
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

PAT verification is performed inside the loop for every .vsix. This adds redundant network calls and increases the chance of transient failures/rate limiting. Verify the PAT once before iterating through packages, then reuse that result for all publishes in the job.

Suggested change
# Verify PAT is valid
Write-Host "Verifying VS Code Marketplace PAT..."
$publisher = "microsoft-aspire"
npx vsce verify-pat $publisher
if ($LASTEXITCODE -ne 0) {
Write-Error "PAT verification failed for publisher '$publisher'. Ensure the token has 'Marketplace: Manage' scope."
exit 1
}
Write-Host "PAT verified successfully"
# Verify PAT is valid once per job (cache result in script scope)
if (-not $script:VscePatVerified) {
Write-Host "Verifying VS Code Marketplace PAT..."
$publisher = "microsoft-aspire"
npx vsce verify-pat $publisher
if ($LASTEXITCODE -ne 0) {
Write-Error "PAT verification failed for publisher '$publisher'. Ensure the token has 'Marketplace: Manage' scope."
exit 1
}
Write-Host "PAT verified successfully"
$script:VscePatVerified = $true
}

Copilot uses AI. Check for mistakes.

# Build publish arguments
$publishArgs = @("vsce", "publish", "--packagePath", $vsix.FullName, "--manifestPath", $manifestPath, "--signaturePath", $signaturePath)
if ($preRelease) {
$publishArgs += "--pre-release"
Write-Host "Publishing $($vsix.Name) to VS Code Marketplace as PRE-RELEASE..."
} else {
Write-Host "Publishing $($vsix.Name) to VS Code Marketplace..."
}

& npx @publishArgs
Comment on lines +585 to +694
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The script installs @vscode/vsce globally, but later uses npx vsce ... / npx for publishing. This can execute a different vsce than the one you pinned (or trigger an on-the-fly download), which makes the release less deterministic. Prefer invoking the installed vsce binary directly, or use npx @vscode/vsce@<pinnedVersion> consistently.

Copilot uses AI. Check for mistakes.
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to publish $($vsix.Name)"
exit 1
}
Write-Host "$($vsix.Name) published successfully"
}

Write-Host "==========================="
displayName: 'Publish Extension to Marketplace'
env:
VSCE_PAT: $(VscePublishToken)
60 changes: 0 additions & 60 deletions eng/pipelines/templates/BuildAndTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,6 @@ parameters:
- name: dockerCliVersion
type: string
default: '28.0.0'
- name: publishVSCodeExtension
type: boolean
default: false
- name: vscePublishToken
type: string
default: ''
- name: vscePublishPreRelease
type: boolean
default: false

steps:
# Internal pipeline: Build with pack+sign
Expand Down Expand Up @@ -148,57 +139,6 @@ steps:
displayName: 🟣Verify VS Code extension signature
condition: and(succeeded(), eq(variables['_SignType'], 'real'))

# Publish the signed VS Code extension to the Marketplace
# Requires vscePublishToken parameter to be set with a Personal Access Token for the VS Marketplace
- ${{ if and(eq(parameters.publishVSCodeExtension, true), ne(parameters.vscePublishToken, '')) }}:
# Verify the PAT is valid before attempting to publish
- pwsh: |
Write-Host "Verifying VS Code Marketplace PAT..."
$publisher = "microsoft-aspire"
npx vsce verify-pat $publisher
if ($LASTEXITCODE -ne 0) {
Write-Host "##[error]PAT verification failed for publisher '$publisher'. Ensure the token has 'Marketplace: Manage' scope."
exit 1
}
Write-Host "✅ PAT verified successfully for publisher '$publisher'"
displayName: 🟣Verify VS Code Marketplace PAT
condition: and(succeeded(), eq(variables['_SignType'], 'real'))
env:
VSCE_PAT: ${{ parameters.vscePublishToken }}

- pwsh: |
$vsixDir = '${{ parameters.repoArtifactsPath }}/packages/Release/vscode'
$preRelease = '${{ parameters.vscePublishPreRelease }}'
$vsixFiles = Get-ChildItem -Path "$vsixDir/*.vsix" -ErrorAction SilentlyContinue
if ($vsixFiles.Count -eq 0) {
Write-Host "##[error]No .vsix files found to publish"
exit 1
}
foreach ($vsix in $vsixFiles) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($vsix.FullName)
$manifestPath = Join-Path $vsixDir "$baseName.manifest"
$signaturePath = Join-Path $vsixDir "$baseName.signature.p7s"

$publishArgs = @("vsce", "publish", "--packagePath", $vsix.FullName, "--manifestPath", $manifestPath, "--signaturePath", $signaturePath)
if ($preRelease -eq 'True') {
$publishArgs += "--pre-release"
Write-Host "Publishing $($vsix.Name) to VS Code Marketplace as PRE-RELEASE..."
} else {
Write-Host "Publishing $($vsix.Name) to VS Code Marketplace..."
}

& npx @publishArgs
if ($LASTEXITCODE -ne 0) {
Write-Host "##[error]Failed to publish $($vsix.Name)"
exit 1
}
Write-Host "✅ $($vsix.Name) published successfully"
}
displayName: 🟣Publish VS Code extension to Marketplace
condition: and(succeeded(), eq(variables['_SignType'], 'real'))
env:
VSCE_PAT: ${{ parameters.vscePublishToken }}

- task: 1ES.PublishBuildArtifacts@1
displayName: 🟣Publish vscode extension
condition: always()
Expand Down