Skip to content
Merged
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
117 changes: 108 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ name: CI

on:
workflow_dispatch:
inputs:
diagnostic-sha:
description: Exact 40-character commit SHA for a single-consumer diagnostic
required: false
type: string
default: ''
diagnostic-scenario:
description: Exact consumer scenario ID for a single-consumer diagnostic
required: false
type: string
default: ''
diagnostic-repeat:
description: Number of diagnostic runs (1-5)
required: false
type: string
default: ''
push:
branches: [ main, upd, release/2.2.0 ]
pull_request:
Expand All @@ -15,17 +31,17 @@ env:

jobs:
validation:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
uses: ./.github/workflows/reusable-release-validation.yml
permissions:
contents: read
with:
runner-labels: ${{ github.event_name == 'pull_request' && '["self-hosted","Windows","X64"]' || '["ubuntu-latest"]' }}
runner-labels: ${{ github.event_name == 'pull_request' && '["self-hosted","Windows","X64","smartpipe-cleanup-v1"]' || '["ubuntu-latest"]' }}

hosting-integration:
name: Hosting integration (${{ matrix.os == 'self-hosted' && 'Windows' || matrix.os }})
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ matrix.os == 'self-hosted' && fromJSON('["self-hosted","Windows","X64"]') || matrix.os }}
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
runs-on: ${{ matrix.os == 'self-hosted' && fromJSON('["self-hosted","Windows","X64","smartpipe-cleanup-v1"]') || matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
Expand All @@ -51,8 +67,8 @@ jobs:
run: dotnet test --project tests/SmartPipe.Extensions.Hosting.Tests/SmartPipe.Extensions.Hosting.Tests.csproj --configuration Release --no-build --filter-class SmartPipe.Extensions.Hosting.Tests.Integration.GenericHostIntegrationTests --minimum-expected-tests 1

json-file-windows:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64"]') || 'windows-latest' }}
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64","smartpipe-cleanup-v1"]') || 'windows-latest' }}
timeout-minutes: 20

steps:
Expand Down Expand Up @@ -107,8 +123,8 @@ jobs:

baseline-contract-windows:
name: Baseline contract (Windows)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64"]') || 'windows-latest' }}
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64","smartpipe-cleanup-v1"]') || 'windows-latest' }}
timeout-minutes: 20

steps:
Expand Down Expand Up @@ -137,11 +153,94 @@ jobs:
- name: Verify 2.1.2 baseline offline
run: dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-build -- verify-baseline --repo-root . --manifest eng/baselines/2.1.2/manifest.json --packages-dir artifacts/baselines/2.1.2 --offline --mode integrity

diagnostic-consumer:
name: Diagnostic consumer (${{ inputs.diagnostic-scenario }})
if: github.event_name == 'workflow_dispatch' && (inputs.diagnostic-sha != '' || inputs.diagnostic-scenario != '' || inputs.diagnostic-repeat != '')
runs-on: [self-hosted, Windows, X64, smartpipe-cleanup-v1]
timeout-minutes: 45
steps:
- name: Validate diagnostic inputs
shell: pwsh
env:
DIAGNOSTIC_SHA: ${{ inputs.diagnostic-sha }}
DIAGNOSTIC_SCENARIO: ${{ inputs.diagnostic-scenario }}
DIAGNOSTIC_REPEAT: ${{ inputs.diagnostic-repeat }}
run: |
$ErrorActionPreference = 'Stop'
if ($env:DIAGNOSTIC_SHA -notmatch '^[0-9a-f]{40}$') { throw 'diagnostic-sha must be exactly 40 lowercase hexadecimal characters.' }
if ($env:DIAGNOSTIC_SCENARIO -notmatch '^[a-z0-9-]+$') { throw 'diagnostic-scenario must contain lowercase letters, digits, or hyphens.' }
if ($env:DIAGNOSTIC_REPEAT -notmatch '^[1-5]$') { throw 'diagnostic-repeat must be an integer from 1 through 5.' }

- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.diagnostic-sha }}
persist-credentials: false
fetch-depth: 1

- name: Verify exact diagnostic checkout
shell: pwsh
env:
DIAGNOSTIC_SHA: ${{ inputs.diagnostic-sha }}
run: |
$ErrorActionPreference = 'Stop'
$actual = (git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or $actual -cne $env:DIAGNOSTIC_SHA) { throw "Checked out SHA '$actual' does not match the requested diagnostic SHA." }

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json

