diff --git a/.github/nuget.github.config b/.github/nuget.github.config
new file mode 100644
index 0000000..765346e
--- /dev/null
+++ b/.github/nuget.github.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/.github/workflows/validate-and-package.yml b/.github/workflows/validate-and-package.yml
new file mode 100644
index 0000000..2214ddc
--- /dev/null
+++ b/.github/workflows/validate-and-package.yml
@@ -0,0 +1,291 @@
+name: Validate And Package
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "**"
+ workflow_dispatch:
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ BUILD_CONFIGURATION: Release
+
+jobs:
+ build-test-package:
+ if: github.event_name == 'pull_request' || startsWith(github.ref, 'refs/heads/') || github.event_name == 'workflow_dispatch'
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
+ cache: true
+ cache-dependency-path: |
+ global.json
+ Directory.Packages.props
+ **/*.csproj
+
+ - name: Restore
+ run: dotnet restore CosmosDBShell.sln --configfile .github/nuget.github.config
+
+ - name: Build solution
+ run: dotnet build CosmosDBShell.sln --configuration $env:BUILD_CONFIGURATION --no-restore
+ shell: pwsh
+
+ - name: Test solution
+ run: >-
+ dotnet test CosmosDBShell.sln
+ --configuration $env:BUILD_CONFIGURATION
+ --no-build
+ --no-restore
+ --logger "trx;LogFileName=test-results.trx"
+ --results-directory TestResults
+ --collect "Code coverage"
+ shell: pwsh
+
+ - name: Run fuzzer smoke test
+ working-directory: CosmosDBShell.Fuzzer
+ run: dotnet run --configuration $env:BUILD_CONFIGURATION --no-build --no-restore -- --all
+ shell: pwsh
+
+ - name: Fail if fuzzer crash findings exist
+ shell: pwsh
+ run: |
+ $crashes = Get-ChildItem -Path CosmosDBShell.Fuzzer/findings -Filter 'crash_*.txt' -ErrorAction SilentlyContinue
+ if ($crashes -and $crashes.Count -gt 0) {
+ Write-Error "Fuzzer recorded $($crashes.Count) crash(es). See the 'fuzz-findings' artifact for details."
+ exit 1
+ }
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results
+ path: TestResults
+ if-no-files-found: ignore
+
+ - name: Upload fuzzer findings
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: fuzz-findings
+ path: CosmosDBShell.Fuzzer/findings
+ if-no-files-found: ignore
+
+ - name: Compute version properties
+ if: github.event_name != 'pull_request'
+ id: version
+ shell: pwsh
+ run: |
+ $runNumber = [int]"${{ github.run_number }}"
+ $assemblyVersion = "1.0.$runNumber"
+ $branchName = "${{ github.ref_name }}"
+ $branchLabel = $branchName.ToLowerInvariant()
+ $branchLabel = $branchLabel -replace '[^0-9a-z-]', '-'
+ $branchLabel = $branchLabel -replace '-+', '-'
+ $branchLabel = $branchLabel.Trim('-')
+
+ if ([string]::IsNullOrWhiteSpace($branchLabel)) {
+ $branchLabel = 'branch'
+ }
+
+ if ($branchLabel.Length -gt 40) {
+ $branchLabel = $branchLabel.Substring(0, 40).Trim('-')
+ }
+
+ $packageVersion = "$assemblyVersion-preview.$branchLabel"
+ $fileVersion = "1.0.$runNumber.0"
+ $infoVersion = "$packageVersion+${{ github.sha }}"
+
+ "assembly_version=$assemblyVersion" >> $env:GITHUB_OUTPUT
+ "package_version=$packageVersion" >> $env:GITHUB_OUTPUT
+ "file_version=$fileVersion" >> $env:GITHUB_OUTPUT
+ "informational_version=$infoVersion" >> $env:GITHUB_OUTPUT
+ "branch_label=$branchLabel" >> $env:GITHUB_OUTPUT
+ "artifact_suffix=$branchLabel-${{ github.run_number }}" >> $env:GITHUB_OUTPUT
+
+ - name: Publish runtime artifacts
+ if: github.event_name != 'pull_request'
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $rids = @('win-x64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
+ foreach ($rid in $rids) {
+ dotnet restore CosmosDBShell/CosmosDBShell.csproj `
+ --configfile .github/nuget.github.config `
+ -r $rid
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "RID restore failed for $rid with exit code $LASTEXITCODE."
+ }
+
+ dotnet publish CosmosDBShell/CosmosDBShell.csproj `
+ --configuration $env:BUILD_CONFIGURATION `
+ --no-restore `
+ -r $rid `
+ --output "out/$rid" `
+ /p:Version=${{ steps.version.outputs.assembly_version }} `
+ /p:FileVersion=${{ steps.version.outputs.file_version }} `
+ /p:InformationalVersion=${{ steps.version.outputs.informational_version }}
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "RID publish failed for $rid with exit code $LASTEXITCODE."
+ }
+ }
+
+ - name: Pack NuGet artifacts
+ if: github.event_name != 'pull_request'
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Path out/nupkg -Force | Out-Null
+
+ # Restore for all RIDs so pack can build packages for each
+ dotnet restore CosmosDBShell/CosmosDBShell.csproj `
+ --configfile .github/nuget.github.config
+
+ dotnet pack CosmosDBShell/CosmosDBShell.csproj `
+ --configuration $env:BUILD_CONFIGURATION `
+ --no-restore `
+ --output out/nupkg `
+ /p:PackageVersion=${{ steps.version.outputs.package_version }} `
+ /p:Version=${{ steps.version.outputs.assembly_version }} `
+ /p:FileVersion=${{ steps.version.outputs.file_version }} `
+ /p:InformationalVersion=${{ steps.version.outputs.informational_version }} `
+ /p:ContinuousIntegrationBuild=true
+
+ - name: Validate NuGet package set
+ if: github.event_name != 'pull_request'
+ shell: pwsh
+ run: |
+ $pkgDir = Join-Path $pwd 'out/nupkg'
+ $ridPatterns = @(
+ 'CosmosDBShell.win-x64.*.nupkg',
+ 'CosmosDBShell.linux-x64.*.nupkg',
+ 'CosmosDBShell.linux-arm64.*.nupkg',
+ 'CosmosDBShell.osx-x64.*.nupkg',
+ 'CosmosDBShell.osx-arm64.*.nupkg'
+ )
+
+ foreach ($pattern in $ridPatterns) {
+ $matches = Get-ChildItem -Path (Join-Path $pkgDir $pattern) -ErrorAction SilentlyContinue
+ if (-not $matches -or $matches.Count -eq 0) {
+ Write-Error "Expected package was not generated: $pattern"
+ exit 1
+ }
+ }
+
+ $allPackages = Get-ChildItem -Path (Join-Path $pkgDir 'CosmosDBShell.*.nupkg') -ErrorAction SilentlyContinue
+ $pointerPackages = $allPackages | Where-Object {
+ $_.Name -notmatch '^CosmosDBShell\.(win-x64|linux-x64|linux-arm64|osx-x64|osx-arm64)\..+\.nupkg$'
+ }
+
+ if (-not $pointerPackages -or $pointerPackages.Count -ne 1) {
+ $names = @($pointerPackages | ForEach-Object { $_.Name })
+ Write-Error "Expected exactly one pointer package (non-RID). Found: $($names -join ', ')"
+ exit 1
+ }
+
+ $anyMatches = Get-ChildItem -Path (Join-Path $pkgDir 'CosmosDBShell.any.*.nupkg') -ErrorAction SilentlyContinue
+ if ($anyMatches -and $anyMatches.Count -gt 0) {
+ $names = $anyMatches | ForEach-Object { $_.Name } | Sort-Object
+ Write-Error "Unexpected any-RID package(s) found: $($names -join ', ')"
+ exit 1
+ }
+
+ - name: List packaged NuGet files
+ if: github.event_name != 'pull_request'
+ shell: pwsh
+ run: |
+ Get-ChildItem -Path out/nupkg -Filter *.nupkg -File | Sort-Object Name | ForEach-Object {
+ Write-Host " - $($_.Name) [$($_.Length) bytes]"
+ }
+
+ - name: Write package install summary
+ if: github.event_name != 'pull_request'
+ shell: pwsh
+ run: |
+ $summary = $env:GITHUB_STEP_SUMMARY
+ $version = '${{ steps.version.outputs.package_version }}'
+ $artifactSuffix = '${{ steps.version.outputs.artifact_suffix }}'
+ $branchName = '${{ github.ref_name }}'
+ $lines = @(
+ '## NuGet packages',
+ '',
+ ('- Branch: `' + $branchName + '`'),
+ ('- Preview version: `' + $version + '`'),
+ '',
+ 'Artifacts:',
+ ('- `CosmosDBShell-pointer-' + $artifactSuffix + '`'),
+ ('- `CosmosDBShell-win-x64-' + $artifactSuffix + '`'),
+ ('- `CosmosDBShell-linux-x64-' + $artifactSuffix + '`'),
+ ('- `CosmosDBShell-linux-arm64-' + $artifactSuffix + '`'),
+ ('- `CosmosDBShell-osx-x64-' + $artifactSuffix + '`'),
+ ('- `CosmosDBShell-osx-arm64-' + $artifactSuffix + '`'),
+ '',
+ 'Download the pointer package artifact and the artifact for your runtime, extract both `.nupkg` files to the same local folder, then install the base package:',
+ '',
+ '```powershell',
+ 'dotnet tool install --global CosmosDBShell --add-source C:\path\to\nupkgs --version ' + $version,
+ '```'
+ )
+ $lines -join "`n" | Out-File -FilePath $summary -Encoding utf8 -Append
+
+ - name: Upload pointer package
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: CosmosDBShell-pointer-${{ steps.version.outputs.artifact_suffix }}
+ path: out/nupkg/CosmosDBShell.${{ steps.version.outputs.package_version }}.nupkg
+ if-no-files-found: error
+
+ - name: Upload win-x64 package
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: CosmosDBShell-win-x64-${{ steps.version.outputs.artifact_suffix }}
+ path: out/nupkg/CosmosDBShell.win-x64.${{ steps.version.outputs.package_version }}.nupkg
+ if-no-files-found: error
+
+ - name: Upload linux-x64 package
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: CosmosDBShell-linux-x64-${{ steps.version.outputs.artifact_suffix }}
+ path: out/nupkg/CosmosDBShell.linux-x64.${{ steps.version.outputs.package_version }}.nupkg
+ if-no-files-found: error
+
+ - name: Upload linux-arm64 package
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: CosmosDBShell-linux-arm64-${{ steps.version.outputs.artifact_suffix }}
+ path: out/nupkg/CosmosDBShell.linux-arm64.${{ steps.version.outputs.package_version }}.nupkg
+ if-no-files-found: error
+
+ - name: Upload osx-x64 package
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: CosmosDBShell-osx-x64-${{ steps.version.outputs.artifact_suffix }}
+ path: out/nupkg/CosmosDBShell.osx-x64.${{ steps.version.outputs.package_version }}.nupkg
+ if-no-files-found: error
+
+ - name: Upload osx-arm64 package
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: CosmosDBShell-osx-arm64-${{ steps.version.outputs.artifact_suffix }}
+ path: out/nupkg/CosmosDBShell.osx-arm64.${{ steps.version.outputs.package_version }}.nupkg
+ if-no-files-found: error
diff --git a/.pipelines/CosmosDB-Shell-Official.yml b/.pipelines/CosmosDB-Shell-Official.yml
index 794a639..c3fa6f3 100644
--- a/.pipelines/CosmosDB-Shell-Official.yml
+++ b/.pipelines/CosmosDB-Shell-Official.yml
@@ -9,7 +9,9 @@
#################################################################################
trigger: # https://aka.ms/obpipelines/triggers
- - main
+ branches:
+ include:
+ - "main"
parameters: # parameters are shown up in ADO UI in a build queue time
- name: "debug"
@@ -28,7 +30,6 @@ variables:
BuildSolution: $(Build.SourcesDirectory)\CosmosDBShell.sln
ReleaseProject: $(Build.SourcesDirectory)\CosmosDBShell\CosmosDBShell.csproj
BuildConfiguration: Release
- OneES_SbomNugetSDLPath: out\nupkg
WindowsContainerImage: "onebranch.azurecr.io/windows/ltsc2022/vse2022:latest" # Docker image which is used to build the project https://aka.ms/obpipelines/containers
@@ -68,6 +69,7 @@ extends:
variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
ob_outputDirectory: '$(Build.SourcesDirectory)\out' # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
ob_artifactBaseName: cosmos_shell_all # combined artifact with all RIDs for SDL scanning
+ OneES_SbomNugetSDLPath: out\nupkg
# https://aka.ms/obpipelines/sdl
ob_sdl_binskim_enabled: true
ob_sdl_binskim_scanOutputDirectoryOnly: true
@@ -98,6 +100,19 @@ extends:
inputs:
targetType: inline
script: |
+ function Normalize-NuGetVersion([string]$value) {
+ if ([string]::IsNullOrWhiteSpace($value)) {
+ return $value
+ }
+
+ if ($value -match '^(?\d+(?:\.\d+)*)(?[-+].*)?$') {
+ $normalizedCore = (($Matches.core -split '\.') | ForEach-Object { [string]([int]$_) }) -join '.'
+ return "$normalizedCore$($Matches.suffix)"
+ }
+
+ return $value
+ }
+
$buildNumber = "$(Build.BuildNumber)"
Write-Host "Build.BuildNumber=$buildNumber"
@@ -106,6 +121,16 @@ extends:
$version = "1.0.$(Build.BuildId)"
}
+ $version = Normalize-NuGetVersion $version
+
+ $packageVersion = "$version-preview"
+
+ if ($buildNumber -ne $version) {
+ Write-Host "Updating Build.BuildNumber to normalized NuGet version: $version"
+ Write-Host "##vso[build.updatebuildnumber]$version"
+ $buildNumber = $version
+ }
+
$fileVersion = $version
$parts = $fileVersion.Split('.')
if ($parts.Count -eq 2) {
@@ -116,9 +141,10 @@ extends:
$fileVersion = ($parts[0..3] -join '.')
}
- $infoVersion = "$buildNumber+$(Build.SourceVersion)"
+ $infoVersion = "$packageVersion+$(Build.SourceVersion)"
Write-Host "##vso[task.setvariable variable=CosmosDBShell_Version]$version"
+ Write-Host "##vso[task.setvariable variable=CosmosDBShell_PackageVersion]$packageVersion"
Write-Host "##vso[task.setvariable variable=CosmosDBShell_FileVersion]$fileVersion"
Write-Host "##vso[task.setvariable variable=CosmosDBShell_InformationalVersion]$infoVersion"
@@ -129,6 +155,9 @@ extends:
projects: $(BuildSolution)
custom: "restore"
+ - task: ComponentGovernanceComponentDetection@0
+ displayName: "Component Governance - Component Detection"
+
# roslynanalyzers task wraps around dotnet build to enable static analysis
- task: RoslynAnalyzers@3
displayName: "DotNetCore build with RoslynAnalyzers"
@@ -298,17 +327,29 @@ extends:
# EXE that bundles all assemblies — the input signatures are lost.
# The actual payload signing happens post-pack via extract/sign/repack below.
- - task: DotNetCoreCLI@2
+ - task: PowerShell@2
displayName: "Pack CosmosDBShell NuGet"
condition: succeeded()
inputs:
- command: "pack"
- projects: "$(ReleaseProject)"
- outputDir: '$(Build.SourcesDirectory)\out\nupkg'
- arguments: >
- --configuration $(BuildConfiguration) --no-build --no-restore
- /p:PackageVersion=$(Build.BuildNumber)
- /p:ContinuousIntegrationBuild=true
+ targetType: inline
+ script: |
+ $pkgDir = "$(Build.SourcesDirectory)\out\nupkg"
+ New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null
+
+ dotnet pack "$(ReleaseProject)" `
+ --configuration "$(BuildConfiguration)" `
+ --no-build `
+ --no-restore `
+ --output $pkgDir `
+ /p:Version=$(CosmosDBShell_Version) `
+ /p:PackageVersion=$(CosmosDBShell_PackageVersion) `
+ /p:FileVersion=$(CosmosDBShell_FileVersion) `
+ /p:InformationalVersion=$(CosmosDBShell_InformationalVersion) `
+ /p:ContinuousIntegrationBuild=true
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "dotnet pack failed with exit code $LASTEXITCODE."
+ }
- task: PowerShell@2
displayName: "Expand RID packages for payload signing"
@@ -457,32 +498,73 @@ extends:
}
Write-Host "Non-shipping build outputs cleaned."
- # Push RID-specific NuGet packages first, then the pointer package last.
- # With ToolPackageRuntimeIdentifiers, dotnet pack produces per-RID packages
- # (e.g. CosmosDBShell.win-x64.*.nupkg) plus a pointer package (CosmosDBShell.*.nupkg).
- # All RID packages must be available before the pointer package is published.
- - task: NuGetCommand@2
- displayName: "Push RID-specific NuGet packages"
- condition: and(succeeded(),
- eq(variables['Build.SourceBranch'], 'refs/heads/main'),
- eq('${{ parameters.publishNuget }}', 'true'))
- inputs:
- command: "push"
- packagesToPush: "$(Build.SourcesDirectory)\\out\\nupkg\\CosmosDBShell.{win-x64,linux-x64,linux-arm64,osx-x64,osx-arm64}.*.nupkg"
- nuGetFeedType: "internal"
- publishVstsFeed: "CosmosDB/CosmosDB_CosmosShell"
- allowPackageConflicts: true
- - task: NuGetCommand@2
- displayName: "Push pointer NuGet package"
- condition: and(succeeded(),
- eq(variables['Build.SourceBranch'], 'refs/heads/main'),
- eq('${{ parameters.publishNuget }}', 'true'))
+ # Keep the pointer package in out\nupkg so it is preserved in pipeline artifacts
+ # and available for publishing to the internal feed alongside the RID packages.
+ - task: PowerShell@2
+ displayName: "List NuGet packages before publish"
+ condition: succeeded()
inputs:
- command: "push"
- packagesToPush: "$(Build.SourcesDirectory)\\out\\nupkg\\CosmosDBShell.$(Build.BuildNumber).nupkg"
- nuGetFeedType: "internal"
- publishVstsFeed: "CosmosDB/CosmosDB_CosmosShell"
- allowPackageConflicts: true
+ targetType: inline
+ script: |
+ $pkgDir = "$(Build.SourcesDirectory)\out\nupkg"
+ Write-Host "NuGet packages in $pkgDir"
+ Get-ChildItem -Path $pkgDir -Filter *.nupkg -File | Sort-Object Name | ForEach-Object {
+ Write-Host " - $($_.Name) [$($_.Length) bytes]"
+ }
+ - ${{ if and(eq(variables['Build.SourceBranch'], 'refs/heads/main'), eq(parameters.publishNuget, true)) }}:
+ - task: NuGetCommand@2
+ displayName: "Push win-x64 NuGet package"
+ inputs:
+ command: "push"
+ packagesToPush: '$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.win-x64.*.nupkg'
+ nuGetFeedType: "internal"
+ publishVstsFeed: "CosmosDB/CosmosDBShell"
+ allowPackageConflicts: true
+
+ - task: NuGetCommand@2
+ displayName: "Push linux-x64 NuGet package"
+ inputs:
+ command: "push"
+ packagesToPush: '$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.linux-x64.*.nupkg'
+ nuGetFeedType: "internal"
+ publishVstsFeed: "CosmosDB/CosmosDBShell"
+ allowPackageConflicts: true
+
+ - task: NuGetCommand@2
+ displayName: "Push linux-arm64 NuGet package"
+ inputs:
+ command: "push"
+ packagesToPush: '$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.linux-arm64.*.nupkg'
+ nuGetFeedType: "internal"
+ publishVstsFeed: "CosmosDB/CosmosDBShell"
+ allowPackageConflicts: true
+
+ - task: NuGetCommand@2
+ displayName: "Push osx-x64 NuGet package"
+ inputs:
+ command: "push"
+ packagesToPush: '$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.osx-x64.*.nupkg'
+ nuGetFeedType: "internal"
+ publishVstsFeed: "CosmosDB/CosmosDBShell"
+ allowPackageConflicts: true
+
+ - task: NuGetCommand@2
+ displayName: "Push osx-arm64 NuGet package"
+ inputs:
+ command: "push"
+ packagesToPush: '$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.osx-arm64.*.nupkg'
+ nuGetFeedType: "internal"
+ publishVstsFeed: "CosmosDB/CosmosDBShell"
+ allowPackageConflicts: true
+
+ - task: NuGetCommand@2
+ displayName: "Push base NuGet package"
+ inputs:
+ command: "push"
+ packagesToPush: '$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.*.nupkg;!$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.win-x64.*.nupkg;!$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.linux-x64.*.nupkg;!$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.linux-arm64.*.nupkg;!$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.osx-x64.*.nupkg;!$(Build.SourcesDirectory)\out\nupkg\CosmosDBShell.osx-arm64.*.nupkg'
+ nuGetFeedType: "internal"
+ publishVstsFeed: "CosmosDB/CosmosDBShell"
+ allowPackageConflicts: true
- job: CodeQLAnalyze
displayName: CodeQL (C#)
@@ -540,3 +622,26 @@ extends:
Write-Host "Copying from $source to $dest"
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Copy-Item -Path "$source\*" -Destination $dest -Recurse -Force
+
+ - task: PowerShell@2
+ displayName: "Validate package output for SBOM"
+ inputs:
+ targetType: inline
+ script: |
+ $outputDir = "$(ob_outputDirectory)"
+
+ if (-not (Test-Path $outputDir)) {
+ Write-Error "Package output directory not found: $outputDir"
+ exit 1
+ }
+
+ $files = Get-ChildItem -Path $outputDir -Recurse -File -ErrorAction SilentlyContinue
+ if (-not $files -or $files.Count -eq 0) {
+ Write-Error "Package output directory is empty: $outputDir"
+ exit 1
+ }
+
+ Write-Host "Package output contains $($files.Count) file(s):"
+ $files | Sort-Object FullName | Select-Object -First 50 | ForEach-Object {
+ Write-Host " - $($_.FullName) [$($_.Length) bytes]"
+ }
diff --git a/.pipelines/CosmosDB-Shell-PullRequest.yml b/.pipelines/CosmosDB-Shell-PullRequest.yml
deleted file mode 100644
index d02a201..0000000
--- a/.pipelines/CosmosDB-Shell-PullRequest.yml
+++ /dev/null
@@ -1,473 +0,0 @@
-#################################################################################
-# OneBranch Pipelines - PR Build #
-# This pipeline was created by EasyStart from a sample located at: #
-# https://aka.ms/obpipelines/easystart/samples #
-# Documentation: https://aka.ms/obpipelines #
-# Yaml Schema: https://aka.ms/obpipelines/yaml/schema #
-# Retail Tasks: https://aka.ms/obpipelines/tasks #
-# Support: https://aka.ms/onebranchsup #
-#################################################################################
-
-trigger: none # https://aka.ms/obpipelines/triggers
-
-parameters: # parameters are shown up in ADO UI in a build queue time
- - name: "debug"
- displayName: "Enable debug output"
- type: boolean
- default: false
-
-variables:
- CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
- system.debug: ${{ parameters.debug }}
-
- BuildSolution: $(Build.SourcesDirectory)\CosmosDBShell.sln
- ReleaseProject: $(Build.SourcesDirectory)\CosmosDBShell\CosmosDBShell.csproj
- BuildConfiguration: Release
- OneES_SbomNugetSDLPath: out\nupkg
-
- WindowsContainerImage: "onebranch.azurecr.io/windows/ltsc2022/vse2022:latest" # Docker image which is used to build the project https://aka.ms/obpipelines/containers
-
-resources:
- repositories:
- - repository: templates
- type: git
- name: OneBranch.Pipelines/GovernedTemplates
- ref: refs/heads/main
-
-extends:
- template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
- parameters:
- featureFlags:
- WindowsHostVersion:
- Version: 2022
- Network: R1
- globalSdl: # https://aka.ms/obpipelines/sdl
- # tsa:
- # enabled: true # SDL results of non-official builds aren't uploaded to TSA by default.
- # credscan:
- # suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json
- policheck:
- break: true # always break the build on policheck issues. You can disable it by setting to 'false'
- # suppression:
- # suppressionFile: $(Build.SourcesDirectory)\.gdn\global.gdnsuppress
-
- stages:
- - stage: build
- jobs:
- - job: main
- pool:
- type: windows # read more about custom job pool types at https://aka.ms/obpipelines/yaml/jobs
-
- variables:
- ob_outputDirectory: '$(Build.SourcesDirectory)\out' # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
- ob_artifactBaseName: cosmos_shell_all # combined artifact with all RIDs for SDL scanning
- # https://aka.ms/obpipelines/sdl
- ob_sdl_binskim_enabled: true # you can disable sdl tools in non-official build
- ob_sdl_binskim_break: true # always break the build on binskim issues. You can disable it by setting to 'false'
- ob_sdl_binskim_scanOutputDirectoryOnly: true
- ob_sdl_roslyn_break: true
- # ob_sdl_suppression_suppressionFile: $(Build.SourcesDirectory)\.gdn\job.gdnsuppress
-
- steps:
- - task: UseDotNet@2
- continueOnError: true
- inputs:
- packageType: "sdk"
- useGlobalJson: true
- performMultiLevelLookup: true
-
- - task: onebranch.pipeline.version@1 # generates automatic version. For other versioning options check https://aka.ms/obpipelines/versioning
- displayName: "Setup BuildNumber"
- inputs:
- system: "RevisionCounter"
- major: "1"
- minor: "0"
- exclude_commit: true
-
- - task: PowerShell@2
- displayName: "Compute version properties"
- inputs:
- targetType: inline
- script: |
- $buildNumber = "$(Build.BuildNumber)"
- Write-Host "Build.BuildNumber=$buildNumber"
-
- $version = $buildNumber
- if (-not ($version -match '^\d+(\.\d+){1,3}$')) {
- $version = "1.0.$(Build.BuildId)"
- }
-
- $fileVersion = $version
- $parts = $fileVersion.Split('.')
- if ($parts.Count -eq 2) {
- $fileVersion = "$fileVersion.$(Build.BuildId).0"
- } elseif ($parts.Count -eq 3) {
- $fileVersion = "$fileVersion.$(Build.BuildId)"
- } elseif ($parts.Count -gt 4) {
- $fileVersion = ($parts[0..3] -join '.')
- }
-
- $infoVersion = "$buildNumber+$(Build.SourceVersion)"
-
- Write-Host "##vso[task.setvariable variable=CosmosDBShell_Version]$version"
- Write-Host "##vso[task.setvariable variable=CosmosDBShell_FileVersion]$fileVersion"
- Write-Host "##vso[task.setvariable variable=CosmosDBShell_InformationalVersion]$infoVersion"
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore restore"
- inputs:
- command: "custom"
- projects: $(BuildSolution)
- custom: "restore"
-
- # roslynanalyzers task wraps around dotnet build to enable static analysis
- - task: RoslynAnalyzers@3
- displayName: "DotNetCore build with RoslynAnalyzers"
- inputs:
- userProvideBuildInfo: "msBuildInfo"
- msBuildCommandline: "dotnet.exe build $(BuildSolution) --no-restore --configuration $(BuildConfiguration)"
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore test"
- inputs:
- command: "test"
- projects: $(BuildSolution)
- arguments: '--no-build --no-restore --configuration $(BuildConfiguration) --logger trx --blame --collect "Code coverage" --results-directory $(Build.SourcesDirectory)\TestResults\'
- publishTestResults: false
-
- - task: PublishTestResults@2
- displayName: "Publish test results"
- inputs:
- testResultsFormat: VSTest
- testResultsFiles: '$(Build.SourcesDirectory)\TestResults\**\*.trx'
- failTaskOnFailedTests: true
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore publish (Windows)"
- inputs:
- command: "publish"
- publishWebProjects: false
- projects: $(ReleaseProject)
- arguments: '--configuration $(BuildConfiguration) -r win-x64 --output $(Build.SourcesDirectory)\out\win-x64 /p:Version=$(CosmosDBShell_Version) /p:FileVersion=$(CosmosDBShell_FileVersion) /p:InformationalVersion=$(CosmosDBShell_InformationalVersion)'
- zipAfterPublish: false
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore publish (MacOS)"
- inputs:
- command: "publish"
- publishWebProjects: false
- projects: $(ReleaseProject)
- arguments: '--configuration $(BuildConfiguration) -r osx-x64 --output $(Build.SourcesDirectory)\out\osx-x64 /p:Version=$(CosmosDBShell_Version) /p:FileVersion=$(CosmosDBShell_FileVersion) /p:InformationalVersion=$(CosmosDBShell_InformationalVersion)'
- zipAfterPublish: false
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore publish (Linux)"
- inputs:
- command: "publish"
- publishWebProjects: false
- projects: $(ReleaseProject)
- arguments: '--configuration $(BuildConfiguration) -r linux-x64 --output $(Build.SourcesDirectory)\out\linux-x64 /p:Version=$(CosmosDBShell_Version) /p:FileVersion=$(CosmosDBShell_FileVersion) /p:InformationalVersion=$(CosmosDBShell_InformationalVersion)'
- zipAfterPublish: false
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore publish (Linux ARM64)"
- inputs:
- command: "publish"
- publishWebProjects: false
- projects: $(ReleaseProject)
- arguments: '--configuration $(BuildConfiguration) -r linux-arm64 --output $(Build.SourcesDirectory)\out\linux-arm64 /p:Version=$(CosmosDBShell_Version) /p:FileVersion=$(CosmosDBShell_FileVersion) /p:InformationalVersion=$(CosmosDBShell_InformationalVersion)'
- zipAfterPublish: false
-
- - task: DotNetCoreCLI@2
- displayName: "DotNetCore publish (MacOS ARM64)"
- inputs:
- command: "publish"
- publishWebProjects: false
- projects: $(ReleaseProject)
- arguments: '--configuration $(BuildConfiguration) -r osx-arm64 --output $(Build.SourcesDirectory)\out\osx-arm64 /p:Version=$(CosmosDBShell_Version) /p:FileVersion=$(CosmosDBShell_FileVersion) /p:InformationalVersion=$(CosmosDBShell_InformationalVersion)'
- zipAfterPublish: false
-
- # Kept for reference, intentionally disabled:
- # - task: DotNetCoreCLI@2
- # displayName: "DotNetCore publish (Any)"
- # inputs:
- # command: "publish"
- # publishWebProjects: false
- # projects: $(ReleaseProject)
- # arguments: '--configuration $(BuildConfiguration) -r any --output $(Build.SourcesDirectory)\out\any /p:Version=$(CosmosDBShell_Version) /p:FileVersion=$(CosmosDBShell_FileVersion) /p:InformationalVersion=$(CosmosDBShell_InformationalVersion)'
- # zipAfterPublish: false
-
- - task: onebranch.pipeline.signing@1
- displayName: "Sign publish output"
- inputs:
- command: "sign"
- use_testsign: true
- signing_profile: "external_distribution"
- files_to_sign: "**/*.exe;**/*.dll"
- search_root: '$(Build.SourcesDirectory)\out'
-
- - task: DotNetCoreCLI@2
- displayName: "Build Fuzzer"
- inputs:
- command: "build"
- projects: "$(Build.SourcesDirectory)\\CosmosDBShell.Fuzzer\\CosmosDBShell.Fuzzer.csproj"
- arguments: "--configuration $(BuildConfiguration) --no-restore"
-
- - task: PowerShell@2
- displayName: "Clean NuGet package output"
- condition: succeeded()
- inputs:
- targetType: inline
- script: |
- $pkgDir = "$(Build.SourcesDirectory)\out\nupkg"
- New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null
- Get-ChildItem -Path $pkgDir -Filter *.nupkg -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
-
- # Note: signing bin/ IL assemblies before pack is ineffective here.
- # PublishSingleFile=true causes dotnet pack to produce a new single-file
- # EXE that bundles all assemblies — the input signatures are lost.
- # The actual payload signing happens post-pack via extract/sign/repack below.
-
- - task: DotNetCoreCLI@2
- displayName: "Pack CosmosDBShell NuGet"
- condition: succeeded()
- inputs:
- command: "pack"
- projects: "$(ReleaseProject)"
- outputDir: '$(Build.SourcesDirectory)\out\nupkg'
- arguments: >
- --configuration $(BuildConfiguration) --no-build --no-restore
- /p:PackageVersion=$(CosmosDBShell_Version)
- /p:ContinuousIntegrationBuild=true
-
- - task: PowerShell@2
- displayName: "Expand RID packages for payload signing"
- condition: succeeded()
- inputs:
- targetType: inline
- script: |
- $pkgDir = "$(Build.SourcesDirectory)\out\nupkg"
- $signStage = "$(Build.SourcesDirectory)\out\nupkg-payload"
-
- if (Test-Path $signStage) {
- Remove-Item -Recurse -Force $signStage
- }
-
- New-Item -ItemType Directory -Path $signStage -Force | Out-Null
- Add-Type -AssemblyName System.IO.Compression.FileSystem
-
- $ridPackages = Get-ChildItem -Path $pkgDir -Filter "CosmosDBShell.*.nupkg" -File -ErrorAction SilentlyContinue | Where-Object {
- $_.Name -match '^CosmosDBShell\.(win-x64|linux-x64|linux-arm64|osx-x64|osx-arm64)\..+\.nupkg$'
- }
-
- if (-not $ridPackages -or $ridPackages.Count -eq 0) {
- Write-Error "No RID-specific packages found to expand for payload signing."
- exit 1
- }
-
- foreach ($pkg in $ridPackages) {
- $targetDir = Join-Path $signStage $pkg.BaseName
- [System.IO.Compression.ZipFile]::ExtractToDirectory($pkg.FullName, $targetDir)
- }
-
- - task: onebranch.pipeline.signing@1
- displayName: "Sign NuGet payload binaries"
- condition: succeeded()
- inputs:
- command: "sign"
- use_testsign: true
- signing_profile: "external_distribution"
- files_to_sign: "**/*.exe;**/*.dll"
- search_root: '$(Build.SourcesDirectory)\out\nupkg-payload'
-
- - task: PowerShell@2
- displayName: "Repack RID packages with signed payload"
- condition: succeeded()
- inputs:
- targetType: inline
- script: |
- $pkgDir = "$(Build.SourcesDirectory)\out\nupkg"
- $signStage = "$(Build.SourcesDirectory)\out\nupkg-payload"
-
- Add-Type -AssemblyName System.IO.Compression.FileSystem
-
- $stagedPackages = Get-ChildItem -Path $signStage -Directory -ErrorAction SilentlyContinue
- if (-not $stagedPackages -or $stagedPackages.Count -eq 0) {
- Write-Error "No expanded packages found to repack."
- exit 1
- }
-
- foreach ($staged in $stagedPackages) {
- $packagePath = Join-Path $pkgDir ($staged.Name + ".nupkg")
-
- if (-not (Test-Path $packagePath)) {
- Write-Error "Expected package not found for repack: $packagePath"
- exit 1
- }
-
- Remove-Item -Path $packagePath -Force
- [System.IO.Compression.ZipFile]::CreateFromDirectory($staged.FullName, $packagePath, [System.IO.Compression.CompressionLevel]::Optimal, $false)
- }
-
- - task: PowerShell@2
- displayName: "Validate NuGet package set"
- condition: succeeded()
- inputs:
- targetType: inline
- script: |
- $pkgDir = "$(Build.SourcesDirectory)\out\nupkg"
- $ridPatterns = @(
- "CosmosDBShell.win-x64.*.nupkg",
- "CosmosDBShell.linux-x64.*.nupkg",
- "CosmosDBShell.linux-arm64.*.nupkg",
- "CosmosDBShell.osx-x64.*.nupkg",
- "CosmosDBShell.osx-arm64.*.nupkg"
- )
-
- foreach ($pattern in $ridPatterns) {
- $matches = Get-ChildItem -Path (Join-Path $pkgDir $pattern) -ErrorAction SilentlyContinue
- if (-not $matches -or $matches.Count -eq 0) {
- Write-Error "Expected package was not generated: $pattern"
- exit 1
- }
- }
-
- $allPackages = Get-ChildItem -Path (Join-Path $pkgDir "CosmosDBShell.*.nupkg") -ErrorAction SilentlyContinue
- $pointerPackages = $allPackages | Where-Object {
- $_.Name -notmatch '^CosmosDBShell\.(win-x64|linux-x64|linux-arm64|osx-x64|osx-arm64)\..+\.nupkg$'
- }
-
- if (-not $pointerPackages -or $pointerPackages.Count -ne 1) {
- $names = @($pointerPackages | ForEach-Object { $_.Name })
- Write-Error "Expected exactly one pointer package (non-RID). Found: $($names -join ', ')"
- exit 1
- }
-
- $anyMatches = Get-ChildItem -Path (Join-Path $pkgDir "CosmosDBShell.any.*.nupkg") -ErrorAction SilentlyContinue
- if ($anyMatches -and $anyMatches.Count -gt 0) {
- $names = $anyMatches | ForEach-Object { $_.Name } | Sort-Object
- Write-Error "Unexpected any-RID package(s) found: $($names -join ', ')"
- exit 1
- }
-
- - task: onebranch.pipeline.signing@1
- displayName: "Sign NuGet packages"
- condition: succeeded()
- inputs:
- command: "sign"
- signing_profile: "external_distribution"
- use_testsign: true
- cp_code: "CP-401405"
- files_to_sign: "**/*.nupkg"
- search_root: '$(Build.SourcesDirectory)\out\nupkg'
-
- - task: PowerShell@2
- displayName: "Run Fuzzer Smoke Test"
- inputs:
- targetType: inline
- script: |
- Write-Host "Starting fuzz smoke test..."
- # Stable artifact directory
- $artifactFindings = "$(Build.SourcesDirectory)\\fuzz-findings"
- if (Test-Path $artifactFindings) {
- Remove-Item -Recurse -Force $artifactFindings
- }
-
- Write-Host "Running fuzz harness"
- cd "$(Build.SourcesDirectory)\\CosmosDBShell.Fuzzer"
- dotnet run --no-build --configuration $(BuildConfiguration) -- --all
-
- # Check if the fuzzer created findings in the expected location
- $sourceFindingsDir = "$(Build.SourcesDirectory)\\CosmosDBShell.Fuzzer\\findings"
-
- $hasCrashes = $false
- $crashCount = 0
-
- # Check both possible locations
- foreach ($findingsPath in $sourceFindingsDir) {
- if (Test-Path $findingsPath) {
- $crashesDir = Join-Path $findingsPath "crashes"
- if (Test-Path $crashesDir) {
- $crashFiles = Get-ChildItem $crashesDir -File
- if ($crashFiles.Count -gt 0) {
- $hasCrashes = $true
- $crashCount = $crashFiles.Count
- Write-Host "Found $crashCount crash(es) at $crashesDir"
-
- # Copy findings to artifact directory for publishing
- New-Item -ItemType Directory -Path $artifactFindings -Force | Out-Null
- Copy-Item $findingsPath\* $artifactFindings -Recurse -Force
- break
- }
- }
- }
- }
-
- if ($hasCrashes) {
- echo "##vso[task.logissue type=error]Fuzzer detected $crashCount crash(es)."
- echo "##vso[task.setvariable variable=FuzzerCrashes]$crashCount"
- echo "##vso[task.setvariable variable=HasFuzzFindings]true"
- } else {
- Write-Host "No crashes detected during fuzz testing."
- echo "##vso[task.setvariable variable=HasFuzzFindings]false"
- }
-
- - task: PowerShell@2
- displayName: "Fail on Fuzz Crashes"
- condition: and(succeededOrFailed(), eq(variables['HasFuzzFindings'], 'true'))
- inputs:
- targetType: inline
- script: |
- if ($env:FuzzerCrashes -and [int]$env:FuzzerCrashes -gt 0) {
- Write-Error "Failing build due to $($env:FuzzerCrashes) fuzz crash(es)."
- exit 1
- }
-
- - job: CodeQLAnalyze
- displayName: CodeQL (C#)
- pool:
- vmImage: ubuntu-latest
- steps:
- - checkout: self
- persistCredentials: true
- - task: CodeQL3000Init@0
- inputs:
- languages: "csharp"
- - script: |
- dotnet restore CosmosDBShell.sln
- dotnet build CosmosDBShell.sln -c Release --no-incremental
- displayName: Build for CodeQL
- - task: CodeQL3000Finalize@0
- displayName: Finalize CodeQL
-
- - stage: package
- displayName: Package ArtifactsFre
- dependsOn: build
- jobs:
- - job: artifacts
- displayName: Package
- pool:
- type: windows
- variables:
- ob_outputDirectory: '$(Build.SourcesDirectory)\package'
- ob_artifactBaseName: cosmos_shell
- ob_artifactSuffix: _nupkg_$(Build.BuildNumber)
- steps:
- - download: current
- artifact: cosmos_shell_all
- displayName: Download build artifacts
-
- - task: PowerShell@2
- displayName: "Copy NuGet packages to output"
- inputs:
- targetType: inline
- script: |
- $source = "$(Pipeline.Workspace)\cosmos_shell_all\nupkg"
- $dest = "$(ob_outputDirectory)"
- Write-Host "Copying from $source to $dest"
- if (-not (Test-Path $source)) {
- Write-Error "NuGet package directory not found: $source"
- exit 1
- }
- New-Item -ItemType Directory -Path $dest -Force | Out-Null
- Copy-Item -Path "$source\*.nupkg" -Destination $dest -Force
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6d148cb..722b0d8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,9 +20,12 @@ There are several ways you can contribute to the CosmosDBShell project:
- **Prerequisites**: [.NET SDK 10.0+](https://dotnet.microsoft.com/download)
- Clone the repository and open it in VS Code or your preferred IDE.
- Restore dependencies: `dotnet restore CosmosDBShell.sln`
- - Build: `dotnet build CosmosDBShell.sln` (or press Ctrl+Shift+B in VS Code).
+ - Build: `dotnet build CosmosDBShell.sln` (or use the VS Code build task with Ctrl+Shift+B).
- Run tests: `dotnet test CosmosDBShell.sln`
- Run the tool locally: `dotnet run --project CosmosDBShell/CosmosDBShell.csproj`
+ - GitHub Actions runs CI and uploads NuGet package artifacts from [.github/workflows/validate-and-package.yml](.github/workflows/validate-and-package.yml).
+ - GitHub Actions uses [.github/nuget.github.config](.github/nuget.github.config) so it can restore from nuget.org independently of Azure Pipelines.
+ - Azure Pipelines runs from [.pipelines/CosmosDB-Shell-Official.yml](.pipelines/CosmosDB-Shell-Official.yml) for signed builds and publishing from the `main` branch (and any manual runs configured there).
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
diff --git a/CosmosDBShell.Tests/Parser/CommandStatementTests.cs b/CosmosDBShell.Tests/Parser/CommandStatementTests.cs
index cc06eac..e990292 100644
--- a/CosmosDBShell.Tests/Parser/CommandStatementTests.cs
+++ b/CosmosDBShell.Tests/Parser/CommandStatementTests.cs
@@ -391,7 +391,10 @@ public void ParseCommandStatement_Error()
// Should have reported an error for the unexpected }
Assert.NotEmpty(errors);
- Assert.Contains(errors, e => e.Message.Contains("}") || e.Message.Contains("unexpected"));
+ var errorSummary = string.Join(", ", errors.Select(e => $"'{e.Message}' at {e.Start} len {e.Length}"));
+ Assert.True(
+ errors.Any(e => e.Message.Contains("}", StringComparison.Ordinal) || e.Message.Contains("unexpected", StringComparison.OrdinalIgnoreCase)),
+ $"Expected a parse error mentioning '}}' or 'unexpected'. Actual errors: [{errorSummary}]");
}
[Fact]
diff --git a/CosmosDBShell.Tests/Shell/ShellTests.cs b/CosmosDBShell.Tests/Shell/ShellTests.cs
index 5dbf356..5f21f0c 100644
--- a/CosmosDBShell.Tests/Shell/ShellTests.cs
+++ b/CosmosDBShell.Tests/Shell/ShellTests.cs
@@ -69,7 +69,9 @@ public async Task VersionCommand_UsesInformationalVersion()
.InformationalVersion;
if (!string.IsNullOrWhiteSpace(informationalVersion))
{
- Assert.Contains("+", actualVersion, StringComparison.Ordinal);
+ Assert.True(
+ !actualVersion.Contains('+'),
+ $"Expected version output to omit build metadata and match the display version contract. Actual version: '{actualVersion}'. Raw informational version: '{informationalVersion}'. Display version: '{expectedVersion}'.");
}
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs
index cc416b4..271b5f2 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs
@@ -304,7 +304,7 @@ internal static string GetDisplayVersion(Assembly assembly)
var informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion;
if (!string.IsNullOrWhiteSpace(informationalVersion))
{
- return informationalVersion;
+ return informationalVersion.Split('+')[0];
}
return assembly.GetName().Version?.ToString() ?? "unknown";
diff --git a/README.md b/README.md
index 533c9c0..60747d4 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Lightweight CLI for Azure Cosmos DB.
## Quick Start
-**Requirements:** .NET SDK 9.0+
+**Requirements:** .NET SDK 10.0+
```bash
dotnet run --project CosmosDBShell
@@ -34,53 +34,31 @@ query "SELECT * FROM c"
When consuming build artifacts (`*.nupkg`) from this repo, install as a .NET global tool.
-1. Download the NuGet package(s) to a local folder.
-2. Install from that folder with `--add-source`.
+`dotnet tool install` for these packages requires .NET 10 because the tool packages target `net10.0`.
-### Platform package IDs
+1. Download the base tool package (`CosmosDBShell..nupkg`) and the package for your runtime to the same local folder.
+2. Install from that folder with `--add-source` using the base package ID `CosmosDBShell`.
-- Linux x64: `CosmosDBShell.linux-x64`
-- Linux ARM64: `CosmosDBShell.linux-arm64`
-- macOS x64: `CosmosDBShell.osx-x64`
-- macOS ARM64: `CosmosDBShell.osx-arm64`
-- Windows x64: `CosmosDBShell.win-x64`
+### Runtime-specific package files
-### Install commands
+- Linux x64: `CosmosDBShell.linux-x64..nupkg`
+- Linux ARM64: `CosmosDBShell.linux-arm64..nupkg`
+- macOS x64: `CosmosDBShell.osx-x64..nupkg`
+- macOS ARM64: `CosmosDBShell.osx-arm64..nupkg`
+- Windows x64: `CosmosDBShell.win-x64..nupkg`
-Linux x64:
+### Install command
-```bash
-dotnet tool install --global CosmosDBShell.linux-x64 --add-source /path/to/nupkgs --version
-```
-
-Linux ARM64:
-
-```bash
-dotnet tool install --global CosmosDBShell.linux-arm64 --add-source /path/to/nupkgs --version
-```
-
-macOS x64:
+After placing the base package and the matching runtime package in the same folder, install with the base package ID:
```bash
-dotnet tool install --global CosmosDBShell.osx-x64 --add-source /path/to/nupkgs --version
-```
-
-macOS ARM64:
-
-```bash
-dotnet tool install --global CosmosDBShell.osx-arm64 --add-source /path/to/nupkgs --version
+dotnet tool install --global CosmosDBShell --add-source /path/to/nupkgs --version
```
-Windows x64 (PowerShell):
+Windows PowerShell example:
```powershell
-dotnet tool install --global CosmosDBShell.win-x64 --add-source C:\path\to\nupkgs --version
-```
-
-If your feed includes the base tool package (`CosmosDBShell..nupkg`) and its RID package, this also works:
-
-```bash
-dotnet tool install --global CosmosDBShell --add-source /path/to/nupkgs --version
+dotnet tool install --global CosmosDBShell --add-source C:\path\to\nupkgs --version
```
### Use, update, uninstall
@@ -99,8 +77,16 @@ dotnet tool update --global --add-source /path/to/nupkgs --version
Uninstall:
+List the installed global tools first so you can identify the exact package ID:
+
```bash
-dotnet tool uninstall --global
+dotnet tool list --global
+```
+
+Then uninstall the tool by its package ID:
+
+```bash
+dotnet tool uninstall --global CosmosDBShell
```
## Documentation
@@ -111,7 +97,17 @@ dotnet tool uninstall --global
- [Programming](docs/programming.md) - Variables, control flow, functions
- [MCP](docs/mcp.md) - Model Context Protocol integration
-## CLI Arguments
+## CI And Packaging
+
+This repo currently uses one GitHub Actions workflow for validation and package artifacts:
+
+- [.github/workflows/validate-and-package.yml](.github/workflows/validate-and-package.yml): runs validation on pull requests, and on branch pushes or manual runs it also builds installable RID-specific NuGet tool packages and uploads them as workflow artifacts
+
+GitHub Actions uses [.github/nuget.github.config](.github/nuget.github.config) so restores do not depend on the Azure DevOps feed.
+
+Packaging runs produce preview versions in the form `1.0.-preview.`, upload separate artifacts for each RID-specific package plus a pointer/base package artifact for the non-RID package ID, and the Azure pipeline publishes both the base package and the RID-specific packages to the internal feed.
+
+## Command-Line Arguments
| Option | Description |
| ------ | ----------- |