From c00b9341f0664c72a26f5c29fef337a35b8a1d5e Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 9 Oct 2026 09:56:38 -0500 Subject: [PATCH 1/2] feat(ci): reusable .NET CI and NuGet release workflows - dotnet.yml: locked restore, zero-warning build, tests with a per-package coverage gate, Kingsrook format check (--severity warn), optional FlaUI UI tests and pack; contexts ci / build-test, ci / format, ci / ui-tests. - release-nuget.yml: tag validation, Release gates, pack, CycloneDX 1.6 SBOM per package from its lock file (Syft, digest-pinned), SHA256SUMS, SLSA build provenance and SBOM attestations, draft-then-publish GitHub release. - nuget.org trusted publishing validates job_workflow_ref against the package's own repository, so the push runs in a caller job (template) that first verifies the attestations with --signer-repo KofTwentyTwo/standards. - Caller templates, docs (ci-cd, releases, README, configs/dotnet), and the conformance checker: CI-30 requires a SHA-pinned standards release-*.yml call, CI-01 expects the .NET contexts when a solution exists. Co-Authored-By: Claude Opus 5.5 Signed-off-by: James Maes --- .github/workflows/dotnet.yml | 257 ++++++++++++++++++ .github/workflows/release-nuget.yml | 365 ++++++++++++++++++++++++++ README.md | 2 + configs/dotnet/README.md | 13 +- standards/ci-cd.md | 16 +- standards/releases.md | 26 ++ templates/workflows/dotnet-ci.yml | 33 +++ templates/workflows/release-nuget.yml | 76 ++++++ tools/Test-RepoConformance.ps1 | 46 +++- 9 files changed, 824 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/dotnet.yml create mode 100644 .github/workflows/release-nuget.yml create mode 100644 templates/workflows/dotnet-ci.yml create mode 100644 templates/workflows/release-nuget.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..f1dd920 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,257 @@ +# .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@ # 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 -p:Platform=x64 --locked-mode +# dotnet build -p:Platform=x64 -warnaserror --no-restore +# dotnet test --no-build --collect:"XPlat Code Coverage" +# dotnet format --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: + SOLUTION: ${{ inputs.solution }} + 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:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + + # -warnaserror also fails on NuGetAudit vulnerability warnings. + - name: Build (warnings as errors) + shell: pwsh + run: dotnet build $env:SOLUTION "-p:Platform=$env: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:SOLUTION -c Release "-p:Platform=$env: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: + SOLUTION: ${{ inputs.solution }} + 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:SOLUTION "-p:Platform=$env: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. + - name: Verify formatting (whitespace, style, analyzers) + shell: pwsh + run: dotnet format $env: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: + SOLUTION: ${{ inputs.solution }} + 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:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + + - name: Build + shell: pwsh + run: dotnet build $env:SOLUTION "-p:Platform=$env: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 } + } diff --git a/.github/workflows/release-nuget.yml b/.github/workflows/release-nuget.yml new file mode 100644 index 0000000..28dad2b --- /dev/null +++ b/.github/workflows/release-nuget.yml @@ -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@ # 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: + SOLUTION: ${{ inputs.solution }} + 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:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + + - name: Build (Release, warnings as errors) + shell: pwsh + env: + VERSION: ${{ steps.version.outputs.version }} + run: dotnet build $env:SOLUTION -c Release "-p:Platform=$env:PLATFORM" "-p:Version=$env: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: + VERSION: ${{ steps.version.outputs.version }} + run: dotnet pack $env:SOLUTION -c Release "-p:Platform=$env:PLATFORM" "-p:Version=$env: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: + VERSION: ${{ steps.version.outputs.version }} + run: | + $suffix = ".$env: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 --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 }} + 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 $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 diff --git a/README.md b/README.md index c40c89d..204ff9b 100644 --- a/README.md +++ b/README.md @@ -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 ()` | 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: diff --git a/configs/dotnet/README.md b/configs/dotnet/README.md index f2f48f5..f109605 100644 --- a/configs/dotnet/README.md +++ b/configs/dotnet/README.md @@ -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 diff --git a/standards/ci-cd.md b/standards/ci-cd.md index e539605..7221a67 100644 --- a/standards/ci-cd.md +++ b/standards/ci-cd.md @@ -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 ()` | 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. diff --git a/standards/releases.md b/standards/releases.md index 420251d..ae403f3 100644 --- a/standards/releases.md +++ b/standards/releases.md @@ -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 diff --git a/templates/workflows/dotnet-ci.yml b/templates/workflows/dotnet-ci.yml new file mode 100644 index 0000000..1bfd44e --- /dev/null +++ b/templates/workflows/dotnet-ci.yml @@ -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}} diff --git a/templates/workflows/release-nuget.yml b/templates/workflows/release-nuget.yml new file mode 100644 index 0000000..6d50ef4 --- /dev/null +++ b/templates/workflows/release-nuget.yml @@ -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 diff --git a/tools/Test-RepoConformance.ps1 b/tools/Test-RepoConformance.ps1 index 0bc32d5..bd27e87 100644 --- a/tools/Test-RepoConformance.ps1 +++ b/tools/Test-RepoConformance.ps1 @@ -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" } -- 2.54.0 From cf94d3044f6fab63433d2e89f6ae9da9c161328b Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 9 Oct 2026 10:23:19 -0500 Subject: [PATCH 2/2] fix(ci): keep MSBuild-colliding env vars out of dotnet steps; build before format - MSBuild turns environment variables into properties, so PLATFORM=x64 made dotnet test look under bin/x64/ while the solution build mapped test projects to AnyCPU (bin/Debug/). SOLUTION, PLATFORM, and VERSION are now K22_-prefixed. - The release title step still read $VERSION after the rename; fixed. - dotnet format runs without XAML compilation, so XAML event handlers looked unused (IDE0051); the format job now builds first. Found by AppKit's adoption PR, the first real run of these workflows. Co-Authored-By: Claude Opus 5.5 Signed-off-by: James Maes --- .github/workflows/dotnet.yml | 34 ++++++++++++++++++----------- .github/workflows/release-nuget.yml | 22 +++++++++---------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f1dd920..bbd4052 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -71,8 +71,10 @@ jobs: permissions: contents: read env: - SOLUTION: ${{ inputs.solution }} - PLATFORM: ${{ inputs.platform }} + # 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 }} @@ -93,12 +95,12 @@ jobs: # the build can only use the dependency graph that was reviewed and scanned. - name: Restore (locked) shell: pwsh - run: dotnet restore $env:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + 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:SOLUTION "-p:Platform=$env:PLATFORM" -warnaserror --no-restore + run: dotnet build $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" -warnaserror --no-restore - name: Test with coverage if: inputs.test-projects != '' @@ -170,7 +172,7 @@ jobs: - name: Pack (Release) if: inputs.pack shell: pwsh - run: dotnet pack $env:SOLUTION -c Release "-p:Platform=$env:PLATFORM" -o artifacts/nuget + run: dotnet pack $env:K22_SOLUTION -c Release "-p:Platform=$env:K22_PLATFORM" -o artifacts/nuget - name: Upload packages if: inputs.pack @@ -187,8 +189,8 @@ jobs: permissions: contents: read env: - SOLUTION: ${{ inputs.solution }} - PLATFORM: ${{ inputs.platform }} + K22_SOLUTION: ${{ inputs.solution }} + K22_PLATFORM: ${{ inputs.platform }} DOTNET_CLI_TELEMETRY_OPTOUT: "1" DOTNET_NOLOGO: "1" steps: @@ -204,14 +206,20 @@ jobs: - name: Restore (locked) shell: pwsh - run: dotnet restore $env:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + 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:SOLUTION --verify-no-changes --severity warn --no-restore + run: dotnet format $env:K22_SOLUTION --verify-no-changes --severity warn --no-restore ui-tests: name: ui-tests @@ -221,8 +229,8 @@ jobs: permissions: contents: read env: - SOLUTION: ${{ inputs.solution }} - PLATFORM: ${{ inputs.platform }} + K22_SOLUTION: ${{ inputs.solution }} + K22_PLATFORM: ${{ inputs.platform }} UI_TEST_PROJECTS: ${{ inputs.ui-test-projects }} DOTNET_CLI_TELEMETRY_OPTOUT: "1" DOTNET_NOLOGO: "1" @@ -239,11 +247,11 @@ jobs: - name: Restore (locked) shell: pwsh - run: dotnet restore $env:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + run: dotnet restore $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --locked-mode - name: Build shell: pwsh - run: dotnet build $env:SOLUTION "-p:Platform=$env:PLATFORM" --no-restore + 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 diff --git a/.github/workflows/release-nuget.yml b/.github/workflows/release-nuget.yml index 28dad2b..f9bf390 100644 --- a/.github/workflows/release-nuget.yml +++ b/.github/workflows/release-nuget.yml @@ -86,8 +86,8 @@ jobs: prerelease: ${{ steps.version.outputs.prerelease }} packages: ${{ steps.packages.outputs.packages }} env: - SOLUTION: ${{ inputs.solution }} - PLATFORM: ${{ inputs.platform }} + K22_SOLUTION: ${{ inputs.solution }} + K22_PLATFORM: ${{ inputs.platform }} TEST_PROJECTS: ${{ inputs.test-projects }} COVERAGE_PACKAGES: ${{ inputs.coverage-packages }} COVERAGE_MINIMUM: ${{ inputs.coverage-minimum }} @@ -133,13 +133,13 @@ jobs: # request run must never reach a release. - name: Restore (locked) shell: pwsh - run: dotnet restore $env:SOLUTION "-p:Platform=$env:PLATFORM" --locked-mode + run: dotnet restore $env:K22_SOLUTION "-p:Platform=$env:K22_PLATFORM" --locked-mode - name: Build (Release, warnings as errors) shell: pwsh env: - VERSION: ${{ steps.version.outputs.version }} - run: dotnet build $env:SOLUTION -c Release "-p:Platform=$env:PLATFORM" "-p:Version=$env:VERSION" -warnaserror --no-restore + 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 != '' @@ -189,8 +189,8 @@ jobs: - name: Pack shell: pwsh env: - VERSION: ${{ steps.version.outputs.version }} - run: dotnet pack $env:SOLUTION -c Release "-p:Platform=$env:PLATFORM" "-p:Version=$env:VERSION" --no-build -o artifacts/nuget + 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. @@ -198,9 +198,9 @@ jobs: id: packages shell: pwsh env: - VERSION: ${{ steps.version.outputs.version }} + K22_VERSION: ${{ steps.version.outputs.version }} run: | - $suffix = ".$env:VERSION.nupkg" + $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 } @@ -348,14 +348,14 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} TAG: ${{ github.ref_name }} - VERSION: ${{ needs.build.outputs.version }} + 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 $VERSION" "${prerelease_flag[@]}" release/* + --title "$REPOSITORY_NAME $K22_VERSION" "${prerelease_flag[@]}" release/* - name: Publish release env: -- 2.54.0