- name: Restore locked
run: dotnet restore SmartPipe.Core.slnx --locked-mode

- name: Build
run: dotnet build SmartPipe.Core.slnx --configuration Release --no-restore -warnaserror

- name: Set package version
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$packageVersion = (dotnet msbuild src/SmartPipe.Core/SmartPipe.Core.csproj -getProperty:Version -nologo).Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($packageVersion)) { throw 'Unable to determine the package version.' }
"PACKAGE_VERSION=$packageVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Pack packages from graph
shell: pwsh
run: >
dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj
--configuration Release --no-build -- pack-packages
--mode current --configuration Release --package-version "$env:PACKAGE_VERSION"
--output artifacts/packages --manifest artifacts/packages/manifest.json

- name: Run diagnostic consumer
shell: pwsh
env:
DIAGNOSTIC_SCENARIO: ${{ inputs.diagnostic-scenario }}
DIAGNOSTIC_REPEAT: ${{ inputs.diagnostic-repeat }}
run: |
$ErrorActionPreference = 'Stop'
$rows = [Collections.Generic.List[string]]::new()
$failed = $false
for ($pass = 1; $pass -le [int]$env:DIAGNOSTIC_REPEAT; $pass++) {
$output = (& dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-build -- run-consumers --set current --scenario $env:DIAGNOSTIC_SCENARIO --package-directory artifacts/packages --package-version "$env:PACKAGE_VERSION" 2>&1 | Out-String).Trim()
$exitCode = $LASTEXITCODE
$snippet = ($output -replace '\r?\n', ' ').Trim()
if ($snippet.Length -gt 512) { $snippet = $snippet.Substring($snippet.Length - 512) }
$rows.Add("- pass ${pass}: exit=$exitCode; $snippet")
if ($exitCode -ne 0) { $failed = $true; break }
}
$summary = @('## Single-consumer diagnostic', '', "- scenario: $env:DIAGNOSTIC_SCENARIO", "- repeat requested: $env:DIAGNOSTIC_REPEAT") + $rows
$summaryText = ($summary -join [Environment]::NewLine)
if ($summaryText.Length -gt 8192) { $summaryText = $summaryText.Substring(0, 8192) + [Environment]::NewLine + '... summary truncated ...' }
Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value $summaryText
if ($failed) { exit 1 }

cleanup-self-hosted:
name: Cleanup self-hosted workspace
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
needs: [validation, hosting-integration, json-file-windows, baseline-contract-windows]
runs-on: [self-hosted, Windows, X64]
runs-on: [self-hosted, Windows, X64, smartpipe-cleanup-v1]
steps:
- name: Cleanup generated outputs
shell: pwsh
Expand Down
65 changes: 11 additions & 54 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CodeQL
name: Hosted .NET static analysis

on:
push:
Expand All @@ -10,15 +10,11 @@ on:

permissions:
contents: read
security-events: write

env:
NUGET_PACKAGES: ${{ github.event_name == 'pull_request' && format('{0}/.nuget/packages', github.workspace) || '' }}

jobs:
analyze:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64"]') || 'ubuntu-latest' }}
name: Hosted .NET static analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand All @@ -29,53 +25,14 @@ jobs:
with:
global-json-file: global.json

- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: csharp

- name: Build
run: dotnet build SmartPipe.Core.slnx -c Release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
ram: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && '16384' || '' }}
threads: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && '2' || '' }}
- name: Restore locked
shell: pwsh
run: |
dotnet restore SmartPipe.Core.slnx --locked-mode
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

