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
38 changes: 33 additions & 5 deletions .pipelines/CosmosDB-Shell-Official.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ extends:
OneES_SbomNugetSDLPath: out\nupkg
# https://aka.ms/obpipelines/sdl
ob_sdl_binskim_enabled: true
ob_sdl_binskim_scanOutputDirectoryOnly: true
# Scan the whole checkout, not just ob_outputDirectory: the Windows exes
# BinSkim needs are staged outside out\ (see "Stage Windows executables
# for BinSkim" below) so the published cosmos_shell_all artifact only
# ever contains zip/nupkg, never loose exes.
ob_sdl_binskim_scanOutputDirectoryOnly: false
ob_sdl_binskim_break: true # always break the build on binskim issues, even if TSA enabled. You can disable it by setting to 'false'
ob_sdl_roslyn_break: true
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: # conditionally enable symbolsPublishing for main branch only
Expand Down Expand Up @@ -524,6 +528,29 @@ extends:
# (SBOM + signed build.manifest) on its own afterwards.
# This MUST run before the NuGet push, because the 1ES Pipeline Template
# injects a full-tree CodeSign scan after the last user step group.
#
# The Windows win-x64/win-arm64 exes are staged to a location outside
# out\ first (below) so BinSkim (scanning the whole checkout, see
# ob_sdl_binskim_scanOutputDirectoryOnly above) still has loose PE files
# to scan, without those exes ending up duplicated in the artifact
# alongside the already-zipped copies in out\zip.
- task: PowerShell@2
displayName: "Stage Windows executables for BinSkim (outside published artifact)"
condition: always()
inputs:
targetType: inline
script: |
$outDir = "$(Build.SourcesDirectory)\out"
$scanDir = "$(Build.SourcesDirectory)\_binskim-scan"
foreach ($rid in @('win-x64', 'win-arm64')) {
$source = Join-Path $outDir $rid
if (Test-Path $source) {
$target = Join-Path $scanDir $rid
New-Item -ItemType Directory -Path $target -Force | Out-Null
Copy-Item -Path (Join-Path $source '*') -Destination $target -Recurse -Force
}
}

