Skip to content

release

release #7

Workflow file for this run

name: release
on:
workflow_dispatch:
inputs:
version:
description: Release version to publish
required: true
type: string
publish:
description: Publish to NuGet when all gates pass
required: true
type: boolean
default: false
channel:
description: Release channel to evaluate
required: true
type: choice
default: dry-run
options:
- dry-run
- prerelease
- stable
confirmation:
description: Type "publish Sigtran.NET <version>" for stable publication
required: false
type: string
push:
tags:
- 'v*'
permissions:
contents: read
id-token: write
attestations: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
SIGTRAN_SUPPLY_CHAIN: true
SIGTRAN_SUPPLY_CHAIN_ARTIFACT_ROOT: artifacts/supply-chain
SIGTRAN_RELEASE_EVIDENCE: true
SIGTRAN_RELEASE_EVIDENCE_ROOT: artifacts/release-evidence
SIGTRAN_DRY_RUN_ARTIFACT_ROOT: artifacts/release-dry-run
TIMESTAMP_AUTHORITY: ${{ vars.TIMESTAMP_AUTHORITY || 'https://timestamp.sectigo.com' }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
- name: Resolve Version
shell: bash
run: |
release_version="${{ inputs.version }}"
if [ -z "$release_version" ]; then
release_version="${GITHUB_REF_NAME#v}"
fi
release_channel="${{ inputs.channel }}"
if [ -z "$release_channel" ]; then
release_channel="stable"
fi
test -n "$release_version"
echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV"
echo "SIGTRAN_RELEASE_CHANNEL=$release_channel" >> "$GITHUB_ENV"
- name: Restore
run: dotnet restore src/Sigtran.NET.sln
- name: Build
run: dotnet build src/Sigtran.NET.sln --configuration Release --no-restore
- name: Test
run: dotnet run --project src/Sigtran.NET.Tests/Sigtran.NET.Tests.csproj --configuration Release --no-build
- name: Generate Current Public API
shell: pwsh
run: |
./eng/generate-public-api-baseline.ps1 `
-OutputPath "artifacts/supply-chain/api/Sigtran.NET.$env:RELEASE_VERSION.public-api.txt"
- name: Compare Public API Baseline
shell: pwsh
run: |
$arguments = @{
CurrentPath = "artifacts/supply-chain/api/Sigtran.NET.$env:RELEASE_VERSION.public-api.txt"
OutputPath = "artifacts/supply-chain/api/Sigtran.NET.$env:RELEASE_VERSION.api-diff.md"
}
if ($env:SIGTRAN_RELEASE_CHANNEL -eq "stable") {
$arguments.FailOnAnyChange = $true
}
else {
$arguments.FailOnBreaking = $true
}
./eng/compare-public-api.ps1 @arguments
- name: Evaluate Stable Release Readiness
if: ${{ github.event_name != 'workflow_dispatch' || inputs.channel == 'stable' }}
shell: pwsh
run: |
./eng/evaluate-stable-release.ps1 `
-ExpectedVersion "$env:RELEASE_VERSION" `
-JsonOutputPath "artifacts/release-evidence/stable-release-decision.json" `
-MarkdownOutputPath "artifacts/release-evidence/stable-release-decision.md"
- name: Enforce Stable Release Readiness
if: ${{ github.event_name != 'workflow_dispatch' || (inputs.channel == 'stable' && inputs.publish == true) }}
shell: pwsh
run: |
if ("${{ github.event_name }}" -eq "workflow_dispatch" -and
"${{ inputs.confirmation }}" -ne "publish Sigtran.NET $env:RELEASE_VERSION") {
throw "Stable publication confirmation does not match the requested version."
}
./eng/evaluate-stable-release.ps1 `
-ExpectedVersion "$env:RELEASE_VERSION" `
-JsonOutputPath "artifacts/release-evidence/stable-release-decision.json" `
-MarkdownOutputPath "artifacts/release-evidence/stable-release-decision.md" `
-RequireGo
- name: Pack
shell: bash
run: dotnet pack src/Sigtran.NET/Sigtran.NET.csproj --configuration Release --no-build /p:Version="$RELEASE_VERSION"
- name: Dry-Run Release Evidence
shell: bash
run: |
dry_run_dir="$SIGTRAN_DRY_RUN_ARTIFACT_ROOT/$RELEASE_VERSION"
mkdir -p "$dry_run_dir"
set +e
dotnet nuget verify "src/Sigtran.NET/bin/Release/Sigtran.NET.$RELEASE_VERSION.nupkg" --all > "$dry_run_dir/package-verification.md" 2>&1
verify_rc=$?
set -e
{
echo "version=$RELEASE_VERSION"
echo "channel=$SIGTRAN_RELEASE_CHANNEL"
echo "package=src/Sigtran.NET/bin/Release/Sigtran.NET.$RELEASE_VERSION.nupkg"
echo "symbols=src/Sigtran.NET/bin/Release/Sigtran.NET.$RELEASE_VERSION.snupkg"
echo "publication=disabled-during-dry-run"
echo "signatureVerificationExitCode=$verify_rc"
echo "signatureVerificationNote=unsigned dry-run packages are retained as release evidence; trusted timestamped signing is enforced in the signing gate."
} > "$dry_run_dir/release-dry-run.md"
sha256sum "$dry_run_dir/package-verification.md" "$dry_run_dir/release-dry-run.md" > "$dry_run_dir/Sigtran.NET.$RELEASE_VERSION.dry-run.sha256"
- name: Prepare Release Artifacts
shell: bash
run: |
mkdir -p artifacts/release artifacts/release-evidence artifacts/supply-chain/sbom artifacts/supply-chain/signing artifacts/supply-chain/provenance artifacts/supply-chain/api artifacts/supply-chain/digests
cp "src/Sigtran.NET/bin/Release/Sigtran.NET.$RELEASE_VERSION.nupkg" artifacts/release/
cp "src/Sigtran.NET/bin/Release/Sigtran.NET.$RELEASE_VERSION.snupkg" artifacts/release/
echo "SIGTRAN_RELEASE_PACKAGE=artifacts/release/Sigtran.NET.$RELEASE_VERSION.nupkg" >> "$GITHUB_ENV"
- name: Install SBOM Tool
run: dotnet tool install --global Microsoft.Sbom.DotNetTool --version 4.1.5
- name: Generate Final SBOM
shell: bash
run: |
sbom-tool generate -b src/Sigtran.NET/bin/Release -bc . -pn Sigtran.NET -pv "$RELEASE_VERSION" -ps Sigtran.NET -nsb https://github.com/araditc/Sigtran.NET -m artifacts/supply-chain/sbom
sbom_source="$(find artifacts/supply-chain/sbom -name '*.spdx.json' -o -name 'manifest.spdx.json' | head -n 1)"
test -n "$sbom_source"
final_sbom="artifacts/supply-chain/sbom/Sigtran.NET.$RELEASE_VERSION.spdx.json"
cp "$sbom_source" "$final_sbom"
final_sbom_sha256="$(sha256sum "$final_sbom" | awk '{print $1}')"
echo "SIGTRAN_FINAL_SBOM_PATH=$final_sbom" >> "$GITHUB_ENV"
echo "SIGTRAN_FINAL_SBOM_SHA256=$final_sbom_sha256" >> "$GITHUB_ENV"
echo "SIGTRAN_FINAL_SBOM_FORMAT=spdx-json" >> "$GITHUB_ENV"
- name: Sign Package
if: ${{ github.event_name != 'workflow_dispatch' || inputs.channel == 'dry-run' || (inputs.channel == 'stable' && inputs.publish == true) }}
shell: bash
env:
SIGNING_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE }}
SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }}
run: |
test -n "$SIGNING_CERTIFICATE"
test -n "$SIGNING_CERTIFICATE_PASSWORD"
signing_certificate_path="$RUNNER_TEMP/sigtran-release-signing.pfx"
printf '%s' "$SIGNING_CERTIFICATE" | base64 -d > "$signing_certificate_path"
if [ "$SIGTRAN_RELEASE_CHANNEL" = "stable" ]; then
test -n "${{ vars.TRUSTED_SIGNING_CERTIFICATE_SHA256 }}"
bash eng/verify-signing-certificate.sh \
"$signing_certificate_path" \
"${{ vars.TRUSTED_SIGNING_CERTIFICATE_SHA256 }}" \
| tee "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.certificate-trust.md"
fi
dotnet nuget sign "$SIGTRAN_RELEASE_PACKAGE" --certificate-path "$signing_certificate_path" --certificate-password "$SIGNING_CERTIFICATE_PASSWORD" --timestamper "$TIMESTAMP_AUTHORITY" --overwrite
sha256sum "$SIGTRAN_RELEASE_PACKAGE" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.package.sha256"
- name: Retain Unsigned Prerelease Package Evidence
if: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'prerelease' }}
shell: bash
run: |
sha256sum "$SIGTRAN_RELEASE_PACKAGE" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.package.sha256"
{
echo "version=$RELEASE_VERSION"
echo "channel=$SIGTRAN_RELEASE_CHANNEL"
echo "authorSigning=skipped-for-public-prerelease"
echo "reason=NuGet.org rejects self-issued author signing certificates; repository signing is applied by NuGet.org after publish."
echo "package=$SIGTRAN_RELEASE_PACKAGE"
} > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.prerelease-signing-policy.md"
sha256sum "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.prerelease-signing-policy.md" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.prerelease-signing-policy.sha256"
- name: Retain Unsigned Stable Assessment Evidence
if: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.publish == false }}
shell: bash
run: |
sha256sum "$SIGTRAN_RELEASE_PACKAGE" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.package.sha256"
{
echo "version=$RELEASE_VERSION"
echo "channel=$SIGTRAN_RELEASE_CHANNEL"
echo "authorSigning=not-executed"
echo "reason=stable assessment does not sign or publish a package"
echo "package=$SIGTRAN_RELEASE_PACKAGE"
} > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.stable-assessment.md"
- name: Trust Dry-Run Signing Certificate
if: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'dry-run' }}
shell: bash
env:
SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }}
run: |
signing_certificate_path="$RUNNER_TEMP/sigtran-release-signing.pfx"
signing_certificate_crt="$RUNNER_TEMP/sigtran-release-signing.crt"
test -f "$signing_certificate_path"
openssl pkcs12 -in "$signing_certificate_path" -passin pass:"$SIGNING_CERTIFICATE_PASSWORD" -clcerts -nokeys -out "$signing_certificate_crt"
sudo cp "$signing_certificate_crt" /usr/local/share/ca-certificates/sigtran-release-signing.crt
sudo update-ca-certificates
for codesign_bundle in /usr/share/dotnet/sdk/*/trustedroots/codesignctl.pem; do
test -f "$codesign_bundle"
sudo tee -a "$codesign_bundle" < "$signing_certificate_crt" > /dev/null
done
signing_certificate_fingerprint="$(openssl x509 -in "$signing_certificate_crt" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d :)"
echo "SIGTRAN_SIGNING_CERTIFICATE_SHA256=$signing_certificate_fingerprint" >> "$GITHUB_ENV"
openssl x509 -in "$signing_certificate_crt" -noout -fingerprint -sha256 | tee "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.dry-run-certificate-fingerprint.txt"
sha256sum "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.dry-run-certificate-fingerprint.txt" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.dry-run-certificate-fingerprint.sha256"
- name: Verify Signature And Timestamp
if: ${{ github.event_name != 'workflow_dispatch' || inputs.channel == 'dry-run' || (inputs.channel == 'stable' && inputs.publish == true) }}
shell: bash
run: |
verify_args=( "$SIGTRAN_RELEASE_PACKAGE" --all --verbosity detailed )
if [ -n "${SIGTRAN_SIGNING_CERTIFICATE_SHA256:-}" ]; then
verify_args+=( --certificate-fingerprint "$SIGTRAN_SIGNING_CERTIFICATE_SHA256" )
fi
dotnet nuget verify "${verify_args[@]}" | tee "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.verification.md"
cp "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.verification.md" "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.timestamp.md"
sha256sum "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.verification.md" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.verification.sha256"
sha256sum "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.timestamp.md" > "artifacts/supply-chain/signing/Sigtran.NET.$RELEASE_VERSION.timestamp.sha256"
- name: Attest Package Provenance
if: ${{ github.event_name != 'workflow_dispatch' || inputs.channel == 'stable' }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: ${{ env.SIGTRAN_RELEASE_PACKAGE }}
- name: Attest SBOM
if: ${{ github.event_name != 'workflow_dispatch' || inputs.channel == 'stable' }}
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: ${{ env.SIGTRAN_RELEASE_PACKAGE }}
sbom-path: ${{ env.SIGTRAN_FINAL_SBOM_PATH }}
- name: Retain Provenance Marker
shell: bash
run: |
{
echo "sourceRepository=${{ github.repository }}"
echo "sourceCommit=${{ github.sha }}"
echo "workflow=release"
echo "runId=${{ github.run_id }}"
echo "subject=$SIGTRAN_RELEASE_PACKAGE"
echo "sbom=$SIGTRAN_FINAL_SBOM_PATH"
} > "artifacts/supply-chain/provenance/Sigtran.NET.$RELEASE_VERSION.intoto.jsonl"
sha256sum "artifacts/supply-chain/provenance/Sigtran.NET.$RELEASE_VERSION.intoto.jsonl" > "artifacts/supply-chain/provenance/Sigtran.NET.$RELEASE_VERSION.intoto.sha256"
- name: Retain Public API Diff Digest
shell: bash
run: |
api_diff="artifacts/supply-chain/api/Sigtran.NET.$RELEASE_VERSION.api-diff.md"
test -f "$api_diff"
echo "SIGTRAN_PUBLIC_API_DIFF_PATH=$api_diff" >> "$GITHUB_ENV"
sha256sum "$api_diff" > "artifacts/supply-chain/api/Sigtran.NET.$RELEASE_VERSION.api-diff.sha256"
- name: Create Digest Manifest
shell: bash
run: |
digest_manifest="$(mktemp)"
find artifacts/release artifacts/supply-chain artifacts/release-dry-run artifacts/release-evidence \
-type f \
! -path "artifacts/supply-chain/digests/Sigtran.NET.$RELEASE_VERSION.sha256" \
-print0 |
sort -z |
xargs -0 sha256sum > "$digest_manifest"
mv "$digest_manifest" "artifacts/supply-chain/digests/Sigtran.NET.$RELEASE_VERSION.sha256"
- name: Upload Package
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sigtran-package
path: artifacts/release/*.nupkg
retention-days: 90
- name: Upload Symbols
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sigtran-symbols
path: artifacts/release/*.snupkg
retention-days: 90
- name: Upload Supply Chain Evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sigtran-supply-chain
path: artifacts/supply-chain
retention-days: 90
- name: Upload Dry-Run Evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sigtran-release-dry-run
path: artifacts/release-dry-run
retention-days: 90
- name: Upload Stable Release Decision
if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.channel == 'stable') }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sigtran-stable-release-decision
path: artifacts/release-evidence
retention-days: 90
- name: Evaluate RC Publication Gate
if: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'prerelease' }}
shell: bash
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
test "${{ inputs.publish }}" = "true"
test -n "$NUGET_API_KEY"
case "$RELEASE_VERSION" in
*-*) ;;
*) echo "Prerelease channel requires a prerelease version."; exit 1 ;;
esac
test -f "$SIGTRAN_DRY_RUN_ARTIFACT_ROOT/$RELEASE_VERSION/release-dry-run.md"
- name: Production Evidence
if: ${{ github.event_name != 'workflow_dispatch' || inputs.channel == 'stable' }}
run: |
test -f "$SIGTRAN_RELEASE_EVIDENCE_ROOT/stable-release-decision.json"
test -f "$SIGTRAN_RELEASE_EVIDENCE_ROOT/stable-release-decision.md"
publish:
needs: release
if: ${{ github.event_name == 'workflow_dispatch' && inputs.channel != 'dry-run' && inputs.publish == true }}
runs-on: ubuntu-latest
environment:
name: ${{ inputs.channel == 'stable' && 'nuget-stable' || 'nuget-prerelease' }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
- name: Download Package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sigtran-package
path: artifacts/release
- name: Download Symbols
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sigtran-symbols
path: artifacts/release
- name: Download Supply Chain Evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sigtran-supply-chain
path: artifacts/supply-chain
- name: Download Stable Release Decision
if: ${{ inputs.channel == 'stable' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sigtran-stable-release-decision
path: artifacts/release-evidence
- name: Validate Publication Request
shell: bash
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_CHANNEL: ${{ inputs.channel }}
run: |
test -n "$NUGET_API_KEY"
if [ "$RELEASE_CHANNEL" = "stable" ]; then
case "$RELEASE_VERSION" in
*-*) echo "Stable channel requires a stable version."; exit 1 ;;
esac
jq -e '.decision == "GO"' artifacts/release-evidence/stable-release-decision.json
test "${{ inputs.confirmation }}" = "publish Sigtran.NET $RELEASE_VERSION"
test "${GITHUB_REF_NAME}" = "main"
tag_commit="$(git rev-list -n 1 "v$RELEASE_VERSION")"
test "$tag_commit" = "$GITHUB_SHA"
dotnet nuget verify "artifacts/release/Sigtran.NET.$RELEASE_VERSION.nupkg" --all
else
case "$RELEASE_VERSION" in
*-*) ;;
*) echo "Prerelease channel requires a prerelease version."; exit 1 ;;
esac
fi
- name: Publish
shell: bash
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
dotnet nuget push \
"artifacts/release/Sigtran.NET.$RELEASE_VERSION.nupkg" \
--source https://api.nuget.org/v3/index.json \
--api-key "$NUGET_API_KEY"
- name: Verify Stable Public Restore
if: ${{ inputs.channel == 'stable' }}
shell: bash
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
for attempt in $(seq 1 30); do
if curl --fail --silent "https://api.nuget.org/v3-flatcontainer/sigtran.net/$RELEASE_VERSION/sigtran.net.$RELEASE_VERSION.nupkg" > /dev/null; then
break
fi
if [ "$attempt" = "30" ]; then
echo "Published package did not become visible in time."
exit 1
fi
sleep 10
done
restore_dir="$RUNNER_TEMP/sigtran-public-restore"
dotnet new classlib --framework net10.0 --output "$restore_dir"
dotnet add "$restore_dir" package Sigtran.NET --version "$RELEASE_VERSION" --source https://api.nuget.org/v3/index.json
- name: Create Stable GitHub Release
if: ${{ inputs.channel == 'stable' }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
gh release create "v$RELEASE_VERSION" \
artifacts/release/Sigtran.NET.* \
artifacts/supply-chain/sbom/Sigtran.NET.$RELEASE_VERSION.spdx.json \
artifacts/release-evidence/stable-release-decision.json \
--repo "$GITHUB_REPOSITORY" \
--title "Sigtran.NET $RELEASE_VERSION" \
--notes-file "docs/releases/$RELEASE_VERSION.md" \
--verify-tag