Skip to content
Merged
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