- task: PowerShell@2
displayName: "Clean non-shipping build outputs before SDL"
condition: always()
Expand Down Expand Up @@ -554,10 +581,11 @@ extends:
# Whitelist approach: keep only the dirs we explicitly want shipped
# in the cosmos_shell_all artifact. Anything else (per-RID publish
# folders, nupkg-payload sign-staging, future intermediates) is noise.
# win-x64 and win-arm64 are kept so SDL/BinSkim has loose PE files
# to scan (it does not descend into the zip/nupkg archives, and
# ob_sdl_binskim_scanOutputDirectoryOnly limits it to out\).
$keep = @('zip', 'nupkg', '_manifest', 'win-x64', 'win-arm64')
# win-x64/win-arm64 are intentionally NOT kept — those exes are
# already zipped into out\zip and were staged to _binskim-scan\
# above for scanning, so keeping a third loose copy here would
# just duplicate them in the published artifact.
$keep = @('zip', 'nupkg', '_manifest')
Get-ChildItem -Path $outDir -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $keep -notcontains $_.Name } | ForEach-Object {
Write-Host "Trimming $($_.FullName) from out\"
Remove-Item -Recurse -Force $_.FullName
Expand Down
212 changes: 212 additions & 0 deletions .pipelines/CosmosDB-Shell-Release-NuGet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#################################################################################
# CosmosDBShell - Release: publish packages to nuget.org #
# #
# This pipeline does NOT build or sign anything. It consumes the already-built #
# and Microsoft-signed NuGet packages produced by the Official build pipeline #
# (.pipelines/CosmosDB-Shell-Official.yml) and pushes them to the public #
# nuget.org feed, gated behind a manual approval on the #
# 'nuget-org-release' Azure DevOps Environment. #
# #
# One-time setup required before the first live run (see notes at bottom): #
# 1. nuget.org: create/own the 'CosmosDBShell' package ID and the #
# 'CosmosDBShell.FrameworkDependent' package ID under the 'cosmosdbshell' #
# organization and generate a scoped API key covering both. #
# 2. Azure DevOps: create a NuGet service connection named #
# '$(NuGetServiceConnection)' pointing at $(NuGetOrgSource) with that key. #
# 3. Azure DevOps: create an Environment named 'nuget-org-release' and add #
# the required approvers as an approval check. #
# 4. Register this YAML as a new pipeline named 'CosmosDB-Shell-Release-NuGet'#
# and confirm the 'source:' below matches the Official build's pipeline #
# name in your ADO project. #
#################################################################################

# Release is initiated manually only. It never runs on commits or PRs.
trigger: none
pr: none

parameters:
- name: "dryRun"
displayName: "Dry run (validate the package set but skip the nuget.org push)"
type: boolean
default: true

- name: "debug"
displayName: "Enable debug output"
type: boolean
default: false

variables:
system.debug: ${{ parameters.debug }}

# Name of the Azure DevOps NuGet service connection that stores the
# nuget.org API key. Create this under Project settings > Service connections.
NuGetServiceConnection: "nuget-org-cosmosdbshell"

# Public nuget.org v3 push source.
NuGetOrgSource: "https://api.nuget.org/v3/index.json"

# Folder (inside the downloaded Official-build artifact) that holds the signed
# *.nupkg files. The Official pipeline publishes out\ as 'cosmos_shell_all',
# and packs the packages into out\nupkg.
PackagesArtifactName: "cosmos_shell_all"
PackagesSubfolder: "nupkg"

resources:
pipelines:
# Pull the signed packages from the most recent successful Official build.
# 'source' must match the *name* of the Official pipeline in Azure DevOps.
- pipeline: officialBuild
source: CosmosDB-Shell-Official
trigger: none # never auto-trigger a release when a build completes

repositories:
- repository: templates
type: git
name: OneBranch.Pipelines/GovernedTemplates
ref: refs/heads/main

extends:
template: v2/OneBranch.Official.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
parameters:
featureFlags:
WindowsHostVersion:
Version: 2022
Network: R1
cloudvault:
enabled: false
globalSdl: # https://aka.ms/obpipelines/sdl
tsa:
enabled: true
policheck:
break: true

stages:
- stage: release
displayName: "Publish to nuget.org"
jobs:
# A deployment job is used so the 'nuget-org-release' Environment's
# approval check fires before any push happens.
- deployment: push_nuget_org
displayName: "Push signed packages to nuget.org"
environment: nuget-org-release
pool:
type: windows # 1ES Official requires pool.type.
variables:
# 1ES requires an output directory even though this job only
# downloads and pushes; nothing new is produced here.
ob_outputDirectory: '$(Build.SourcesDirectory)\out'
ob_artifactBaseName: cosmos_shell_release
strategy:
runOnce:
deploy:
steps:
- task: UseDotNet@2
displayName: "Install .NET SDK"
inputs:
packageType: "sdk"
useGlobalJson: true
performMultiLevelLookup: true

# Satisfy the OneBranch/1ES template contract: ob_outputDirectory
# must exist even though this release job produces nothing new
# (it only downloads and pushes). The template uploads this
# directory as an artifact after the last user step, which can
# fail if the folder was never created.
- task: PowerShell@2
displayName: "Ensure output directory exists"
inputs:
targetType: inline
pwsh: true
script: |
New-Item -ItemType Directory -Path '$(Build.SourcesDirectory)\out' -Force | Out-Null

# Download the signed packages from the Official build.
# Lands at: $(Pipeline.Workspace)\officialBuild\<PackagesArtifactName>\<PackagesSubfolder>\*.nupkg
- download: officialBuild
artifact: $(PackagesArtifactName)
displayName: "Download signed packages from Official build"

- task: PowerShell@2
displayName: "Validate package set before publishing"
inputs:
targetType: inline
pwsh: true
script: |
$ErrorActionPreference = 'Stop'

$pkgDir = Join-Path '$(Pipeline.Workspace)' 'officialBuild\$(PackagesArtifactName)\$(PackagesSubfolder)'
if (-not (Test-Path $pkgDir)) {
throw "Package directory not found: $pkgDir. Confirm the Official build published the '$(PackagesArtifactName)' artifact with a '$(PackagesSubfolder)' folder."
}

$packages = Get-ChildItem -Path $pkgDir -Filter *.nupkg -File
if (-not $packages -or $packages.Count -eq 0) {
throw "No .nupkg files found in $pkgDir."
}

# Every RID-specific tool package must be present, plus exactly
# one non-RID 'pointer' package that ties them together.
$ridPatterns = @(
'^CosmosDBShell\.win-x64\..+\.nupkg$',
'^CosmosDBShell\.win-arm64\..+\.nupkg$',
'^CosmosDBShell\.linux-x64\..+\.nupkg$',
'^CosmosDBShell\.linux-arm64\..+\.nupkg$',
'^CosmosDBShell\.osx-x64\..+\.nupkg$',
'^CosmosDBShell\.osx-arm64\..+\.nupkg$'
)

foreach ($pattern in $ridPatterns) {
$matched = @($packages | Where-Object { $_.Name -match $pattern })
if ($matched.Count -eq 0) {
throw "Missing expected RID package matching: $pattern"
}

if ($matched.Count -gt 1) {
$names = @($matched | ForEach-Object { $_.Name })
throw "Expected exactly one package matching $pattern (publishing a duplicate would push it twice). Found: $($names -join ', ')"
}
}

# The framework-dependent tool package (requires .NET 10
# on the target machine) ships alongside the RID set.
$fdd = $packages | Where-Object { $_.Name -match '^CosmosDBShell\.FrameworkDependent\..+\.nupkg$' }
if (-not $fdd -or $fdd.Count -ne 1) {
$names = @($fdd | ForEach-Object { $_.Name })
throw "Expected exactly one framework-dependent package (CosmosDBShell.FrameworkDependent.*). Found: $($names -join ', ')"
}

$pointer = $packages | Where-Object {
$_.Name -notmatch '^CosmosDBShell\.(win-x64|win-arm64|linux-x64|linux-arm64|osx-x64|osx-arm64|FrameworkDependent)\..+\.nupkg$'
}
if (-not $pointer -or $pointer.Count -ne 1) {
$names = @($pointer | ForEach-Object { $_.Name })
throw "Expected exactly one pointer (non-RID) package. Found: $($names -join ', ')"
}

Write-Host "Packages to publish:"
$packages | Sort-Object Name | ForEach-Object {
Write-Host " - $($_.Name) [$($_.Length) bytes]"
}

Write-Host "Target feed: $(NuGetOrgSource)"

Write-Host "##vso[task.setvariable variable=ResolvedPackageDir]$pkgDir"

- ${{ if eq(parameters.dryRun, true) }}:
- task: PowerShell@2
displayName: "Dry run - skipping nuget.org push"
inputs:
targetType: inline
pwsh: true
script: |
Write-Host "dryRun=true: validated the package set but did not push to $(NuGetOrgSource)."
Write-Host "Re-run with dryRun=false to publish."

- ${{ if eq(parameters.dryRun, false) }}:
- task: NuGetCommand@2
displayName: "Push packages to nuget.org"
inputs:
command: "push"
packagesToPush: '$(ResolvedPackageDir)\*.nupkg'
nuGetFeedType: "external"
publishFeedCredentials: "$(NuGetServiceConnection)"
Loading