Verifiable .NET build #3492
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |