Skip to content
Open
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
17 changes: 15 additions & 2 deletions .github/instructions/onebranch-pipeline-design.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Defined in `stages/build-stages.yml`. Four build stages plus validation, ordered
- **`build_abstractions`** (Stage 2) — Abstractions; `dependsOn: build_independent`; downloads Logging artifact
- **`build_dependent`** (Stage 3) — SqlClient and Extensions.Azure in parallel; `dependsOn: build_abstractions`; downloads Abstractions + Logging artifacts
- **`build_addons`** (Stage 4) — AKV Provider; `dependsOn: build_dependent`; downloads SqlClient + Abstractions + Logging artifacts
- **`sqlclient_package_validation`** — Validates signed SqlClient package; `dependsOn: build_dependent`; runs in parallel with Stage 4
- **`package_validation`** (Stage 5) — Validates every package produced by the run; `dependsOn` all four build stages plus `compute_versions`

Each build job copies PDB files into `$(JOB_OUTPUT)/symbols/` so they are included in the auto-published pipeline artifact alongside the NuGet packages in `$(JOB_OUTPUT)/packages/`.

Expand All @@ -46,7 +46,7 @@ Stage conditional rules:
## Job Templates

- **`build-buildproj-job.yml`** — Shared build.proj-driven package job used for all shipped packages. Flow: build via `build.proj` → optional ESRP DLL signing → pack via `build.proj` → optional ESRP NuGet signing → copy outputs for APIScan/artifacts
- **`validate-signed-package-job.yml`** — Validates signed MDS package (signature, strong names, folder structure, target frameworks)
- **`validate-packages-job.yml`** — Validates every package produced by the run. Downloads all package artifacts into one tree and validates them together, so `tools/PackageValidator` can apply its cross-package rules (the SqlClient family must share one version, and inter-package dependency ranges must agree); validating per package would silently skip those findings. Runs on Windows because Authenticode verification has no Linux equivalent
- **`publish-nuget-package-job.yml`** — Reusable release job using OneBranch `templateContext.type: releaseJob` with `inputs` for artifact download; pushes via `NuGetCommand@2`
- **`publish-symbols-job.yml`** — Reusable symbols job: downloads a build artifact, locates PDBs under `symbols/`, and invokes `publish-symbols-step.yml`

Expand All @@ -56,6 +56,19 @@ When adding a new package to the OneBranch flow:
- Add version variables to `variables/common-variables.yml`
- Add artifact name variables to `variables/onebranch-variables.yml`

## Package Validation Stage

- Defined in `stages/build-stages.yml`; produces stage `package_validation`
- Consumes the package and file versions published by `compute_versions` and asserts the produced packages carry exactly those values, so nothing is re-derived
- All packages are validated together in one job so `tools/PackageValidator` can apply cross-package rules; the SqlServer artifact and its expectations are conditional on `buildSqlServer`
- Expectations use the validator's `[id=]value` form: the SqlClient family version is applied as a wildcard (proving the family agrees, and catching the case where all packages are consistently wrong), with `Microsoft.SqlServer.Server` as a per-id override
- When SqlServer is not built its expectations are **omitted entirely** rather than passed empty — the validator rejects an expectation with an empty value
- Gate categories are derived from `isOfficial`: `error`, `missing-symbols`, `dependency-inconsistency`, `delay-signed`, and `unsigned` always, plus `package-unsigned` on official runs only. The `error` severity covers only error-severity findings, so each warning/info category must be named explicitly — `missing-symbols`, `dependency-inconsistency`, and `delay-signed` are warnings, and `unsigned` and `package-unsigned` are info. Strong-name signing is unconditional in `build-buildproj-step.yml`, so the two strong-name categories gate everywhere; NuGet package signing is ESRP and official-only, so `package-unsigned` would fire on every non-official run
- The validator runs twice: once with `--json` and no gate so the report exists even for a failing run, then once human-readable with the gate so failures appear in the job log
- Signature verification (`dotnet nuget verify --all`, Authenticode) runs on official builds only, and verifies that signatures are *trusted* — PackageValidator reports only their presence, from metadata
- The release stage `dependsOn: package_validation`, so a package that fails validation is never published
- Step and job logic lives in `scripts/validate-packages.ps1`, `scripts/verify-package-signatures.ps1`, and `scripts/verify-assembly-signatures.ps1`, each with Pester tests under `scripts/tests/`

