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 | | ------ | ----------- |