diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index 75d22b26e68..c80a6c72eb4 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -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: @@ -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 @@ -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 diff --git a/eng/pipelines/release-publish-nuget.yml b/eng/pipelines/release-publish-nuget.yml index 8b8944c4cc7..4a1366d15b6 100644 --- a/eng/pipelines/release-publish-nuget.yml +++ b/eng/pipelines/release-publish-nuget.yml @@ -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 @@ -43,10 +44,26 @@ parameters: type: boolean default: false + # VS Code Extension Publishing Parameters + - name: PublishVSCodeExtension + 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 @@ -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' @@ -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" + } + 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)" @@ -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" + + # 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 + } + Write-Host "PAT verified successfully" + + # 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 + 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) diff --git a/eng/pipelines/templates/BuildAndTest.yml b/eng/pipelines/templates/BuildAndTest.yml index ab5a2b4977c..90bd51b1854 100644 --- a/eng/pipelines/templates/BuildAndTest.yml +++ b/eng/pipelines/templates/BuildAndTest.yml @@ -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 @@ -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()