cleanup-self-hosted:
name: Cleanup self-hosted workspace
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
needs: [analyze]
runs-on: [self-hosted, Windows, X64]
steps:
- name: Cleanup generated outputs
- name: Build static analysis
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($env:GITHUB_WORKSPACE)) { throw 'GITHUB_WORKSPACE is required.' }
$workspace = [IO.Path]::GetFullPath($env:GITHUB_WORKSPACE).TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
if ((Get-Item -LiteralPath $workspace -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Workspace is a reparse point.' }
$prefix = "$workspace$([IO.Path]::DirectorySeparatorChar)"
$targets = [Collections.Generic.List[string]]::new()
$targets.Add((Join-Path $workspace 'artifacts'))
$targets.Add((Join-Path $workspace 'BenchmarkDotNet.Artifacts'))
$targets.Add((Join-Path $workspace '.nuget'))
$pending = [Collections.Generic.Stack[string]]::new()
$pending.Push($workspace)
while ($pending.Count) {
foreach ($directory in Get-ChildItem -LiteralPath $pending.Pop() -Force -Directory) {
if ($directory.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
if ($directory.Name -in 'bin', 'obj') { $targets.Add($directory.FullName) }
else { $pending.Push($directory.FullName) }
}
}
foreach ($target in $targets | Sort-Object Length -Descending -Unique) {
$fullPath = [IO.Path]::GetFullPath($target)
if (!$fullPath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Outside workspace: $fullPath" }
if (Test-Path -LiteralPath $fullPath -PathType Container) {
if ((Get-Item -LiteralPath $fullPath -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Reparse point: $fullPath" }
if (Get-ChildItem -LiteralPath $fullPath -Force -Recurse | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }) { throw "Reparse point: $fullPath" }
Remove-Item -LiteralPath $fullPath -Recurse -Force
}
}
dotnet build SmartPipe.Core.slnx --configuration Release --no-restore -warnaserror
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
79 changes: 40 additions & 39 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -1,57 +1,58 @@
name: Dependency Review
name: Repository security audit

on:
pull_request:
branches: [ main, release/2.2.0, sp220/checkpoint-c, sp220/checkpoint-d ]

permissions:
contents: read
pull-requests: read

jobs:
dependency-review:
repository-security-audit:
name: Repository security audit
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: [self-hosted, Windows, X64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Dependency review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json

cleanup-self-hosted:
name: Cleanup self-hosted workspace
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
needs: [dependency-review]
runs-on: [self-hosted, Windows, X64]
steps:
- name: Cleanup generated outputs
- name: Restore locked
shell: pwsh
run: |
dotnet restore SmartPipe.Core.slnx --locked-mode
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Build repository checks
shell: pwsh
run: |
dotnet build eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-restore -warnaserror
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Verify repository package contracts
shell: pwsh
run: |
dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-build --no-restore -- verify --profile sp220-05 --format github --failures-only
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Vulnerable package scan
shell: pwsh
run: |
New-Item -ItemType Directory -Path artifacts/audit -Force | Out-Null
dotnet package list --project SmartPipe.Core.slnx --vulnerable --include-transitive --format json --output-version 1 --no-restore > artifacts/audit/vulnerable.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Verify direct production audit policy
shell: pwsh
run: dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-build --no-restore -- verify-nuget-audit --repo-root . --report artifacts/audit/vulnerable.json

- name: Deprecated package scan
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($env:GITHUB_WORKSPACE)) { throw 'GITHUB_WORKSPACE is required.' }
$workspace = [IO.Path]::GetFullPath($env:GITHUB_WORKSPACE).TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
if ((Get-Item -LiteralPath $workspace -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Workspace is a reparse point.' }
$prefix = "$workspace$([IO.Path]::DirectorySeparatorChar)"
$targets = [Collections.Generic.List[string]]::new()
$targets.Add((Join-Path $workspace 'artifacts'))
$targets.Add((Join-Path $workspace 'BenchmarkDotNet.Artifacts'))
$pending = [Collections.Generic.Stack[string]]::new()
$pending.Push($workspace)
while ($pending.Count) {
foreach ($directory in Get-ChildItem -LiteralPath $pending.Pop() -Force -Directory) {
if ($directory.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
if ($directory.Name -in 'bin', 'obj') { $targets.Add($directory.FullName) }
else { $pending.Push($directory.FullName) }
}
}
foreach ($target in $targets | Sort-Object Length -Descending -Unique) {
$fullPath = [IO.Path]::GetFullPath($target)
if (!$fullPath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Outside workspace: $fullPath" }
if (Test-Path -LiteralPath $fullPath -PathType Container) {
if ((Get-Item -LiteralPath $fullPath -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Reparse point: $fullPath" }
if (Get-ChildItem -LiteralPath $fullPath -Force -Recurse | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }) { throw "Reparse point: $fullPath" }
Remove-Item -LiteralPath $fullPath -Recurse -Force
}
}
dotnet package list --project SmartPipe.Core.slnx --deprecated --include-transitive --format json --output-version 1 --no-restore > artifacts/audit/deprecated.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Loading
Loading