Skip to content

Verifiable .NET build #3492

Verifiable .NET build

Verifiable .NET build #3492

Workflow file for this run

name: Verifiable .NET build
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
paths-ignore:
- "**.md"
release:
types:
- published
schedule:
# A daily build to check all is still functioning.
- cron: "0 0 * * *"
env:
# This environment variable is checked in tests.
# For TPM either a Windows simulator or a platform TPM is used.
# Tests check that platform TPM tests must be run on Windows or Linux,
# MacOS tests are skipped.
USE_PLATFORM_TPM: true
DOTNET_ENVIRONMENT: CI
BUILD_CONFIGURATION: Release
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
# The code coverage tool reads its own opt-out variables, documented as the literal value 1.
DOTNET_COVERAGE_TELEMETRY_OPTOUT: 1
DOTNET_COVERAGE_NOLOGO: 1
NUGET_XMLDOC_MODE: skip
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
NUGET_FEED: https://api.nuget.org/v3/index.json
GITHUB_USER: ${{ github.repository_owner }}
GITHUB_FEED: https://nuget.pkg.github.com/lumoin/
# These are the library NuGet packages that will be packed and uploaded.
# The Verifiable CLI tool is handled separately with native AOT per-platform packaging.
LIBRARY_PROJECTS: >
Verifiable.Acdc
Verifiable.Apdu
Verifiable.BouncyCastle
Verifiable.Cbor
Verifiable.Cesr
Verifiable.Core
Verifiable.Cryptography
Verifiable.DidComm
Verifiable.DidWebs
Verifiable.Fido2
Verifiable.Foundation
Verifiable.JCose
Verifiable.Json
Verifiable.JsonPointer
Verifiable.Keri
Verifiable.Libsodium
Verifiable.Microsoft
Verifiable.OAuth
Verifiable.Server
Verifiable.Tpm
Verifiable.Tpm.Spec
Verifiable.Vcalm
Verifiable.WebFinger
# These are the test projects that will be reported.
VERIFIABLE_TESTS: Verifiable.Tests
# These are allowed endpoints these actions can contact.
COMMON_ALLOWED_ENDPOINTS: >
api.clearlydefined.io:443
aka.ms:443
api.github.com:443
*.actions.githubusercontent.com:443
api.nuget.org:443
www.nuget.org:443
builds.dotnet.microsoft.com:443
ci.dot.net:443
pkgs.dev.azure.com:443
github.com:443
nuget.pkg.github.com:443
fulcio.sigstore.dev:443
rekor.sigstore.dev:443
cacerts.digicert.com:80
ts-crl.ws.symantec.com:80
crl3.digicert.com:80
crl4.digicert.com:80
s.symcb.com:80
ocsp.digicert.com:80
# BouncyCastle's NuGet package signature chains to Sectigo/Comodo/UserTrust;
# its CRL/OCSP endpoints must be reachable to verify the signature's revocation.
crl.sectigo.com:443
ocsp.sectigo.com:443
crl.comodoca.com:443
ocsp.comodoca.com:443
crl.usertrust.com:443
# Dotnet tools download CRL list from here.
www.microsoft.com:80
jobs:
build:
name: Build Verifiable packages
permissions:
# contents: read is the only permission needed on Windows and macOS.
# The ubuntu leg additionally needs checks/pull-requests (test summary/
# coverage comment) and id-token + attestations (artifact signing via
# Sigstore). GitHub does not support per-matrix-leg permission blocks,
# so all three runners carry these. The ubuntu-only step guards prevent
# the elevated permissions from being exercised on Windows or macOS.
checks: write
pull-requests: write
id-token: write
attestations: write
contents: read
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest, ubuntu-24.04-arm ]
steps:
- name: Harden Runner
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }}
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: ${{ env.COMMON_ALLOWED_ENDPOINTS }}
# Windows has no sudo to disable; otherwise the same block-mode allowlist so the
# workflow governs egress on the Windows host (package-signature revocation included).
- name: Harden Runner
if: ${{ matrix.os == 'windows-latest' }}
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c
with:
egress-policy: block
allowed-endpoints: ${{ env.COMMON_ALLOWED_ENDPOINTS }}
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
fetch-tags: true
- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
cache: true
cache-dependency-path: '**/packages.lock.json'
global-json-file: global.json
- name: Install dependencies
run: dotnet restore --locked-mode
- name: Restore local .NET tools
run: dotnet tool restore
- name: Add Problem Matcher for dotnet-format
uses: xt0rted/dotnet-format-problem-matcher@b90c4f18e3daa4f8fd266e41eba4f351b2e00b75
# The version numbers are determined and set here so it can be set appropriately to code and NuGet packages.
# See explanation at https://dusted.codes/github-actions-for-dotnet-core-nuget-packages.
- name: Set NuGet packages and suffix version
run: |
# Bash script to determine the version and version suffix for CI/CD
# Use git to find the latest tag; if none are found, default to "0.0.1".
latestTag=$(git describe --tags --abbrev=0 2>/dev/null || echo 0.0.1)
# Case 1: A release is being created.
# Extract the version from the tag and add a suffix with the short SHA.
if [[ $GITHUB_EVENT_NAME == 'release' ]]; then
# Extract the version number from the release tag and remove any 'v' prefix.
arrTag=(${GITHUB_REF//\// })
VERSION="${arrTag[2]}"
VERSION="${VERSION//v}"
# Set PACKAGE_VERSION and VERSION, and add a VERSION_SUFFIX for clarity in identifying the release and commit.
echo "PACKAGE_VERSION=${VERSION}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${VERSION}" >> "$GITHUB_ENV"
# Case 2: The 'develop' branch.
# Use the latestTag as the base version and add a suffix with "develop", the run_id, and the short SHA.
elif [[ $GITHUB_REF == 'refs/heads/develop' ]]; then
# Set PACKAGE_VERSION and PACKAGE, and add a VERSION_SUFFIX for clarity in identifying the development build, run, and commit.
echo "PACKAGE_VERSION=${latestTag//v}-develop.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${latestTag//v}-develop.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${latestTag//v}" >> "$GITHUB_ENV"
# Case 3: Any other build (e.g., feature branches).
# Use the latestTag as the base version and add a suffix with "build", the run_id, and the short SHA.
else
# Set PACKAGE_VERSION and VERSION, and add a VERSION_SUFFIX for clarity in identifying the build, run, and commit.
echo "PACKAGE_VERSION=${latestTag//v}-build.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${latestTag//v}-build.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${latestTag//v}" >> "$GITHUB_ENV"
fi
shell: bash
# Note that code coverage requires 'MessageUponIsolationViolation'.
- name: Build
run: dotnet build --binaryLogger --tl --graphBuild -isolate:MessageUponIsolationViolation --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore --property:ContinuousIntegrationBuild=true --property:Version=${{ env.VERSION }} --property:InformationalVersion=${{ env.PACKAGE_VERSION }} --property:AssemblyVersion=${{ env.TAG_VERSION }} --property:FileVersion=${{ env.TAG_VERSION }}
timeout-minutes: 15
# The test executable has no file extension on Linux/macOS, so the same
# dotnet-coverage command (which finds the binary by name) works on every OS.
- name: Test
run: dotnet tool run dotnet-coverage collect --output '${{ github.workspace }}/reports/coverage.cobertura.xml' --output-format cobertura '${{ github.workspace }}/test/${{ env.VERIFIABLE_TESTS }}/bin/${{ env.BUILD_CONFIGURATION }}/net10.0/${{ env.VERIFIABLE_TESTS }}' --report-trx --report-trx-filename testresults.trx --results-directory '${{ github.workspace }}/reports'
timeout-minutes: 15
# Mutation testing (Stryker.NET) is disabled in CI for two reasons:
# 1. BLOCKER: Verifiable.Tests uses Microsoft.Testing.Platform (the MSTest.Sdk
# default runner), which Stryker.NET 4.14.x does not support. Stryker
# discovers tests via VsTest, finds 0 tests, and the run fails. Track:
# https://github.com/stryker-mutator/stryker-net/issues/3094
# 2. A full mutation sweep is very resource intensive — across every library
# project it far exceeds the CI time budget. It is meant to run out-of-band
# via stryker-tests-dev.ps1 or on a dedicated schedule, not on each push.
# When re-enabled this mutates every shippable library project (LIBRARY_PROJECTS),
# not just Verifiable.Core. Stryker auto-detects Verifiable.slnx (.slnx parses fine
# in 4.14.x — no --solution needed); --project selects the source project to mutate.
# The per-project HTML report is uploaded as an artifact below. The Stryker dashboard
# reporter is intentionally not used — there is no Stryker dashboard account / API key.
# - name: Stryker.NET mutation tests (all library projects)
# if: ${{ matrix.os == 'ubuntu-latest' }}
# timeout-minutes: 90
# run: |
# for project in $LIBRARY_PROJECTS; do
# echo "=== Stryker: mutating $project ==="
# dotnet stryker \
# --config-file stryker-config.json \
# --reporter progress --reporter html \
# --project "$project.csproj" \
# --output "StrykerOutput/$project"
# done
#- name: Upload Stryker output artefacts
# if: ${{ matrix.os == 'ubuntu-latest' }}
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# with:
# name: 'Stryker output artefacts'
# path: ${{ github.workspace }}/StrykerOutput/
# - name: Publish
# run: dotnet publish -c Release --verbosity normal -o ./publish/
# - name: Archive publish results
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# with:
# name: Verifiable.Benchmarks
# path: ./publish/*
# - name: Run Benchmarks
# run: dotnet "./publish/Verifiable.Benchmarks.dll" -f "Verifiable.Benchmarks.*"
# - name: Upload benchmark results
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# with:
# name: Benchmark_Results
# path: ./BenchmarkDotNet.Artifacts/results/*
# This step is run always (e.g. also for non-PRs) so the results can be inspected on the command line too.
- name: ReportGenerator
if: ${{ matrix.os == 'ubuntu-latest' }}
run: dotnet reportgenerator -filefilters:'-**/obj/**;-**/*.g.cs' -assemblyfilters:'+Verifiable*' -reports:'${{ github.workspace }}/reports/coverage.cobertura.xml' -targetdir:'${{ github.workspace }}/reports/coverage/' -reporttypes:'HtmlInline;Cobertura'
- name: Create test summary
if: ${{ matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }}
uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3
with:
trx_files: '${{ github.workspace }}/reports/**/*.trx'
# The full file-by-file coverage report is uploaded as an artifact rather than posted to
# the PR — the per-file table was too long to be useful inline. Only the headline line/
# branch numbers go to the run's job summary.
- name: Upload coverage report
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: coverage-report
path: '${{ github.workspace }}/reports/coverage/'
- name: Coverage headline to job summary
if: ${{ matrix.os == 'ubuntu-latest' }}
shell: bash
run: |
xml='${{ github.workspace }}/reports/coverage.cobertura.xml'
lineRate=$(grep -m1 '<coverage ' "$xml" | grep -oE 'line-rate="[0-9.]+"' | grep -oE '[0-9.]+')
branchRate=$(grep -m1 '<coverage ' "$xml" | grep -oE 'branch-rate="[0-9.]+"' | grep -oE '[0-9.]+')
linePct=$(awk -v v="$lineRate" 'BEGIN { printf "%.1f", v * 100 }')
branchPct=$(awk -v v="$branchRate" 'BEGIN { printf "%.1f", v * 100 }')
{
echo "### Coverage"
echo ""
echo "- Line: ${linePct}%"
echo "- Branch: ${branchPct}%"
} >> "$GITHUB_STEP_SUMMARY"
- name: Update MCP server manifest version
if: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'lumoin' }}
run: |
sed -i 's/"version": "[^"]*"/"version": "'"${VERSION%%+*}"'"/' src/Verifiable/.mcp/server.json
echo "Updated server.json version to: ${VERSION%%+*}"
cat src/Verifiable/.mcp/server.json
- name: Pack library NuGet packages
if: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'lumoin' }}
run: |
for project in $LIBRARY_PROJECTS; do
path="src/$project/$project.csproj"
echo "Packing $path"
dotnet pack --verbosity normal \
--configuration "$BUILD_CONFIGURATION" \
--no-build \
--output nupkgs \
--property:PackageVersion="${PACKAGE_VERSION%%+*}" \
"$path"
done
# Note that on Windows this would be simply "dotnet dotnet-validate package local ./snupkgs/*.nupkg", the handling of an array of paths works directly.
- name: Validate generated NuGet files
if: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'lumoin' && github.actor != 'dependabot[bot]' }}
run: |
for file in ./nupkgs/*.nupkg; do
dotnet dotnet-validate package local "$file"
done
- name: Generate artifact attestation
if: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'lumoin' && github.actor != 'dependabot[bot]' && github.event.repository.private == false }}
uses: lumoin/trusted-devsecops/.github/actions/sign-artifact@bd2b7f91e73d758f3126562d6a569fc0f31814ee
with:
subject-path: './nupkgs/*.nupkg'
- name: Upload library NuGet packages
if: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'lumoin' && github.actor != 'dependabot[bot]' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: nupkg-libraries
path: ./nupkgs/*.*
# ---------------------------------------------------------------------------
# Native AOT CLI tool packaging
#
# The .NET 10 multi-platform tool packaging model (hybrid AOT + CoreCLR):
# 1. Per-RID native AOT packages: dotnet pack -r <rid>, built on the matching
# OS because AOT produces native binaries (linux on ubuntu, osx on macos,
# win on windows). Cross-arch within the same OS (e.g. win-arm64 from a
# win-x64 runner) works because the AOT compiler cross-compiles the IL.
# 2. CoreCLR "any" fallback: dotnet pack -r any --property:PublishAot=false,
# framework-dependent and portable, for consumers on pre-.NET 10 SDKs or
# platforms not covered by the AOT matrix.
# 3. Top-level pointer package: dotnet pack (no -r), produces only a
# DotnetToolSettings.xml listing all RID packages. Must run after all RID
# packages are in ./nupkgs so the SDK enumerates them for the manifest.
# 4. All RID packages must be published to the feed BEFORE the pointer package,
# otherwise dotnet tool install fails to resolve them.
#
# References:
# https://learn.microsoft.com/dotnet/core/tools/rid-specific-tools
# https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/
# https://github.com/baronfel/multi-rid-tool
# ---------------------------------------------------------------------------
pack-cli-aot:
name: Pack Verifiable CLI (native AOT, ${{ matrix.os }})
# Only run native AOT packing on main, develop, and release events.
# On all other branches the build job already provides the AOT compatibility
# signal: IsAotCompatible=true and ILLinkTreatWarningsAsErrors=true in
# Directory.Build.props mean trim/AOT warnings are errors during the normal
# managed dotnet build, and the test suite exercises the CLI via process
# invocation. Native packing is expensive (cross-compiler toolchain installs,
# multi-platform runners) and adds no further signal on feature branches.
needs: build
if: |
github.repository_owner == 'lumoin' && (
github.event_name == 'release' ||
github.event_name == 'workflow_dispatch' ||
github.ref == 'refs/heads/main' ||
github.ref == 'refs/heads/develop'
)
permissions:
contents: read
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
rids: linux-x64
- os: ubuntu-24.04-arm
rids: linux-arm64
- os: macos-latest
rids: osx-x64 osx-arm64
- os: windows-latest
rids: win-x64
- os: windows-11-arm
rids: win-arm64
steps:
- name: Harden Runner
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }}
# Note: disable-sudo is intentionally omitted here because apt-get is
# needed below to install native AOT prerequisites (clang, zlib1g-dev).
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c
with:
egress-policy: block
allowed-endpoints: ${{ env.COMMON_ALLOWED_ENDPOINTS }}
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
fetch-tags: true
- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
cache: true
cache-dependency-path: '**/packages.lock.json'
global-json-file: global.json
- name: Install dependencies
run: dotnet restore --locked-mode
- name: Restore local .NET tools
run: dotnet tool restore
- name: Set version
run: |
latestTag=$(git describe --tags --abbrev=0 2>/dev/null || echo 0.0.1)
if [[ $GITHUB_EVENT_NAME == 'release' ]]; then
arrTag=(${GITHUB_REF//\// })
VERSION="${arrTag[2]}"
VERSION="${VERSION//v}"
echo "PACKAGE_VERSION=${VERSION}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${VERSION}" >> "$GITHUB_ENV"
elif [[ $GITHUB_REF == 'refs/heads/develop' ]]; then
echo "PACKAGE_VERSION=${latestTag//v}-develop.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${latestTag//v}-develop.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${latestTag//v}" >> "$GITHUB_ENV"
else
echo "PACKAGE_VERSION=${latestTag//v}-build.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${latestTag//v}-build.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${latestTag//v}" >> "$GITHUB_ENV"
fi
shell: bash
# Native AOT on Linux requires clang and zlib headers for x64, plus the
# full aarch64 cross-compilation toolchain for linux-arm64 cross-compilation
# from the x86_64 runner.
# The multiarch approach (dpkg --add-architecture arm64 + libc6-dev:arm64)
# installs the aarch64 runtime libraries directly into /usr/aarch64-linux-gnu/
# where the cross linker expects them, rather than the cross-dev packages
# which put them in the wrong location for sysroot-based linking.
# macOS and Windows runners have their native toolchains pre-installed.
- name: Install native AOT prerequisites (Linux)
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }}
# Each Linux runner compiles only its native RID so no cross-compilation
# toolchain is needed. clang and zlib1g-dev are sufficient for both
# linux-x64 (ubuntu-latest) and linux-arm64 (ubuntu-24.04-arm).
run: sudo apt-get install -y clang zlib1g-dev
# Build one native AOT RID package per RID on the matching host OS.
# dotnet pack -r <rid> with PublishAot=true in the csproj produces a
# DotnetToolRidPackage containing a fully native self-contained binary.
# Each runner compiles only its native RID — no cross-compilation needed.
- name: Pack native AOT RID packages
run: |
for rid in ${{ matrix.rids }}; do
echo "Packing native AOT RID package for $rid"
dotnet pack src/Verifiable/Verifiable.csproj \
--configuration "$BUILD_CONFIGURATION" \
--output nupkgs \
-r "$rid" \
--property:PackageVersion="${PACKAGE_VERSION%%+*}" \
--property:Version="$VERSION" \
--property:InformationalVersion="$PACKAGE_VERSION" \
--property:AssemblyVersion="$TAG_VERSION" \
--property:FileVersion="$TAG_VERSION" \
--property:ContinuousIntegrationBuild=true
done
shell: bash
# Runtime conformance: prove the native AOT binary just produced actually runs
# on this platform. The managed build's IsAotCompatible analysis catches trim/AOT
# issues statically, but only executing the compiled, trimmed binary proves the
# System.CommandLine parse -> action -> output pipeline survives AOT on each RID.
# The host RID is derived from runner.os/runner.arch and only that native binary
# is run (on macOS the matrix packs both osx RIDs; only the native one executes).
# A real command must exit 0 and emit the expected output.
- name: Verify native AOT binary runs (${{ matrix.os }})
shell: bash
timeout-minutes: 5
run: |
case "${RUNNER_OS}-${RUNNER_ARCH}" in
Linux-X64) rid=linux-x64 ;;
Linux-ARM64) rid=linux-arm64 ;;
macOS-ARM64) rid=osx-arm64 ;;
macOS-X64) rid=osx-x64 ;;
Windows-X64) rid=win-x64 ;;
Windows-ARM64) rid=win-arm64 ;;
*) echo "Unsupported host ${RUNNER_OS}-${RUNNER_ARCH}"; exit 1 ;;
esac
bin="src/Verifiable/bin/${BUILD_CONFIGURATION}/net10.0/${rid}/publish/Verifiable"
if [ -f "${bin}.exe" ]; then bin="${bin}.exe"; fi
echo "Running native AOT binary: ${bin}"
out="$("${bin}" did create 42 conformance-check)"
echo "${out}"
echo "${out}" | grep -q 'Created DID document with ID: 42'
echo "${out}" | grep -q 'Parameter: conformance-check'
- name: Upload AOT RID packages (${{ matrix.os }})
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: nupkg-cli-aot-${{ matrix.os }}
path: ./nupkgs/*.nupkg
# Intermediate artifacts consumed by pack-cli-pointer only.
# One day retention is sufficient; final nupkg-cli has normal retention.
retention-days: 1
# The native binary is already produced as a side effect of dotnet pack -r
# <rid> with PublishAot=true — no extra build work. Upload it so people
# without a .NET SDK can download and run the tool directly.
- name: Upload native CLI binaries (${{ matrix.os }})
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: cli-binaries-${{ matrix.os }}
path: src/Verifiable/bin/Release/net10.0/*/publish/Verifiable*
retention-days: 30
pack-cli-pointer:
name: Pack Verifiable CLI (pointer + CoreCLR any fallback)
needs: pack-cli-aot
if: ${{ github.repository_owner == 'lumoin' }}
permissions:
checks: write
id-token: write
attestations: write
contents: read
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: ${{ env.COMMON_ALLOWED_ENDPOINTS }}
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
fetch-tags: true
- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
cache: true
cache-dependency-path: '**/packages.lock.json'
global-json-file: global.json
- name: Install dependencies
run: dotnet restore --locked-mode
- name: Restore local .NET tools
run: dotnet tool restore
- name: Set version
run: |
latestTag=$(git describe --tags --abbrev=0 2>/dev/null || echo 0.0.1)
if [[ $GITHUB_EVENT_NAME == 'release' ]]; then
arrTag=(${GITHUB_REF//\// })
VERSION="${arrTag[2]}"
VERSION="${VERSION//v}"
echo "PACKAGE_VERSION=${VERSION}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${VERSION}" >> "$GITHUB_ENV"
elif [[ $GITHUB_REF == 'refs/heads/develop' ]]; then
echo "PACKAGE_VERSION=${latestTag//v}-develop.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${latestTag//v}-develop.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${latestTag//v}" >> "$GITHUB_ENV"
else
echo "PACKAGE_VERSION=${latestTag//v}-build.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "VERSION=${latestTag//v}-build.${GITHUB_RUN_ID}+${GITHUB_SHA::7}" >> "$GITHUB_ENV"
echo "TAG_VERSION=${latestTag//v}" >> "$GITHUB_ENV"
fi
shell: bash
# Download all native AOT RID packages produced by pack-cli-aot across all
# three OS runners so the SDK can enumerate them when building the pointer.
- name: Download AOT RID packages (Linux x64)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli-aot-ubuntu-latest
path: ./nupkgs
- name: Download AOT RID packages (Linux arm64)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli-aot-ubuntu-24.04-arm
path: ./nupkgs
- name: Download AOT RID packages (macOS)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli-aot-macos-latest
path: ./nupkgs
- name: Download AOT RID packages (Windows x64)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli-aot-windows-latest
path: ./nupkgs
- name: Download AOT RID packages (Windows arm64)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli-aot-windows-11-arm
path: ./nupkgs
# The CoreCLR "any" fallback is a framework-dependent portable package for
# platforms not covered by AOT builds or consumers on pre-.NET 10 SDKs.
# PublishAot=false and PublishSelfContained=false produce managed IL only.
# RuntimeIdentifiers is cleared so the SDK does not iterate all RIDs from
# the csproj — we only want the single "any" RID package here.
- name: Pack CoreCLR "any" fallback package
run: |
dotnet pack src/Verifiable/Verifiable.csproj \
--configuration "$BUILD_CONFIGURATION" \
--output nupkgs \
-r any \
--property:PublishAot=false \
--property:PublishSelfContained=false \
--property:RuntimeIdentifiers= \
--property:PackageVersion="${PACKAGE_VERSION%%+*}" \
--property:Version="$VERSION" \
--property:InformationalVersion="$PACKAGE_VERSION" \
--property:AssemblyVersion="$TAG_VERSION" \
--property:FileVersion="$TAG_VERSION" \
--property:ContinuousIntegrationBuild=true
# The top-level pointer package (no -r) contains only a DotnetToolSettings.xml
# listing all RID packages. The .NET 10 CLI reads this at install time to
# select the correct RID package. Must run after all RID packages exist.
# RuntimeIdentifiers is cleared so the SDK does not re-build all RIDs from
# the csproj — the pointer pack itself produces no binaries.
- name: Pack top-level pointer package
run: |
dotnet pack src/Verifiable/Verifiable.csproj \
--configuration "$BUILD_CONFIGURATION" \
--output nupkgs \
--property:PublishAot=false \
--property:PublishSelfContained=false \
--property:RuntimeIdentifiers= \
--property:PackageVersion="${PACKAGE_VERSION%%+*}" \
--property:Version="$VERSION" \
--property:InformationalVersion="$PACKAGE_VERSION" \
--property:AssemblyVersion="$TAG_VERSION" \
--property:FileVersion="$TAG_VERSION" \
--property:ContinuousIntegrationBuild=true
- name: Validate CLI NuGet packages
if: ${{ github.actor != 'dependabot[bot]' }}
run: |
for file in ./nupkgs/*.nupkg; do
dotnet dotnet-validate package local "$file"
done
- name: Generate artifact attestation for CLI packages
if: ${{ github.actor != 'dependabot[bot]' && github.event.repository.private == false }}
uses: lumoin/trusted-devsecops/.github/actions/sign-artifact@bd2b7f91e73d758f3126562d6a569fc0f31814ee
with:
subject-path: './nupkgs/*.nupkg'
- name: Upload CLI NuGet packages
if: ${{ github.actor != 'dependabot[bot]' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: nupkg-cli
path: ./nupkgs/*.*
# The release idea is inspired by https://dusted.codes/github-actions-for-dotnet-core-nuget-packages.
prerelease:
name: Create and upload Verifiable pre-release packages to GitHub
permissions:
packages: write
needs: [ build, pack-cli-pointer ]
if: ${{ github.ref == 'refs/heads/develop' && github.repository_owner == 'lumoin' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: ${{ env.COMMON_ALLOWED_ENDPOINTS }}
- name: Download library NuGet packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-libraries
path: ./nupkgs
- name: Download CLI NuGet packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli
path: ./nupkgs
# Push RID-specific packages before the pointer package. The pointer package
# filename matches Verifiable.<version>.nupkg where the first char after
# "Verifiable." is a digit. All other *.nupkg files are RID or library
# packages and are safe to push first.
- name: Push RID and library packages to GitHub feed
run: |
for file in ./nupkgs/*.nupkg; do
name=$(basename "$file")
if [[ "$name" =~ ^Verifiable\.[0-9] ]]; then
continue
fi
dotnet nuget push "$file" --source "$GITHUB_FEED" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
done
- name: Push top-level pointer package to GitHub feed
run: |
for file in ./nupkgs/*.nupkg; do
name=$(basename "$file")
if [[ "$name" =~ ^Verifiable\.[0-9] ]]; then
dotnet nuget push "$file" --source "$GITHUB_FEED" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
fi
done
deploy:
name: Create and upload Verifiable release packages to NuGet.org
permissions:
packages: write
id-token: write # Required for NuGet trusted publishing (OIDC) and Sigstore attestation.
needs: [ build, pack-cli-pointer ]
if: ${{ github.event_name == 'release' && github.repository_owner == 'lumoin' }}
runs-on: ubuntu-latest
# Bind publishing to the "Release" deployment environment so the OIDC token carries the
# environment claim nuget.org trusted publishing requires, and deployments are recorded
# against it.
environment: Release
steps:
- name: Harden Runner
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: ${{ env.COMMON_ALLOWED_ENDPOINTS }}
- name: Download library NuGet packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-libraries
path: ./nupkgs
- name: Download CLI NuGet packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: nupkg-cli
path: ./nupkgs
# RID-specific packages must arrive at the feed before the pointer package.
# The pointer package filename matches Verifiable.<version>.nupkg where the
# character after "Verifiable." is a digit. All other Verifiable.*.nupkg
# files (RID packages, library packages) are safe to push first.
- name: Push RID and library packages to GitHub feed
run: |
for file in ./nupkgs/*.nupkg; do
name=$(basename "$file")
if [[ "$name" =~ ^Verifiable\.[0-9] ]]; then
continue
fi
dotnet nuget push "$file" --source "$GITHUB_FEED" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
done
- name: Push top-level pointer package to GitHub feed
run: |
for file in ./nupkgs/*.nupkg; do
name=$(basename "$file")
if [[ "$name" =~ ^Verifiable\.[0-9] ]]; then
dotnet nuget push "$file" --source "$GITHUB_FEED" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
fi
done
# NuGet trusted publishing: exchange the GitHub OIDC token for a
# short-lived API key.
- name: NuGet trusted publishing login (OIDC)
id: nuget_login
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841
with:
# nuget.org profile name (not email). Store as a secret to avoid
# hardcoding an account name in the workflow file.
user: ${{ secrets.NUGET_USER }}
- name: Push RID and library packages to NuGet.org
run: |
for file in ./nupkgs/*.nupkg; do
name=$(basename "$file")
if [[ "$name" =~ ^Verifiable\.[0-9] ]]; then
continue
fi
dotnet nuget push "$file" --source "$NUGET_FEED" --api-key "${{ steps.nuget_login.outputs.NUGET_API_KEY }}" --skip-duplicate
done
- name: Push top-level pointer package to NuGet.org
run: |
for file in ./nupkgs/*.nupkg; do
name=$(basename "$file")
if [[ "$name" =~ ^Verifiable\.[0-9] ]]; then
dotnet nuget push "$file" --source "$NUGET_FEED" --api-key "${{ steps.nuget_login.outputs.NUGET_API_KEY }}" --skip-duplicate
fi
done