Support linking the SDKs against secretspec-ffi via pkg-config #49
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: ".NET SDK" | |
| # Builds the C# client and the runtime-specific secretspec-ffi libraries packed | |
| # into Cachix.SecretSpec. Glibc Linux uses a manylinux_2_28 baseline; Alpine | |
| # receives separate musl assets. | |
| on: | |
| workflow_call: | |
| inputs: | |
| publish: | |
| description: Publish the built package to NuGet | |
| required: false | |
| type: boolean | |
| default: false | |
| workflow_dispatch: | |
| inputs: | |
| publish: | |
| description: Publish the built package to NuGet | |
| required: false | |
| type: boolean | |
| default: false | |
| push: | |
| tags: | |
| - v** | |
| pull_request: | |
| paths: | |
| - "secretspec-dotnet/**" | |
| - "secretspec-ffi/**" | |
| - ".github/workflows/dotnet-package.yml" | |
| - "scripts/sync-sdk-versions.sh" | |
| permissions: | |
| contents: read | |
| jobs: | |
| native: | |
| name: ${{ matrix.rid }} | |
| runs-on: ${{ matrix.runner }} | |
| container: ${{ matrix.container || null }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - rid: linux-x64 | |
| target: x86_64-unknown-linux-gnu | |
| runner: ubuntu-latest | |
| container: quay.io/pypa/manylinux_2_28_x86_64 | |
| library: libsecretspec_ffi.so | |
| rustflags: -C strip=symbols | |
| - rid: linux-arm64 | |
| target: aarch64-unknown-linux-gnu | |
| runner: ubuntu-24.04-arm | |
| container: quay.io/pypa/manylinux_2_28_aarch64 | |
| library: libsecretspec_ffi.so | |
| rustflags: -C strip=symbols | |
| - rid: osx-x64 | |
| target: x86_64-apple-darwin | |
| runner: macos-15-intel | |
| library: libsecretspec_ffi.dylib | |
| deployment_target: "12.0" | |
| rustflags: -C strip=symbols | |
| - rid: osx-arm64 | |
| target: aarch64-apple-darwin | |
| runner: macos-latest | |
| library: libsecretspec_ffi.dylib | |
| deployment_target: "12.0" | |
| rustflags: -C strip=symbols | |
| - rid: win-x64 | |
| target: x86_64-pc-windows-msvc | |
| runner: windows-latest | |
| library: secretspec_ffi.dll | |
| rustflags: -C strip=symbols -C target-feature=+crt-static | |
| - rid: win-arm64 | |
| target: aarch64-pc-windows-msvc | |
| runner: windows-11-arm | |
| library: secretspec_ffi.dll | |
| rustflags: -C strip=symbols -C target-feature=+crt-static | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Sync SDK package versions | |
| shell: bash | |
| run: bash scripts/sync-sdk-versions.sh | |
| - name: Install ICU in manylinux (the .NET runtime needs it) | |
| if: matrix.container | |
| shell: bash | |
| run: yum install -y libicu | |
| - name: Install rustup in manylinux | |
| if: matrix.container | |
| shell: bash | |
| run: | | |
| curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | | |
| sh -s -- -y --default-toolchain none | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| - name: Install Rust | |
| run: rustup toolchain install | |
| - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Build native resolver | |
| shell: bash | |
| env: | |
| MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} | |
| RUSTFLAGS: ${{ matrix.rustflags }} | |
| run: >- | |
| cargo build -p secretspec-ffi --release | |
| --target ${{ matrix.target }} | |
| - name: Verify glibc portability (glibc <= 2.28, no libdbus) | |
| if: matrix.container | |
| shell: bash | |
| run: >- | |
| bash scripts/check-linux-portability.sh | |
| "target/${{ matrix.target }}/release/${{ matrix.library }}" | |
| - name: Verify Windows CRT is statically linked | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| run: | | |
| rustup component add llvm-tools-preview | |
| host="$(rustc -vV | sed -n 's/^host: //p')" | |
| llvm_objdump="$(rustc --print sysroot)/lib/rustlib/$host/bin/llvm-objdump" | |
| imports="$("$llvm_objdump" -p \ | |
| "target/${{ matrix.target }}/release/${{ matrix.library }}")" | |
| if grep -Eiq 'DLL Name: (VCRUNTIME|MSVCP)' <<<"$imports"; then | |
| echo "the packaged resolver still depends on the MSVC runtime" >&2 | |
| grep -Ei 'DLL Name: (VCRUNTIME|MSVCP)' <<<"$imports" >&2 | |
| exit 1 | |
| fi | |
| - name: Run C# SDK tests against native resolver | |
| shell: bash | |
| env: | |
| SECRETSPEC_FFI_LIB: ${{ github.workspace }}/target/${{ matrix.target }}/release/${{ matrix.library }} | |
| run: >- | |
| dotnet run | |
| --project secretspec-dotnet/tests/SecretSpec.Tests | |
| --configuration Release | |
| - name: Stage native NuGet asset | |
| shell: bash | |
| run: | | |
| mkdir -p "staged/${{ matrix.rid }}/native" | |
| cp "target/${{ matrix.target }}/release/${{ matrix.library }}" \ | |
| "staged/${{ matrix.rid }}/native/" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dotnet-native-${{ matrix.rid }} | |
| path: staged/${{ matrix.rid }} | |
| musl: | |
| name: ${{ matrix.rid }} | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - rid: linux-musl-x64 | |
| target: x86_64-unknown-linux-musl | |
| runner: ubuntu-latest | |
| image: quay.io/pypa/musllinux_1_2_x86_64 | |
| - rid: linux-musl-arm64 | |
| target: aarch64-unknown-linux-musl | |
| runner: ubuntu-24.04-arm | |
| image: quay.io/pypa/musllinux_1_2_aarch64 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Sync SDK package versions | |
| run: bash scripts/sync-sdk-versions.sh | |
| - name: Build dynamically loadable musl resolver | |
| shell: bash | |
| run: | | |
| docker run --rm \ | |
| --volume "$GITHUB_WORKSPACE:/workspace" \ | |
| --workdir /workspace \ | |
| --env CARGO_TARGET_DIR=/workspace/target \ | |
| --env "RUSTFLAGS=-C target-feature=-crt-static -C strip=symbols" \ | |
| "${{ matrix.image }}" \ | |
| bash -c ' | |
| set -euo pipefail | |
| curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | | |
| sh -s -- -y --default-toolchain none | |
| export PATH="$HOME/.cargo/bin:$PATH" | |
| rustup toolchain install | |
| cargo build -p secretspec-ffi --release \ | |
| --target "${{ matrix.target }}" | |
| ' | |
| test -f \ | |
| "target/${{ matrix.target }}/release/libsecretspec_ffi.so" | |
| - name: Verify musl portability | |
| shell: bash | |
| run: | | |
| library="target/${{ matrix.target }}/release/libsecretspec_ffi.so" | |
| dynamic="$(readelf -d "$library")" | |
| needed="$(grep NEEDED <<<"$dynamic")" | |
| case "${{ matrix.target }}" in | |
| x86_64-unknown-linux-musl) | |
| expected_libc=libc.musl-x86_64.so.1 | |
| ;; | |
| aarch64-unknown-linux-musl) | |
| expected_libc=libc.musl-aarch64.so.1 | |
| ;; | |
| esac | |
| # ARM musl's libgcc_s exports a compatibility symbol version named | |
| # GLIBC_2.0, so inspect the actual dynamic dependencies instead. | |
| if grep -q '\[libc\.so\.6\]' <<<"$needed" || | |
| ! grep -Fq "[$expected_libc]" <<<"$needed"; then | |
| echo "$library does not use the expected musl libc" >&2 | |
| echo "$needed" >&2 | |
| exit 1 | |
| fi | |
| if grep -q dbus <<<"$needed"; then | |
| echo "$library links libdbus dynamically" >&2 | |
| echo "$needed" >&2 | |
| exit 1 | |
| fi | |
| - name: Stage native NuGet asset | |
| run: | | |
| mkdir -p "staged/${{ matrix.rid }}/native" | |
| cp "target/${{ matrix.target }}/release/libsecretspec_ffi.so" \ | |
| "staged/${{ matrix.rid }}/native/" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dotnet-native-${{ matrix.rid }} | |
| path: staged/${{ matrix.rid }} | |
| package: | |
| name: NuGet package | |
| needs: [native, musl] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Sync SDK package versions | |
| run: bash scripts/sync-sdk-versions.sh | |
| - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: dotnet-native-* | |
| path: staged | |
| - name: Place runtime assets | |
| shell: bash | |
| run: | | |
| for artifact in staged/dotnet-native-*; do | |
| rid="${artifact##*/dotnet-native-}" | |
| mkdir -p "secretspec-dotnet/src/SecretSpec/runtimes/$rid" | |
| cp -R "$artifact/"* "secretspec-dotnet/src/SecretSpec/runtimes/$rid/" | |
| done | |
| - name: Pack | |
| run: >- | |
| dotnet pack secretspec-dotnet/src/SecretSpec/SecretSpec.csproj | |
| --configuration Release --output artifacts | |
| - name: Verify runtime assets are present | |
| shell: bash | |
| run: | | |
| # The staged directories come from the same matrix that builds the | |
| # native libraries, so this check cannot drift from the matrix. | |
| package="$(find artifacts -name '*.nupkg' ! -name '*.symbols.nupkg' -print -quit)" | |
| shopt -s nullglob | |
| staged=(staged/dotnet-native-*) | |
| if [ "${#staged[@]}" -eq 0 ]; then | |
| echo "no staged native artifacts were downloaded" >&2 | |
| exit 1 | |
| fi | |
| for artifact in "${staged[@]}"; do | |
| rid="${artifact##*/dotnet-native-}" | |
| unzip -l "$package" | grep -q "runtimes/$rid/native/" | |
| done | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dotnet-nuget | |
| path: artifacts/*.nupkg | |
| consumer: | |
| name: consume ${{ matrix.rid }} | |
| needs: [package] | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - rid: linux-x64 | |
| runner: ubuntu-latest | |
| musl: false | |
| - rid: linux-arm64 | |
| runner: ubuntu-24.04-arm | |
| musl: false | |
| - rid: linux-musl-x64 | |
| runner: ubuntu-latest | |
| musl: true | |
| - rid: linux-musl-arm64 | |
| runner: ubuntu-24.04-arm | |
| musl: true | |
| - rid: osx-x64 | |
| runner: macos-15-intel | |
| musl: false | |
| - rid: osx-arm64 | |
| runner: macos-latest | |
| musl: false | |
| - rid: win-x64 | |
| runner: windows-latest | |
| musl: false | |
| - rid: win-arm64 | |
| runner: windows-11-arm | |
| musl: false | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dotnet-nuget | |
| path: artifacts | |
| - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| if: matrix.musl == false | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Install Linux NativeAOT toolchain | |
| if: runner.os == 'Linux' && matrix.musl == false | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang zlib1g-dev | |
| - name: Restore, run, and NativeAOT-publish a clean NuGet consumer | |
| if: matrix.musl == false && runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="$(sed -n \ | |
| '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' \ | |
| Cargo.toml)" | |
| test_root="$RUNNER_TEMP/SecretSpec.PackageSmoke" | |
| package_source="$GITHUB_WORKSPACE/artifacts" | |
| export NUGET_PACKAGES="$RUNNER_TEMP/nuget-packages" | |
| cp -R secretspec-dotnet/tests/SecretSpec.PackageSmoke "$test_root" | |
| project="$test_root/SecretSpec.PackageSmoke.csproj" | |
| # Prime an isolated package cache from the artifact-only source. The | |
| # RID restore then adds NuGet.org for Microsoft runtime/AOT packs | |
| # without any chance of selecting the already-published bootstrap. | |
| dotnet restore "$project" --source "$package_source" \ | |
| -p:Version="$version" | |
| dotnet restore "$project" --runtime "${{ matrix.rid }}" \ | |
| --source "$package_source" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| -p:Version="$version" | |
| ( | |
| cd "$RUNNER_TEMP" | |
| unset SECRETSPEC_FFI_LIB | |
| dotnet run --project "$project" --configuration Release \ | |
| --runtime "${{ matrix.rid }}" --no-restore -p:Version="$version" | |
| ) | |
| publish_dir="$RUNNER_TEMP/publish-${{ matrix.rid }}" | |
| dotnet publish "$project" --configuration Release \ | |
| --runtime "${{ matrix.rid }}" --self-contained true \ | |
| -p:PublishAot=true -p:Version="$version" \ | |
| --source "$package_source" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --output "$publish_dir" | |
| app="$publish_dir/SecretSpec.PackageSmoke" | |
| if [[ -f "$app.exe" ]]; then app="$app.exe"; fi | |
| ( | |
| cd "$RUNNER_TEMP" | |
| unset SECRETSPEC_FFI_LIB | |
| "$app" | |
| ) | |
| - name: Restore, run, and NativeAOT-publish a clean NuGet consumer on Windows | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $versionMatch = Select-String -Path Cargo.toml ` | |
| -Pattern '^version = "([^"]+)"' | Select-Object -First 1 | |
| if (-not $versionMatch) { | |
| throw "workspace package version not found" | |
| } | |
| $version = $versionMatch.Matches[0].Groups[1].Value | |
| $testRoot = Join-Path $env:RUNNER_TEMP "SecretSpec.PackageSmoke" | |
| $packageSource = Join-Path $env:GITHUB_WORKSPACE "artifacts" | |
| $env:NUGET_PACKAGES = Join-Path $env:RUNNER_TEMP "nuget-packages" | |
| New-Item -ItemType Directory -Path $testRoot | Out-Null | |
| Copy-Item -Recurse ` | |
| "secretspec-dotnet/tests/SecretSpec.PackageSmoke/*" $testRoot | |
| $project = Join-Path $testRoot "SecretSpec.PackageSmoke.csproj" | |
| # Prime an isolated package cache from the artifact-only source. The | |
| # RID/AOT restore then uses the runner's standard NuGet configuration | |
| # for Microsoft runtime packs without selecting a published SDK copy. | |
| dotnet restore $project --source $packageSource "-p:Version=$version" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| dotnet restore $project --runtime "${{ matrix.rid }}" ` | |
| "-p:PublishAot=true" "-p:Version=$version" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| Remove-Item Env:SECRETSPEC_FFI_LIB -ErrorAction SilentlyContinue | |
| Push-Location $env:RUNNER_TEMP | |
| try { | |
| dotnet run --project $project --configuration Release ` | |
| --runtime "${{ matrix.rid }}" --no-restore ` | |
| "-p:Version=$version" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| } finally { | |
| Pop-Location | |
| } | |
| $publishDir = Join-Path ` | |
| $env:RUNNER_TEMP "publish-${{ matrix.rid }}" | |
| dotnet publish $project --configuration Release ` | |
| --runtime "${{ matrix.rid }}" --self-contained true ` | |
| "-p:PublishAot=true" "-p:Version=$version" ` | |
| --no-restore --output $publishDir | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| $app = Join-Path $publishDir "SecretSpec.PackageSmoke.exe" | |
| Push-Location $env:RUNNER_TEMP | |
| try { | |
| & $app | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| } finally { | |
| Pop-Location | |
| } | |
| - name: Restore, run, and NativeAOT-publish on Alpine | |
| if: matrix.musl | |
| shell: bash | |
| run: | | |
| docker run --rm \ | |
| --volume "$GITHUB_WORKSPACE:/workspace" \ | |
| --volume "$RUNNER_TEMP:/runner" \ | |
| --workdir /runner \ | |
| mcr.microsoft.com/dotnet/sdk:8.0-alpine \ | |
| sh -c ' | |
| set -eu | |
| apk add --no-cache build-base clang zlib-dev | |
| version="$(sed -n \ | |
| "/^\[workspace.package\]/,/^\[/s/^version = \"\(.*\)\"/\1/p" \ | |
| /workspace/Cargo.toml)" | |
| test_root=/runner/SecretSpec.PackageSmoke | |
| package_source=/workspace/artifacts | |
| export NUGET_PACKAGES=/runner/nuget-packages | |
| cp -R /workspace/secretspec-dotnet/tests/SecretSpec.PackageSmoke \ | |
| "$test_root" | |
| project="$test_root/SecretSpec.PackageSmoke.csproj" | |
| # Prime the isolated cache from the just-built package before | |
| # NuGet.org is enabled for Microsoft runtime/AOT packs. | |
| dotnet restore "$project" --source "$package_source" \ | |
| -p:Version="$version" | |
| dotnet restore "$project" --runtime "${{ matrix.rid }}" \ | |
| --source "$package_source" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| -p:Version="$version" | |
| unset SECRETSPEC_FFI_LIB | |
| dotnet run --project "$project" --configuration Release \ | |
| --runtime "${{ matrix.rid }}" --no-restore -p:Version="$version" | |
| publish_dir=/runner/publish-${{ matrix.rid }} | |
| dotnet publish "$project" --configuration Release \ | |
| --runtime "${{ matrix.rid }}" --self-contained true \ | |
| -p:PublishAot=true -p:Version="$version" \ | |
| --source "$package_source" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --output "$publish_dir" | |
| cd /runner | |
| unset SECRETSPEC_FFI_LIB | |
| "$publish_dir/SecretSpec.PackageSmoke" | |
| ' | |
| publish: | |
| name: publish to NuGet | |
| if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || inputs.publish | |
| needs: [consumer] | |
| runs-on: ubuntu-latest | |
| # Must match the "Environment" on nuget.org's trusted publishing policy | |
| # for Cachix.SecretSpec (see RELEASE.md); nuget.org matches policies by | |
| # repo + workflow filename + environment. | |
| environment: nuget | |
| permissions: | |
| id-token: write # NuGet Trusted Publishing (OIDC), no long-lived key | |
| contents: read | |
| steps: | |
| - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dotnet-nuget | |
| path: artifacts | |
| # Trades the job's OIDC token for an API key that expires in about an | |
| # hour, so it must stay immediately before the push. | |
| - name: NuGet login (OIDC trusted publishing) | |
| uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 | |
| id: login | |
| with: | |
| user: ${{ secrets.NUGET_USER }} | |
| - name: Publish package | |
| run: >- | |
| dotnet nuget push "artifacts/*.nupkg" | |
| --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" | |
| --source https://api.nuget.org/v3/index.json | |
| --skip-duplicate | |
| - name: Unlist the unsupported 0.15.0 bootstrap | |
| shell: bash | |
| run: | | |
| package="$(find artifacts -name '*.nupkg' \ | |
| ! -name '*.symbols.nupkg' -print -quit)" | |
| if [[ "$package" == *Cachix.SecretSpec.0.15.0.nupkg ]]; then | |
| echo "keeping the bootstrap listed until 0.16.0 is published" | |
| exit 0 | |
| fi | |
| listed="$(curl -fsSL \ | |
| https://api.nuget.org/v3/registration5-semver1/cachix.secretspec/0.15.0.json | | |
| python3 -c 'import json, sys; print(str(json.load(sys.stdin)["listed"]).lower())')" | |
| if [[ "$listed" != true ]]; then | |
| echo "the 0.15.0 bootstrap is already unlisted" | |
| exit 0 | |
| fi | |
| dotnet nuget delete Cachix.SecretSpec 0.15.0 \ | |
| --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --non-interactive |