## Symbols Publishing Stage

- Defined in `stages/publish-symbols-stage.yml`; produces stage `publish_symbols`
Expand Down
206 changes: 206 additions & 0 deletions eng/pipelines/onebranch/jobs/validate-packages-job.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#################################################################################
# Licensed to the .NET Foundation under one or more agreements. #
# The .NET Foundation licenses this file to you under the MIT license. #
# See the LICENSE file in the project root for more information. #
#################################################################################

# Validates every NuGet package produced by this run.
#
# All packages are downloaded into a single tree and validated together in one job, rather than one
# job per package, so that PackageValidator can apply its cross-package rules: the SqlClient family
# must share a single version, and their inter-package dependency ranges must agree.
#
# The job runs on Windows because Authenticode verification has no equivalent on the Linux agents.
# PackageValidator itself is cross-platform, so only the signature checks are OS-bound.

parameters:
# Package Parameters -----------------------------------------------------

- name: abstractionsArtifactsName
type: string

- name: akvProviderArtifactsName
type: string

- name: azureArtifactsName
type: string

- name: loggingArtifactsName
type: string

- name: sqlClientArtifactsName
type: string

- name: sqlServerArtifactsName
type: string

# Version Parameters -----------------------------------------------------
# Pre-computed by the compute-versions stage. Validation asserts the produced packages carry
# exactly these versions, so nothing here is re-derived.

- name: sqlClientPackageVersion
type: string

- name: sqlClientFileVersion
type: string

- name: sqlServerPackageVersion
type: string

- name: sqlServerFileVersion
type: string

# Behaviour Parameters ---------------------------------------------------

# Whether Microsoft.SqlServer.Server was built this run. When false there is no SqlServer
# artifact to download and no SqlServer package in the drop to validate.
- name: buildSqlServer
type: boolean

# True for official builds, which sign their packages and assemblies. Signature verification is
# skipped otherwise, because non-official runs deliberately produce unsigned output.
- name: isOfficial
type: boolean

jobs:
- job: validate_packages
displayName: 'Validate Packages'

pool:
type: windows

# 1ES auto-injects Roslyn into any job holding a DotNetCoreCLI build task, which here is only
# the PackageValidator tool build -- never shipped, so out of SDL scope.
templateContext:
sdl:
roslyn:
enabled: false

variables:
- name: ob_outputDirectory
value: '$(JOB_OUTPUT)'

# This job inspects already-built packages and produces no assemblies, so it has nothing for
# APIScan or BinSkim to scan and no shipping component to describe in an SBOM. The build
# jobs cover all three for the packages they produce.
- name: ob_sdl_apiscan_enabled
value: false
- name: ob_sdl_binskim_enabled
value: false
- name: ob_sdl_sbom_enabled
value: false

# Every package artifact is downloaded beneath this root, each into its own subdirectory so
# that identically-named files from different packages cannot collide.
- name: packagesRoot
value: '$(Pipeline.Workspace)/validate-packages'

- name: extractRoot
value: '$(Pipeline.Workspace)/validate-extract'

steps:
- template: /eng/pipelines/onebranch/steps/script-output-environment-variables-step.yml@self

# Only the packages themselves are needed, not the full build output each artifact carries.
- task: DownloadPipelineArtifact@2
displayName: 'Download Packages - Logging'
inputs:
artifactName: '${{ parameters.loggingArtifactsName }}'
targetPath: '$(packagesRoot)/Logging'
patterns: '**/*.*nupkg'

