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
6 changes: 5 additions & 1 deletion .github/REPOSITORY_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ the `ooples` NuGet account with these values:

Enter only the workflow filename in NuGet, not the `.github/workflows/` path. The publish job requests a single-use
OIDC token immediately before publication and exchanges it for a short-lived NuGet API key through `NuGet/login`.
The workflow never stores a long-lived NuGet credential.
The workflow never stores a long-lived NuGet credential. Package provenance runs through the separately named
`attest-release.yml` reusable workflow. NuGet validates GitHub's `job_workflow_ref` claim, so that workflow's OIDC
token cannot satisfy the trusted-publishing policy for `automated-release.yml`; only the publish job in the trusted
workflow can obtain a NuGet credential. Verified package artifacts are immutable and retained for 30 days so a
publication outage can be retried without rebuilding different package bytes.

## Repository services

Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/attest-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Attest Release Packages

on:
workflow_call:
inputs:
artifact-name:
description: Immutable package artifact produced by the release workflow
required: true
type: string

jobs:
attest:
name: Attest NuGet packages
permissions:
contents: read
id-token: write
attestations: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ inputs.artifact-name }}
path: out
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
out/*.nupkg
out/*.snupkg
30 changes: 20 additions & 10 deletions .github/workflows/automated-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
dotnet-version: '10.0.x'
dotnet-quality: preview
- run: dotnet restore AiDotNet.Evolution.slnx
- name: Verify release workflow security contract
shell: pwsh
run: ./eng/Test-ReleaseWorkflow.ps1 -NoRestore
- run: dotnet build src/AiDotNet.Evolution/AiDotNet.Evolution.csproj --configuration Release --no-restore
- run: dotnet test tests/AiDotNet.Evolution.Tests/AiDotNet.Evolution.Tests.csproj --configuration Release --framework net10.0 --no-restore --verbosity normal
- run: dotnet test tests/AiDotNet.Evolution.Tests/AiDotNet.Evolution.Tests.csproj --configuration Release --framework net8.0 --no-restore --verbosity normal
Expand All @@ -76,11 +79,9 @@ jobs:
- run: dotnet test tests/AiDotNet.Evolution.Tests/AiDotNet.Evolution.Tests.csproj --configuration Release --framework net471 --no-restore --verbosity normal

pack:
name: Pack, sign, and attest
name: Pack and sign
permissions:
contents: read
id-token: write
attestations: write
needs: [validate, legacy]
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down Expand Up @@ -132,26 +133,35 @@ jobs:
finally {
Remove-Item -LiteralPath $certificate -Force -ErrorAction SilentlyContinue
}
- name: Attest package provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
out/*.nupkg
out/*.snupkg
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: nuget-packages-${{ github.sha }}
path: |
out/*.nupkg
out/*.snupkg
if-no-files-found: error
retention-days: 30

attest:
name: Attest package provenance
# Keep attestation OIDC in a differently named reusable workflow. NuGet matches
# job_workflow_ref, so this token cannot satisfy the automated-release.yml policy.
permissions:
contents: read
id-token: write
attestations: write
needs: pack
uses: ./.github/workflows/attest-release.yml
with:
artifact-name: nuget-packages-${{ github.sha }}

publish:
name: Publish to NuGet
# This is the only runner job whose OIDC identity matches the NuGet policy.
permissions:
contents: read
id-token: write
needs: [validate, pack]
needs: [validate, pack, attest]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
run: dotnet restore AiDotNet.Evolution.slnx
- name: Verify formatting
run: dotnet format AiDotNet.Evolution.slnx --no-restore --verify-no-changes
- name: Verify release workflow security contract
shell: pwsh
run: ./eng/Test-ReleaseWorkflow.ps1 -NoRestore
- name: Build all package target frameworks
run: dotnet build src/AiDotNet.Evolution/AiDotNet.Evolution.csproj --configuration Release --no-restore
- name: Test net10.0 with coverage
Expand Down
22 changes: 22 additions & 0 deletions eng/Test-ReleaseWorkflow.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[CmdletBinding()]
param([switch] $NoRestore)

$ErrorActionPreference = 'Stop'

$repositoryRoot = Split-Path -Parent $PSScriptRoot
$testProject = Join-Path $repositoryRoot 'tests/AiDotNet.Evolution.Tests/AiDotNet.Evolution.Tests.csproj'
$arguments = @(
'test',
$testProject,
'--configuration', 'Release',
'--framework', 'net10.0',
'--filter', 'FullyQualifiedName~ReleaseWorkflowSecurityContractTests',
'--verbosity', 'minimal')
if ($NoRestore) {
$arguments += '--no-restore'
}

& dotnet @arguments
if ($LASTEXITCODE -ne 0) {
throw 'Release workflow security contract tests failed.'
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="YamlDotNet" Version="18.1.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
Loading
Loading