feat(ci): reusable .NET CI and NuGet release workflows #3
No files matched your search
@@ -0,0 +1,265 @@
|
||||
# .NET build, test, coverage, and format gates (K22-CI-01, K22-CI-02, K22-TEST-10,
|
||||
# K22-DEP-11, K22-CODE-01). Called as:
|
||||
#
|
||||
# jobs:
|
||||
# ci:
|
||||
# uses: KofTwentyTwo/standards/.github/workflows/dotnet.yml@<sha> # vX.Y.Z
|
||||
# permissions:
|
||||
# contents: read
|
||||
# with:
|
||||
# solution: MySolution.slnx
|
||||
# coverage-packages: My.Library,My.Library.Core
|
||||
# test-projects: tests/My.Library.Tests
|
||||
# ui-test-projects: tests/My.App.UiTests
|
||||
#
|
||||
# producing the required contexts `ci / build-test` and `ci / format`, plus
|
||||
# `ci / ui-tests` when ui-test-projects is set. Every step runs the same command a
|
||||
# developer runs locally (K22-CI-03):
|
||||
#
|
||||
# dotnet restore <solution> -p:Platform=x64 --locked-mode
|
||||
# dotnet build <solution> -p:Platform=x64 -warnaserror --no-restore
|
||||
# dotnet test <project> --no-build --collect:"XPlat Code Coverage"
|
||||
# dotnet format <solution> --verify-no-changes --severity warn --no-restore
|
||||
#
|
||||
# Test projects are run without -p:Platform: an x64 solution build maps AnyCPU test
|
||||
# projects to their AnyCPU output folders, which is where `dotnet test` must look.
|
||||
name: dotnet
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
solution:
|
||||
description: Solution (.slnx or .sln) or project file to build, relative to the repository root.
|
||||
type: string
|
||||
required: true
|
||||
platform:
|
||||
description: Solution platform passed as -p:Platform (WinUI solutions need x64, x86, or ARM64).
|
||||
type: string
|
||||
default: x64
|
||||
runs-on:
|
||||
description: Runner label. WinUI and WPF solutions need a Windows runner.
|
||||
type: string
|
||||
default: windows-latest
|
||||
coverage-packages:
|
||||
description: Comma-separated assembly (coverage package) names that must meet coverage-minimum. Empty disables the gate.
|
||||
type: string
|
||||
default: ""
|
||||
coverage-minimum:
|
||||
description: Minimum line coverage, in percent, for each package in coverage-packages.
|
||||
type: number
|
||||
default: 100
|
||||
test-projects:
|
||||
description: Newline-separated test projects to run with coverage.
|
||||
type: string
|
||||
default: ""
|
||||
ui-test-projects:
|
||||
description: Newline-separated end-to-end UI test projects (FlaUI). Empty skips the ui-tests job.
|
||||
type: string
|
||||
default: ""
|
||||
pack:
|
||||
description: Also run dotnet pack (Release) and upload the packages as an artifact.
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
name: build-test
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
# Prefixed: MSBuild turns every environment variable into a property, so a variable
|
||||
# named PLATFORM would silently change output paths (bin/x64/) for dotnet test.
|
||||
K22_SOLUTION: ${{ inputs.solution }}
|
||||
K22_PLATFORM: ${{ inputs.platform }}
|
||||
TEST_PROJECTS: ${{ inputs.test-projects }}
|
||||
COVERAGE_PACKAGES: ${{ inputs.coverage-packages }}
|
||||
COVERAGE_MINIMUM: ${{ inputs.coverage-minimum }}
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET SDK
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
# --locked-mode fails if a resolved version differs from packages.lock.json, so
|
||||
# the build can only use the dependency graph that was reviewed and scanned.
|
||||
- name: Restore (locked)
|
||||
shell: pwsh
|
||||
run: dotnet restore $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --locked-mode
|
||||
|
||||
# -warnaserror also fails on NuGetAudit vulnerability warnings.
|
||||
- name: Build (warnings as errors)
|
||||
shell: pwsh
|
||||
run: dotnet build $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" -warnaserror --no-restore
|
||||
|
||||
- name: Test with coverage
|
||||
if: inputs.test-projects != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$settings = @()
|
||||
if (Test-Path -LiteralPath coverage.runsettings) { $settings = @('--settings', 'coverage.runsettings') }
|
||||
$projects = $env:TEST_PROJECTS -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
foreach ($project in $projects)
|
||||
{
|
||||
dotnet test $project --no-build @settings --collect:"XPlat Code Coverage" --results-directory TestResults
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# Best line rate per package across every report: a package can be exercised by
|
||||
# several test projects, and the report that covers it most is authoritative.
|
||||
- name: Coverage gate
|
||||
if: inputs.coverage-packages != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = @(Get-ChildItem -Path TestResults -Recurse -Filter coverage.cobertura.xml -ErrorAction SilentlyContinue)
|
||||
if ($files.Count -eq 0)
|
||||
{
|
||||
Write-Output '::error::No coverage.cobertura.xml under TestResults; did the coverage collector run?'
|
||||
exit 1
|
||||
}
|
||||
$reports = foreach ($file in $files) { [xml] (Get-Content -LiteralPath $file.FullName -Raw) }
|
||||
$required = [double] $env:COVERAGE_MINIMUM / 100
|
||||
$failed = $false
|
||||
foreach ($name in ($env:COVERAGE_PACKAGES -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }))
|
||||
{
|
||||
$best = $null
|
||||
$bestPackage = $null
|
||||
foreach ($report in $reports)
|
||||
{
|
||||
$package = @($report.coverage.packages.package) | Where-Object { $_.name -eq $name }
|
||||
if (-not $package) { continue }
|
||||
$rate = [double]::Parse($package.'line-rate', [Globalization.CultureInfo]::InvariantCulture)
|
||||
if ($null -eq $best -or $rate -gt $best) { $best = $rate; $bestPackage = $package }
|
||||
}
|
||||
if ($null -eq $best)
|
||||
{
|
||||
Write-Output "::error::Package '$name' was not found in any coverage report."
|
||||
$failed = $true
|
||||
continue
|
||||
}
|
||||
Write-Output ('{0} line coverage: {1}% (required {2}%)' -f $name, [math]::Round($best * 100, 2), $env:COVERAGE_MINIMUM)
|
||||
if ($best -lt $required)
|
||||
{
|
||||
foreach ($class in @($bestPackage.classes.class))
|
||||
{
|
||||
$missed = @($class.lines.line) | Where-Object { $_.hits -eq '0' } | ForEach-Object { $_.number }
|
||||
if ($missed) { Write-Output "::error file=$($class.filename)::$name uncovered lines $($missed -join ', ')" }
|
||||
}
|
||||
$failed = $true
|
||||
}
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
Write-Output 'Coverage gate passed.'
|
||||
|
||||
- name: Upload coverage
|
||||
if: always() && inputs.test-projects != ''
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: coverage-cobertura
|
||||
path: TestResults/**/coverage.cobertura.xml
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Pack (Release)
|
||||
if: inputs.pack
|
||||
shell: pwsh
|
||||
run: dotnet pack $env:K22_SOLUTION -c Release "-p:Platform=$env:K22_PLATFORM" -o artifacts/nuget
|
||||
|
||||
- name: Upload packages
|
||||
if: inputs.pack
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: artifacts/nuget/*
|
||||
if-no-files-found: error
|
||||
|
||||
format:
|
||||
name: format
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
K22_SOLUTION: ${{ inputs.solution }}
|
||||
K22_PLATFORM: ${{ inputs.platform }}
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET SDK
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Restore (locked)
|
||||
shell: pwsh
|
||||
run: dotnet restore $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --locked-mode
|
||||
|
||||
# The Kingsrook .editorconfig sets layout (IDE0055) and the style rules to
|
||||
# warning, so --severity warn makes any layout or style drift fail here exactly
|
||||
# as it fails `dotnet build` at the desk.
|
||||
# dotnet format analyzes the workspace without running XAML compilation; without the
|
||||
# generated *.g.cs files from a build, XAML event handlers look unused (IDE0051).
|
||||
- name: Build (generates XAML code the analyzers need)
|
||||
shell: pwsh
|
||||
run: dotnet build $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --no-restore
|
||||
|
||||
- name: Verify formatting (whitespace, style, analyzers)
|
||||
shell: pwsh
|
||||
run: dotnet format $env:K22_SOLUTION --verify-no-changes --severity warn --no-restore
|
||||
|
||||
ui-tests:
|
||||
name: ui-tests
|
||||
if: inputs.ui-test-projects != ''
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
K22_SOLUTION: ${{ inputs.solution }}
|
||||
K22_PLATFORM: ${{ inputs.platform }}
|
||||
UI_TEST_PROJECTS: ${{ inputs.ui-test-projects }}
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET SDK
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Restore (locked)
|
||||
shell: pwsh
|
||||
run: dotnet restore $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --locked-mode
|
||||
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: dotnet build $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --no-restore
|
||||
|
||||
# FlaUI drives the built apps on the hosted runner's interactive desktop.
|
||||
- name: Run UI tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$projects = $env:UI_TEST_PROJECTS -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
foreach ($project in $projects)
|
||||
{
|
||||
dotnet test $project --no-build
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
# Release of NuGet packages (K22-CI-30, K22-CI-31, K22-REL-01..07). Called from the
|
||||
# calling repository's tag workflow; see templates/workflows/release-nuget.yml:
|
||||
#
|
||||
# on:
|
||||
# push:
|
||||
# tags: ['v*']
|
||||
# jobs:
|
||||
# release:
|
||||
# uses: KofTwentyTwo/standards/.github/workflows/release-nuget.yml@<sha> # vX.Y.Z
|
||||
# permissions:
|
||||
# contents: write # create the GitHub release
|
||||
# id-token: write # sign attestations (Sigstore)
|
||||
# attestations: write # store attestations
|
||||
# with:
|
||||
# solution: MySolution.slnx
|
||||
# coverage-packages: My.Library
|
||||
# test-projects: tests/My.Library.Tests
|
||||
# publish: # nuget.org push: must live in the CALLER's workflow file
|
||||
# needs: release
|
||||
# ...
|
||||
#
|
||||
# What this workflow does, in order:
|
||||
# build (Windows, read-only): validate the tag, locked restore, Release build with
|
||||
# warnings as errors, tests and coverage gate, dotnet pack with the tag version.
|
||||
# sbom (Linux, per package): CycloneDX 1.6 SBOM from the project's lock file
|
||||
# (Syft), attested against its package.
|
||||
# attest (Linux): SHA256SUMS over every asset, then SLSA build provenance for all
|
||||
# of them. Because the build runs here, in KofTwentyTwo/standards, the
|
||||
# provenance names this reusable workflow as the builder (SLSA Build L3).
|
||||
# release (Linux): draft GitHub release with generated notes and every asset, then
|
||||
# publish (releases are immutable once published).
|
||||
#
|
||||
# Publishing to nuget.org is deliberately NOT done here. nuget.org trusted publishing
|
||||
# matches the OIDC token's `repository` AND `job_workflow_ref` claims against the
|
||||
# package's own repository, so the push must run in a job defined in the calling
|
||||
# repository's workflow file (see the template). That job downloads the `packages`
|
||||
# artifact, verifies its attestation was signed by this workflow, then pushes.
|
||||
name: release-nuget
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
solution:
|
||||
description: Solution (.slnx or .sln) to build and pack.
|
||||
type: string
|
||||
required: true
|
||||
platform:
|
||||
description: Solution platform passed as -p:Platform.
|
||||
type: string
|
||||
default: x64
|
||||
runs-on:
|
||||
description: Runner for the build job (Windows for WinUI/WPF solutions).
|
||||
type: string
|
||||
default: windows-latest
|
||||
coverage-packages:
|
||||
description: Comma-separated coverage packages that must meet coverage-minimum. Empty disables the gate.
|
||||
type: string
|
||||
default: ""
|
||||
coverage-minimum:
|
||||
description: Minimum line coverage, in percent.
|
||||
type: number
|
||||
default: 100
|
||||
test-projects:
|
||||
description: Newline-separated test projects to run with coverage before packing.
|
||||
type: string
|
||||
default: ""
|
||||
outputs:
|
||||
version:
|
||||
description: The released version (tag without the leading v).
|
||||
value: ${{ jobs.build.outputs.version }}
|
||||
prerelease:
|
||||
description: '"true" when the version has a prerelease suffix.'
|
||||
value: ${{ jobs.build.outputs.prerelease }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
packages: ${{ steps.packages.outputs.packages }}
|
||||
env:
|
||||
K22_SOLUTION: ${{ inputs.solution }}
|
||||
K22_PLATFORM: ${{ inputs.platform }}
|
||||
TEST_PROJECTS: ${{ inputs.test-projects }}
|
||||
COVERAGE_PACKAGES: ${{ inputs.coverage-packages }}
|
||||
COVERAGE_MINIMUM: ${{ inputs.coverage-minimum }}
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Strict SemVer 2.0 tag; build metadata ('+...') is rejected (K22-REL-01).
|
||||
- name: Validate tag and derive version
|
||||
id: version
|
||||
shell: pwsh
|
||||
env:
|
||||
REF_TYPE: ${{ github.ref_type }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if ($env:REF_TYPE -ne 'tag')
|
||||
{
|
||||
Write-Output "::error::Releases run only for tags; got $env:REF_TYPE '$env:REF_NAME'."
|
||||
exit 1
|
||||
}
|
||||
$semver = '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$'
|
||||
if ($env:REF_NAME -cnotmatch $semver)
|
||||
{
|
||||
Write-Output "::error::Tag '$env:REF_NAME' is not vMAJOR.MINOR.PATCH with an optional prerelease suffix."
|
||||
exit 1
|
||||
}
|
||||
$version = $env:REF_NAME.Substring(1)
|
||||
$prerelease = $version.Contains('-').ToString().ToLowerInvariant()
|
||||
Write-Output "Version $version, prerelease $prerelease"
|
||||
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
"prerelease=$prerelease" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
- name: Set up .NET SDK
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
# No caches anywhere in this workflow (K22-CI-15): a cache written by a pull
|
||||
# request run must never reach a release.
|
||||
- name: Restore (locked)
|
||||
shell: pwsh
|
||||
run: dotnet restore $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --locked-mode
|
||||
|
||||
- name: Build (Release, warnings as errors)
|
||||
shell: pwsh
|
||||
env:
|
||||
K22_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: dotnet build $env:K22_SOLUTION -c Release "-p:Platform=$env:K22_PLATFORM" "-p:Version=$env:K22_VERSION" -warnaserror --no-restore
|
||||
|
||||
- name: Test with coverage
|
||||
if: inputs.test-projects != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$settings = @()
|
||||
if (Test-Path -LiteralPath coverage.runsettings) { $settings = @('--settings', 'coverage.runsettings') }
|
||||
$projects = $env:TEST_PROJECTS -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
foreach ($project in $projects)
|
||||
{
|
||||
dotnet test $project -c Release --no-build @settings --collect:"XPlat Code Coverage" --results-directory TestResults
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
- name: Coverage gate
|
||||
if: inputs.coverage-packages != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = @(Get-ChildItem -Path TestResults -Recurse -Filter coverage.cobertura.xml -ErrorAction SilentlyContinue)
|
||||
if ($files.Count -eq 0)
|
||||
{
|
||||
Write-Output '::error::No coverage.cobertura.xml under TestResults.'
|
||||
exit 1
|
||||
}
|
||||
$reports = foreach ($file in $files) { [xml] (Get-Content -LiteralPath $file.FullName -Raw) }
|
||||
$required = [double] $env:COVERAGE_MINIMUM / 100
|
||||
$failed = $false
|
||||
foreach ($name in ($env:COVERAGE_PACKAGES -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }))
|
||||
{
|
||||
$rates = foreach ($report in $reports)
|
||||
{
|
||||
$package = @($report.coverage.packages.package) | Where-Object { $_.name -eq $name }
|
||||
if ($package) { [double]::Parse($package.'line-rate', [Globalization.CultureInfo]::InvariantCulture) }
|
||||
}
|
||||
if (-not $rates)
|
||||
{
|
||||
Write-Output "::error::Package '$name' was not found in any coverage report."
|
||||
$failed = $true
|
||||
continue
|
||||
}
|
||||
$best = ($rates | Measure-Object -Maximum).Maximum
|
||||
Write-Output ('{0} line coverage: {1}% (required {2}%)' -f $name, [math]::Round($best * 100, 2), $env:COVERAGE_MINIMUM)
|
||||
if ($best -lt $required) { $failed = $true }
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
|
||||
- name: Pack
|
||||
shell: pwsh
|
||||
env:
|
||||
K22_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: dotnet pack $env:K22_SOLUTION -c Release "-p:Platform=$env:K22_PLATFORM" "-p:Version=$env:K22_VERSION" --no-build -o artifacts/nuget
|
||||
|
||||
# Maps each package to the project folder whose packages.lock.json describes its
|
||||
# dependencies, for the SBOM job. Package ids come from the produced file names.
|
||||
- name: List packages
|
||||
id: packages
|
||||
shell: pwsh
|
||||
env:
|
||||
K22_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
$suffix = ".$env:K22_VERSION.nupkg"
|
||||
$entries = foreach ($file in Get-ChildItem -Path artifacts/nuget -Filter '*.nupkg')
|
||||
{
|
||||
if (-not $file.Name.EndsWith($suffix, [StringComparison]::OrdinalIgnoreCase)) { continue }
|
||||
$id = $file.Name.Substring(0, $file.Name.Length - $suffix.Length)
|
||||
$project = Get-ChildItem -Path . -Recurse -Filter "$id.csproj" -File | Select-Object -First 1
|
||||
if (-not $project)
|
||||
{
|
||||
Write-Output "::error::No project file named $id.csproj for package $($file.Name)."
|
||||
exit 1
|
||||
}
|
||||
$directory = [IO.Path]::GetRelativePath((Get-Location).Path, $project.DirectoryName) -replace '\\', '/'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $project.DirectoryName 'packages.lock.json')))
|
||||
{
|
||||
Write-Output "::error::$directory has no packages.lock.json (K22-REPO-09); the SBOM needs it."
|
||||
exit 1
|
||||
}
|
||||
[ordered]@{ id = $id; package = $file.Name; directory = $directory }
|
||||
}
|
||||
$json = ConvertTo-Json -InputObject @($entries) -Compress
|
||||
Write-Output "Packages: $json"
|
||||
"packages=$json" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
- name: Upload packages
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: packages
|
||||
path: artifacts/nuget/*
|
||||
if-no-files-found: error
|
||||
|
||||
sbom:
|
||||
name: sbom (${{ matrix.package.id }})
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # sign the SBOM attestation
|
||||
attestations: write # store it
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
package: ${{ fromJSON(needs.build.outputs.packages) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: packages
|
||||
path: artifacts/nuget
|
||||
|
||||
- name: Prepare SBOM folder
|
||||
run: mkdir -p artifacts/sbom
|
||||
|
||||
# CycloneDX 1.6 from the project's packages.lock.json: the exact resolved graph
|
||||
# the package was built with (K22-REL-06).
|
||||
- name: Generate SBOM (Syft)
|
||||
uses: docker://ghcr.io/anchore/syft:v1.54.1@sha256:3eb5379ba7b409c3f4069b686110527af0c47df993fa5c10d13e7cf34f49b1aa
|
||||
with:
|
||||
args: >-
|
||||
scan dir:/github/workspace/${{ matrix.package.directory }}
|
||||
--source-name ${{ matrix.package.id }}
|
||||
--source-version ${{ needs.build.outputs.version }}
|
||||
--output cyclonedx-json@1.6=/github/workspace/artifacts/sbom/${{ matrix.package.id }}.${{ needs.build.outputs.version }}.cdx.json
|
||||
--quiet
|
||||
|
||||
- name: Attest SBOM
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
with:
|
||||
subject-path: artifacts/nuget/${{ matrix.package.package }}
|
||||
sbom-path: artifacts/sbom/${{ matrix.package.id }}.${{ needs.build.outputs.version }}.cdx.json
|
||||
|
||||
- name: Upload SBOM
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: sbom-${{ matrix.package.id }}
|
||||
path: artifacts/sbom/*.cdx.json
|
||||
if-no-files-found: error
|
||||
|
||||
attest:
|
||||
name: attest
|
||||
needs: [build, sbom]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # sign the provenance attestation
|
||||
attestations: write # store it
|
||||
steps:
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: packages
|
||||
path: release
|
||||
|
||||
- name: Download SBOMs
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: sbom-*
|
||||
path: release
|
||||
merge-multiple: true
|
||||
|
||||
- name: Checksums
|
||||
working-directory: release
|
||||
run: |
|
||||
find . -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort | xargs sha256sum > "$RUNNER_TEMP/SHA256SUMS"
|
||||
mv "$RUNNER_TEMP/SHA256SUMS" SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
# Anyone can verify an asset with:
|
||||
# gh attestation verify <file> --repo <owner/repo> --signer-repo KofTwentyTwo/standards
|
||||
- name: Attest build provenance
|
||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
||||
with:
|
||||
subject-path: release/*
|
||||
|
||||
- name: Upload release assets
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-assets
|
||||
path: release/*
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: release
|
||||
needs: [build, attest]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write # create and publish the GitHub release
|
||||
steps:
|
||||
- name: Download release assets
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: release-assets
|
||||
path: release
|
||||
|
||||
# Releases are immutable once published (K22-REL-07): everything lands on a draft
|
||||
# first, and publishing is the last step.
|
||||
- name: Create draft release with assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
K22_VERSION: ${{ needs.build.outputs.version }}
|
||||
PRERELEASE: ${{ needs.build.outputs.prerelease }}
|
||||
REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
run: |
|
||||
prerelease_flag=()
|
||||
if [[ "$PRERELEASE" == "true" ]]; then prerelease_flag=(--prerelease); fi
|
||||
gh release create "$TAG" --draft --verify-tag --generate-notes \
|
||||
--title "$REPOSITORY_NAME $K22_VERSION" "${prerelease_flag[@]}" release/*
|
||||
|
||||
- name: Publish release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: gh release edit "$TAG" --draft=false
|
||||
@@ -234,6 +234,8 @@ from each repository:
|
||||
| `pr.yml` | `pr / title`, `pr / dco`, `pr / dependency-review` | Conventional Commit PR title, DCO sign-off on every commit, new dependencies' vulnerabilities and licenses |
|
||||
| `security.yml` | `security / secrets`, `security / sca`, `security / workflows` | gitleaks over full history; Trivy vulnerabilities and OSV malicious-package advisories in the dependency graph; zizmor and actionlint on workflows |
|
||||
| `codeql.yml` | `codeql / analyze (<language>)` | CodeQL `security-extended`, failing on high severity and above |
|
||||
| `dotnet.yml` | `ci / build-test`, `ci / format`, `ci / ui-tests` | .NET locked restore, zero-warning build, tests, coverage gate, Kingsrook format check, FlaUI UI tests |
|
||||
| `release-nuget.yml` | (tag workflow) | NuGet release: gates, pack, SBOM, checksums, SLSA provenance, GitHub release; pairs with a caller `publish` job for nuget.org |
|
||||
|
||||
A caller looks like this:
|
||||
|
||||
|
||||
@@ -22,16 +22,23 @@ formatter cannot express (three blank lines between members).
|
||||
1. Copy the three files. Keep any repository-specific groups of an existing
|
||||
`Directory.Build.props` (version, package metadata) and replace its quality settings.
|
||||
2. Set `* text=auto eol=lf` in `.gitattributes` (K22-CODE-07).
|
||||
3. Restore once so `packages.lock.json` files are created, and commit them.
|
||||
4. Reformat and fix:
|
||||
3. Add a `global.json` pinning the SDK feature band (`"rollForward": "latestFeature"`);
|
||||
CI installs the SDK from it.
|
||||
4. Restore once so `packages.lock.json` files are created, and commit them. CI restores
|
||||
with `--locked-mode`, which fails on any drift from the lock files, and the release
|
||||
workflow builds each package's SBOM from them.
|
||||
5. Reformat and fix:
|
||||
|
||||
```powershell
|
||||
dotnet format # layout, var, usings, naming, line endings
|
||||
dotnet build -warnaserror # remaining analyzer findings
|
||||
```
|
||||
|
||||
5. Commit the reformat on its own (`style: adopt Kingsrook layout`) so `git blame` can
|
||||
6. Commit the reformat on its own (`style: adopt Kingsrook layout`) so `git blame` can
|
||||
skip it: add that commit's hash to `.git-blame-ignore-revs`.
|
||||
7. Add the CI and release callers from
|
||||
[`templates/workflows/dotnet-ci.yml`](../../templates/workflows/dotnet-ci.yml) and
|
||||
[`templates/workflows/release-nuget.yml`](../../templates/workflows/release-nuget.yml).
|
||||
|
||||
## IDEs
|
||||
|
||||
|
||||
+12
-4
@@ -19,7 +19,10 @@ requires all of them:
|
||||
| `security / sca` | No HIGH or CRITICAL vulnerability, and no package flagged as malicious (OSV `MAL-` advisories), in the resolved dependency graph | `security.yml` |
|
||||
| `security / workflows` | Workflow files pass zizmor and actionlint | `security.yml` |
|
||||
| `codeql / analyze (<language>)` | No CodeQL alert at *high* severity or above | `codeql.yml` |
|
||||
| Language checks, e.g. `ci / build-test`, `ci / format` | Zero-warning build, tests, coverage gate, format | language workflow |
|
||||
| `ci / build-test` (.NET) | Locked restore, zero-warning build, tests, coverage gate | `dotnet.yml` |
|
||||
| `ci / format` (.NET) | `dotnet format --verify-no-changes --severity warn`: Kingsrook layout, style, analyzers | `dotnet.yml` |
|
||||
| `ci / ui-tests` (.NET apps with UI tests) | FlaUI end-to-end tests against the built app | `dotnet.yml` |
|
||||
| Other languages | The language profile's build, test, coverage, and format commands | language workflow |
|
||||
|
||||
*Why:* the merge gate is the same in every repository and cannot be skipped.
|
||||
*Verified by:* conformance checker (required contexts in the ruleset). *Maps to:*
|
||||
@@ -127,10 +130,15 @@ badge.
|
||||
|
||||
**K22-CI-30 (MUST)** Releases are built only by a workflow triggered by a protected
|
||||
`v*` tag, running on a GitHub-hosted runner, through a **reusable workflow** from
|
||||
`KofTwentyTwo/standards`, so the build platform and its definition are isolated from
|
||||
the calling repository.
|
||||
`KofTwentyTwo/standards` (`release-nuget.yml` for NuGet packages), so the build
|
||||
platform and its definition are isolated from the calling repository. Publishing to a
|
||||
registry whose trusted publishing binds the token to the calling repository's own
|
||||
workflow file (nuget.org does) runs in a job of the caller's tag workflow, after it has
|
||||
verified the artifacts' attestations were signed by the reusable workflow.
|
||||
*Why:* this is what makes provenance trustworthy (SLSA Build L3). *Verified by:*
|
||||
provenance `builder.id` names the reusable workflow. See [releases](releases.md).
|
||||
provenance `builder.id` names the reusable workflow; conformance checker (the tag
|
||||
workflow calls `KofTwentyTwo/standards/.github/workflows/release-*.yml` pinned by SHA).
|
||||
See [releases](releases.md).
|
||||
|
||||
**K22-CI-31 (MUST)** A release re-runs every gate that guards `main` (build, tests,
|
||||
coverage, security scans) before publishing anything.
|
||||
|
||||
@@ -78,6 +78,32 @@ protected `release` environment.
|
||||
*Why:* no long-lived publishing credential to steal. *Verified by:*
|
||||
[`K22-CI-16`](ci-cd.md#workflow-security).
|
||||
|
||||
### .NET packages
|
||||
|
||||
NuGet packages are released by the shared
|
||||
[`release-nuget.yml`](../.github/workflows/release-nuget.yml) workflow, called from the
|
||||
repository's tag workflow ([template](../templates/workflows/release-nuget.yml)). It
|
||||
builds, tests, packs, writes a CycloneDX SBOM per package from its lock file, attests
|
||||
provenance and SBOMs, and publishes the GitHub release. The template's `publish` job
|
||||
then pushes to nuget.org.
|
||||
|
||||
nuget.org trusted publishing compares the OIDC token's `repository` and
|
||||
`job_workflow_ref` claims with the policy. `job_workflow_ref` names the workflow file
|
||||
that *defines the job*, so the push cannot run inside the reusable workflow (that file
|
||||
lives in `KofTwentyTwo/standards`); it runs in the calling repository's own
|
||||
`release.yml`, after verifying the packages' attestations. The policy is an owner
|
||||
action, made once per repository on nuget.org (profile → Trusted Publishing):
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Repository Owner | `KofTwentyTwo` |
|
||||
| Repository | the repository name, e.g. `AppKit` |
|
||||
| Workflow File | `release.yml` (the calling workflow's file name) |
|
||||
| Environment | `release` |
|
||||
|
||||
Then set the repository variable `NUGET_USER` to the nuget.org profile name. Until the
|
||||
variable is set, the `publish` job is skipped and the release stays GitHub-only.
|
||||
|
||||
## Release process
|
||||
|
||||
**K22-REL-12 (MUST)** Each MINOR or MAJOR release has a release checklist (an issue made
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# .NET build, test, coverage, and format gates for a KofTwentyTwo repository
|
||||
# (K22-CI-01, K22-CI-02). Copy to .github/workflows/ci.yml and fill the inputs.
|
||||
#
|
||||
# Produces the required contexts `ci / build-test` and `ci / format` (and
|
||||
# `ci / ui-tests` when ui-test-projects is set). Reusable workflows are pinned by full
|
||||
# commit SHA (K22-CI-11): replace {{STANDARDS_SHA}} with the commit of the standards
|
||||
# release you conform to; Dependabot's github-actions ecosystem keeps it current.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: KofTwentyTwo/standards/.github/workflows/dotnet.yml@{{STANDARDS_SHA}} # {{STANDARDS_VERSION}}
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
solution: "{{SOLUTION}}"
|
||||
platform: x64
|
||||
coverage-packages: "{{COVERAGE_PACKAGES}}"
|
||||
test-projects: |
|
||||
{{TEST_PROJECT}}
|
||||
ui-test-projects: |
|
||||
{{UI_TEST_PROJECT}}
|
||||
@@ -0,0 +1,76 @@
|
||||
# NuGet package release for a KofTwentyTwo repository (K22-CI-30, K22-REL-*).
|
||||
# Copy to .github/workflows/release.yml. Pushing a protected v* tag builds, tests,
|
||||
# packs, generates SBOMs, attests, and publishes the GitHub release through the shared
|
||||
# reusable workflow, then pushes the packages to nuget.org.
|
||||
#
|
||||
# nuget.org trusted publishing (one-time owner setup, no API key stored anywhere):
|
||||
# nuget.org → your profile → Trusted Publishing → Add policy:
|
||||
# Repository Owner: {{OWNER}}
|
||||
# Repository: {{REPOSITORY}}
|
||||
# Workflow File: release.yml (THIS file's name, not the reusable workflow's)
|
||||
# Environment: release
|
||||
# then set the repository variable NUGET_USER to your nuget.org profile name.
|
||||
# The publish job must stay in this file: nuget.org checks that the OIDC token's
|
||||
# job_workflow_ref belongs to this repository, which a job inside the reusable
|
||||
# workflow (in KofTwentyTwo/standards) can never satisfy.
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: KofTwentyTwo/standards/.github/workflows/release-nuget.yml@{{STANDARDS_SHA}} # {{STANDARDS_VERSION}}
|
||||
permissions:
|
||||
contents: write # create and publish the GitHub release
|
||||
id-token: write # sign attestations
|
||||
attestations: write # store attestations
|
||||
with:
|
||||
solution: "{{SOLUTION}}"
|
||||
platform: x64
|
||||
coverage-packages: "{{COVERAGE_PACKAGES}}"
|
||||
test-projects: |
|
||||
{{TEST_PROJECT}}
|
||||
|
||||
publish:
|
||||
name: publish (nuget.org)
|
||||
needs: release
|
||||
if: vars.NUGET_USER != ''
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
attestations: read # verify the attestations before pushing
|
||||
id-token: write # trusted publishing token exchange
|
||||
steps:
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: packages
|
||||
path: packages
|
||||
|
||||
# Push only bytes that the shared release workflow built and attested.
|
||||
- name: Verify attestations
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
for package in packages/*.nupkg; do
|
||||
gh attestation verify "$package" --repo "$REPOSITORY" --signer-repo KofTwentyTwo/standards
|
||||
done
|
||||
|
||||
- name: NuGet login (OIDC to a one-hour API key)
|
||||
id: login
|
||||
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
|
||||
with:
|
||||
user: ${{ vars.NUGET_USER }}
|
||||
|
||||
- name: Push to nuget.org
|
||||
env:
|
||||
NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}
|
||||
run: dotnet nuget push "packages/*.nupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
@@ -317,6 +317,31 @@ function Get-MissingLocks
|
||||
}
|
||||
|
||||
# Any of these makes a repository buildable software rather than documentation (K22-REPO-02).
|
||||
# True when workflow text calls a KofTwentyTwo/standards reusable release workflow
|
||||
# pinned to a full commit SHA (K22-CI-30, K22-CI-11), or calls it by local path inside
|
||||
# the standards repository itself.
|
||||
function Test-StandardsReleaseCall
|
||||
{
|
||||
param([string] $Text)
|
||||
return [bool] ($Text -match '(?m)^\s*(?:-\s+)?uses:\s*["'']?(KofTwentyTwo/standards/\.github/workflows/release-[A-Za-z0-9_.-]+\.ya?ml@[0-9a-f]{40}|\./\.github/workflows/release-[A-Za-z0-9_.-]+\.ya?ml)')
|
||||
}
|
||||
|
||||
# Status contexts the protect-main ruleset must require for a repository with these
|
||||
# files (K22-CI-01): the shared gates, plus the .NET gates when there is a solution
|
||||
# or project file.
|
||||
function Get-RequiredContexts
|
||||
{
|
||||
param([string[]] $Paths)
|
||||
$contexts = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($context in $script:RequiredContexts) { $contexts.Add($context) }
|
||||
if (@($Paths | Where-Object { $_ -match '(^|/)[^/]+\.(slnx?|csproj)$' }).Count -gt 0)
|
||||
{
|
||||
$contexts.Add('ci / build-test')
|
||||
$contexts.Add('ci / format')
|
||||
}
|
||||
return , $contexts.ToArray()
|
||||
}
|
||||
|
||||
$script:BuildManifestPattern = '(^|/)([^/]+\.(slnx?|csproj|fsproj|vbproj)|package\.json|pyproject\.toml|Cargo\.toml|go\.mod|pom\.xml|build\.gradle(\.kts)?|Package\.swift)$'
|
||||
|
||||
$script:BinaryPattern = '\.(exe|dll|so|dylib|a|lib|pdb|o|obj|class|jar|war|ear|pyc|nupkg|snupkg|msi|msix|appx|appxbundle|zip|7z|rar|tar|gz|tgz|bz2|xz|dmg|pkg|deb|rpm|apk|ipa|wasm)$'
|
||||
@@ -397,6 +422,17 @@ jobs:
|
||||
Assert (@(Get-MissingLocks @('samples/x/package.json')).Count -eq 0) 'locks: samples ignored'
|
||||
Assert (@(Get-MissingLocks @('Cargo.toml', 'Cargo.lock', 'go.mod')) -contains 'Go') 'locks: go'
|
||||
|
||||
$sha = '0123456789abcdef0123456789abcdef01234567'
|
||||
Assert (Test-StandardsReleaseCall "jobs:`n release:`n uses: KofTwentyTwo/standards/.github/workflows/release-nuget.yml@$sha # v1.0.0") 'release call: pinned'
|
||||
Assert (-not (Test-StandardsReleaseCall " uses: KofTwentyTwo/standards/.github/workflows/release-nuget.yml@main")) 'release call: branch ref'
|
||||
Assert (-not (Test-StandardsReleaseCall " uses: KofTwentyTwo/standards/.github/workflows/dotnet.yml@$sha")) 'release call: not a release workflow'
|
||||
Assert (-not (Test-StandardsReleaseCall " uses: Someone/standards/.github/workflows/release-nuget.yml@$sha")) 'release call: other owner'
|
||||
Assert (Test-StandardsReleaseCall " uses: ./.github/workflows/release-nuget.yml") 'release call: local in standards'
|
||||
Assert ((Get-RequiredContexts @('AppKit.slnx', 'README.md')) -contains 'ci / build-test') 'contexts: dotnet build-test'
|
||||
Assert ((Get-RequiredContexts @('src/a/a.csproj')) -contains 'ci / format') 'contexts: dotnet format'
|
||||
Assert (-not ((Get-RequiredContexts @('README.md')) -contains 'ci / build-test')) 'contexts: docs repo'
|
||||
Assert ((Get-RequiredContexts @('README.md')) -contains 'security / sca') 'contexts: shared'
|
||||
|
||||
Assert ('src/App/App.csproj' -match $script:BuildManifestPattern) 'manifest: csproj'
|
||||
Assert ('AppKit.slnx' -match $script:BuildManifestPattern) 'manifest: slnx'
|
||||
Assert ('tools/x/package.json' -match $script:BuildManifestPattern) 'manifest: nested package.json'
|
||||
@@ -736,8 +772,12 @@ else
|
||||
|
||||
# K22-CI-30: release only through the standards' reusable release workflow.
|
||||
if ($releaseFlows.Count -eq 0) { Add-Result 'K22-CI-30' 'N/A' 'no tag-triggered release workflow' }
|
||||
elseif (Test-Calls 'release[^/]*') { Add-Result 'K22-CI-30' 'PASS' 'releases through a KofTwentyTwo/standards reusable workflow' }
|
||||
else { Add-Result 'K22-CI-30' 'FAIL' ('release workflow builds in the calling repository, not through a standards reusable workflow (no SLSA Build L3): ' + ($releaseFlows -join ', ')) }
|
||||
else
|
||||
{
|
||||
$direct = @($releaseFlows | Where-Object { -not (Test-StandardsReleaseCall $workflows[$_]) })
|
||||
if ($direct.Count -eq 0) { Add-Result 'K22-CI-30' 'PASS' 'releases through a SHA-pinned KofTwentyTwo/standards release-*.yml reusable workflow' }
|
||||
else { Add-Result 'K22-CI-30' 'FAIL' ('tag workflow does not call a SHA-pinned KofTwentyTwo/standards release-*.yml (no SLSA Build L3): ' + ($direct -join ', ')) }
|
||||
}
|
||||
}
|
||||
|
||||
Add-Result 'K22-CI-02' 'REVIEW' 'tests run on every PR and push: confirm in the language workflow'
|
||||
@@ -816,7 +856,7 @@ else
|
||||
|
||||
# K22-CI-01: required contexts.
|
||||
$contexts = if ($rules.ContainsKey('required_status_checks')) { @($rules['required_status_checks'].parameters.required_status_checks | ForEach-Object context) } else { @() }
|
||||
$missingContexts = @($script:RequiredContexts | Where-Object { $contexts -notcontains $_ })
|
||||
$missingContexts = @((Get-RequiredContexts $script:Paths) | Where-Object { $contexts -notcontains $_ })
|
||||
foreach ($prefix in $script:RequiredContextPrefixes) { if (-not ($contexts | Where-Object { $_.StartsWith($prefix) })) { $missingContexts += "$prefix (...)" } }
|
||||
if ($missingContexts) { Add-Result 'K22-CI-01' 'FAIL' ('protect-main does not require: ' + ($missingContexts -join ', ') + $(if ($contexts) { " (requires: $($contexts -join ', '))" } else { '' })) }
|
||||
else { Add-Result 'K22-CI-01' 'PASS' "requires $($contexts.Count) checks including every standard context" }
|
||||
|
||||
Reference in new issue
Block a user