- task: DownloadPipelineArtifact@2
displayName: 'Download Packages - Abstractions'
inputs:
artifactName: '${{ parameters.abstractionsArtifactsName }}'
targetPath: '$(packagesRoot)/Abstractions'
patterns: '**/*.*nupkg'

- task: DownloadPipelineArtifact@2
displayName: 'Download Packages - SqlClient'
inputs:
artifactName: '${{ parameters.sqlClientArtifactsName }}'
targetPath: '$(packagesRoot)/SqlClient'
patterns: '**/*.*nupkg'

- task: DownloadPipelineArtifact@2
displayName: 'Download Packages - Azure'
inputs:
artifactName: '${{ parameters.azureArtifactsName }}'
targetPath: '$(packagesRoot)/Azure'
patterns: '**/*.*nupkg'

- task: DownloadPipelineArtifact@2
displayName: 'Download Packages - AkvProvider'
inputs:
artifactName: '${{ parameters.akvProviderArtifactsName }}'
targetPath: '$(packagesRoot)/AkvProvider'
patterns: '**/*.*nupkg'

- ${{ if eq(parameters.buildSqlServer, true) }}:
- task: DownloadPipelineArtifact@2
displayName: 'Download Packages - SqlServer'
inputs:
artifactName: '${{ parameters.sqlServerArtifactsName }}'
targetPath: '$(packagesRoot)/SqlServer'
patterns: '**/*.*nupkg'

# PackageValidator targets net10.0, which the repo's global.json already pins.
- template: /eng/pipelines/common/steps/install-dotnet.yml@self

- template: /eng/pipelines/onebranch/steps/validate-packages-step.yml@self
parameters:
packagesPath: '$(packagesRoot)'
reportPath: '$(JOB_OUTPUT)/validation/package-validation.json'
sqlClientPackageVersion: '${{ parameters.sqlClientPackageVersion }}'
sqlClientFileVersion: '${{ parameters.sqlClientFileVersion }}'
# Omitted when SqlServer is not built: its package is absent from the drop, and the
# validator rejects an expectation with an empty value.
${{ if eq(parameters.buildSqlServer, true) }}:
sqlServerPackageVersion: '${{ parameters.sqlServerPackageVersion }}'
sqlServerFileVersion: '${{ parameters.sqlServerFileVersion }}'
# The error severity covers only error-severity findings, so every warning/info category
# this job relies on must be named explicitly: missing-symbols, dependency-inconsistency
# and delay-signed are warnings, and unsigned and package-unsigned are info.
#
# Strong-name signing is unconditional in build-buildproj-step.yml, so delay-signed and
# unsigned gate on every run. NuGet package signing is ESRP and runs on official builds
# only, so package-unsigned would fire on every non-official build.
${{ if eq(parameters.isOfficial, true) }}:
failOn:
- error
- missing-symbols
- dependency-inconsistency
- delay-signed
- unsigned
- package-unsigned
Comment thread
paulmedynski marked this conversation as resolved.
${{ else }}:
failOn:
- error
- missing-symbols
- dependency-inconsistency
- delay-signed
- unsigned

# Signature verification, official builds only. PackageValidator reports strong-name and
# NuGet signature *presence* cross-platform; these steps additionally verify that the
# signatures are trusted, which requires the Windows trust store.
- ${{ if eq(parameters.isOfficial, true) }}:
- task: PowerShell@2
displayName: 'Verify NuGet package signatures'
inputs:
targetType: filePath
pwsh: true
filePath: $(REPO_ROOT)/eng/pipelines/onebranch/scripts/verify-package-signatures.ps1
arguments: >-
-PackagesPath "$(packagesRoot)"

- task: PowerShell@2
displayName: 'Verify assembly Authenticode signatures'
inputs:
targetType: filePath
pwsh: true
filePath: $(REPO_ROOT)/eng/pipelines/onebranch/scripts/verify-assembly-signatures.ps1
arguments: >-
-PackagesPath "$(packagesRoot)"
-ExtractPath "$(extractRoot)"
Loading
Loading