Merge pull request #24 from ShikeChen-MS/fixes #23
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: Build AOT CLI (cross-platform) | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| configuration: | |
| description: 'Build configuration' | |
| type: choice | |
| required: true | |
| default: 'Release' | |
| options: | |
| - Release | |
| - Debug | |
| # Run on every push to master so a regression never sits unbuilt. | |
| # paths-ignore skips runs when a push contains ONLY documentation | |
| # changes — Markdown files and the LICENSE. Any file outside this | |
| # list (source, tests, workflows, project files, .gitignore, etc.) | |
| # still triggers a full build so we don't accidentally let a code | |
| # change merge unbuilt. If the next push reverts to a code change, | |
| # the next scheduled run also re-exercises the full pipeline within | |
| # 20 days at the latest. | |
| push: | |
| branches: | |
| - master | |
| paths-ignore: | |
| - '**/*.md' | |
| - 'LICENSE' | |
| # Periodic refresh: runs at 06:00 UTC on the 1st and 21st of every | |
| # month. Worst-case gap between runs is exactly 20 days (day 1 to | |
| # day 21 within the same month), so the build is exercised at most | |
| # 20 days apart even if nothing is pushed. | |
| schedule: | |
| - cron: '0 6 1,21 * *' | |
| permissions: | |
| contents: read | |
| # Needed by actions/attest-build-provenance to mint the Sigstore OIDC | |
| # token and to record the attestation in the repo's attestation log. | |
| id-token: write | |
| attestations: write | |
| env: | |
| PROJECT: src/JwtDecoder/JwtDecoder.csproj | |
| PROJECT_JWKSFETCH: src/JwksFetch/JwksFetch.csproj | |
| # Push and schedule events have no workflow inputs, so fall back to | |
| # 'Release' for non-dispatch runs. | |
| CONFIGURATION: ${{ inputs.configuration || 'Release' }} | |
| jobs: | |
| build: | |
| name: ${{ matrix.label }} | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| # Don't cancel siblings if one platform fails; we still want the others. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - label: windows-x64 | |
| runner: windows-latest | |
| rid: win-x64 | |
| exe: jwtdecode.exe | |
| jwksfetch_exe: jwksfetch.exe | |
| - label: windows-arm64 | |
| runner: windows-11-arm | |
| rid: win-arm64 | |
| exe: jwtdecode.exe | |
| jwksfetch_exe: jwksfetch.exe | |
| - label: linux-x64 | |
| runner: ubuntu-latest | |
| rid: linux-x64 | |
| exe: jwtdecode | |
| jwksfetch_exe: jwksfetch | |
| - label: linux-arm64 | |
| runner: ubuntu-24.04-arm | |
| rid: linux-arm64 | |
| exe: jwtdecode | |
| jwksfetch_exe: jwksfetch | |
| - label: macos-arm64 | |
| runner: macos-latest | |
| rid: osx-arm64 | |
| exe: jwtdecode | |
| jwksfetch_exe: jwksfetch | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET SDKs (8 for Core/PS module, 10 for CLI + tests) | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: | | |
| 8.0.x | |
| 10.0.x | |
| - name: Show .NET info | |
| shell: pwsh | |
| run: dotnet --info | |
| # Native AOT on Linux needs clang + zlib headers for the final native link. | |
| # ubuntu-latest typically already has them; install is idempotent and cheap. | |
| - name: Install Linux AOT dependencies | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang zlib1g-dev | |
| # Run the full unit-test suite (Core library + CLI argument parser) | |
| # on this matrix entry's runner BEFORE any AOT publish or artifact | |
| # upload. A failure here aborts the job and the artifact step never | |
| # runs, so a regression cannot ship. Runs on every platform so a | |
| # platform-specific crypto regression cannot slip past one runner. | |
| # | |
| # `-m:1` limits MSBuild to a single worker process to avoid a race | |
| # where multiple test projects rebuild the shared JwtDecoder.Core | |
| # in parallel and collide writing its intermediate dll / deps.json | |
| # (reproduces on .NET SDK 10.0.301 Linux and Windows runners). | |
| # Tests within a project still parallelise via xunit so wall-clock | |
| # cost is negligible (build ~6s on a 16-core runner). | |
| - name: Run unit tests (dotnet test) | |
| shell: pwsh | |
| run: dotnet test -c $env:CONFIGURATION --logger "console;verbosity=normal" -m:1 | |
| - name: Restore | |
| shell: pwsh | |
| run: dotnet restore "$env:PROJECT" -r ${{ matrix.rid }} | |
| - name: Publish (Native AOT) | |
| shell: pwsh | |
| run: > | |
| dotnet publish "$env:PROJECT" | |
| -c $env:CONFIGURATION | |
| -r ${{ matrix.rid }} | |
| --no-restore | |
| - name: Locate published binary | |
| id: locate | |
| shell: pwsh | |
| run: | | |
| $publishDir = "src/JwtDecoder/bin/$env:CONFIGURATION/net10.0/${{ matrix.rid }}/publish" | |
| $bin = Join-Path $publishDir '${{ matrix.exe }}' | |
| if (-not (Test-Path $bin)) { | |
| Write-Error "Expected output not found: $bin" | |
| if (Test-Path $publishDir) { | |
| Get-ChildItem -Recurse $publishDir | Format-Table FullName, Length | |
| } | |
| exit 1 | |
| } | |
| # Make sure the binary is executable on Unix runners (publish usually does | |
| # this already, but we set it explicitly so the smoke test never fails for | |
| # a permissions reason). | |
| if ($IsLinux -or $IsMacOS) { chmod +x $bin } | |
| $sizeMB = [math]::Round((Get-Item $bin).Length / 1MB, 2) | |
| Write-Host "Built ${{ matrix.exe }} ($sizeMB MB) for ${{ matrix.rid }}" | |
| "binPath=$bin" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| # Every matrix entry runs on its own native architecture, so the | |
| # functional tests run unconditionally. They exercise every supported | |
| # algorithm family plus the documented security hardening (algorithm | |
| # confusion, private-key refusal, curve binding, oversized input, | |
| # duplicate header keys, terminal-injection guard, alg=none, ...). | |
| - name: Functional tests (samples/) | |
| shell: pwsh | |
| run: ./tools/Test-AotBuild.ps1 -BinPath "${{ steps.locate.outputs.binPath }}" | |
| # -------------------------------------------------------------- | |
| # Offline-guarantee verification (Phase 6). | |
| # Asserts that JwtDecoder.Core.dll, the pre-AOT jwtdecode.dll, and | |
| # the published native jwtdecode binary have no networking | |
| # references. Four independent layers (managed IL grep via | |
| # ilspycmd, native imports via dumpbin/objdump/otool, raw-bytes | |
| # string scan, transitive NuGet package check) plus a SHA-256 | |
| # equality check between the file we inspect and the file we | |
| # upload. All disassembly + imports listings are uploaded as a | |
| # transparency artifact for downstream auditors. | |
| # -------------------------------------------------------------- | |
| - name: Install ilspycmd | |
| shell: pwsh | |
| # Pin version for supply-chain hygiene; bump when validating a newer | |
| # 10.x release of ICSharpCode/ILSpy. | |
| run: dotnet tool install -g ilspycmd --version 10.1.0.8386 | |
| - name: Locate dumpbin (Windows only) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| # The [B] layer of Verify-OfflineGuarantee.ps1 invokes `dumpbin` | |
| # to inspect the AOT exe's native imports. dumpbin ships with | |
| # MSVC but is NOT on PATH by default on either windows-latest | |
| # or windows-11-arm; we have to locate it via vswhere and add | |
| # the directory to GITHUB_PATH for the next step. Linux/macOS | |
| # use objdump/otool which are always on PATH so this step is | |
| # skipped there. | |
| run: | | |
| $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" | |
| if (-not (Test-Path $vswhere)) { | |
| Write-Error "vswhere.exe not found at $vswhere" | |
| exit 1 | |
| } | |
| $vsInstall = & $vswhere -latest -property installationPath | |
| if (-not $vsInstall) { | |
| Write-Error "No Visual Studio installation found by vswhere." | |
| exit 1 | |
| } | |
| # Prefer an architecture-native dumpbin (HostX64\x64 on x64, | |
| # HostARM64\arm64 on arm64). Cross-host variants work too but | |
| # native is faster and more compatible. Fall back to ANY | |
| # dumpbin so a future MSVC layout change doesn't silently | |
| # break the verifier. | |
| $hostPattern = if ($env:RUNNER_ARCH -eq 'ARM64') { | |
| 'HostARM64(\\|/)arm64' | |
| } else { | |
| 'Host[xX]64(\\|/)x64' | |
| } | |
| $candidates = Get-ChildItem -Path "$vsInstall\VC\Tools\MSVC" ` | |
| -Recurse -Filter 'dumpbin.exe' -ErrorAction SilentlyContinue | |
| $dumpbin = $candidates | Where-Object { $_.FullName -match $hostPattern } | Select-Object -First 1 | |
| if (-not $dumpbin) { $dumpbin = $candidates | Select-Object -First 1 } | |
| if (-not $dumpbin) { | |
| Write-Error "dumpbin.exe not found under $vsInstall\VC\Tools\MSVC" | |
| exit 1 | |
| } | |
| Write-Host "Found dumpbin: $($dumpbin.FullName)" | |
| Add-Content -Path $env:GITHUB_PATH -Value $dumpbin.Directory.FullName | |
| - name: Verify offline guarantee (jwtdecode) | |
| shell: pwsh | |
| run: | | |
| # Ensure the global tool dir is on PATH for this step. | |
| $tools = if ($IsWindows) { Join-Path $env:USERPROFILE '.dotnet\tools' } else { Join-Path $env:HOME '.dotnet/tools' } | |
| $env:PATH = "$tools$([System.IO.Path]::PathSeparator)$env:PATH" | |
| # The pre-AOT managed dll lives at .../net10.0/<rid>/jwtdecode.dll. | |
| $managedCli = "src/JwtDecoder/bin/$env:CONFIGURATION/net10.0/${{ matrix.rid }}/jwtdecode.dll" | |
| if (-not (Test-Path $managedCli)) { | |
| Write-Error "pre-AOT managed assembly not found at $managedCli" | |
| exit 1 | |
| } | |
| ./tools/Verify-OfflineGuarantee.ps1 ` | |
| -CoreDllPath "src/JwtDecoder.Core/bin/$env:CONFIGURATION/net8.0/JwtDecoder.Core.dll" ` | |
| -ManagedCliDllPath $managedCli ` | |
| -AotExePath "${{ steps.locate.outputs.binPath }}" ` | |
| -DisasmOutDir "ci-artifacts/disasm-${{ matrix.rid }}" ` | |
| -UploadHashPath "${{ steps.locate.outputs.binPath }}" | |
| - name: Upload offline-guarantee transparency artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: offline-guarantee-${{ matrix.rid }}-${{ env.CONFIGURATION }}-${{ github.sha }} | |
| path: ci-artifacts/disasm-${{ matrix.rid }} | |
| if-no-files-found: error | |
| retention-days: 30 | |
| # Mint a Sigstore-backed build provenance attestation for the binary | |
| # we just tested. Anyone who downloads the artifact can later run | |
| # `gh attestation verify <file> --owner ShikeChen-MS` to confirm it | |
| # came from this workflow, this repo, and this commit. | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: ${{ steps.locate.outputs.binPath }} | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: jwtdecode-${{ matrix.rid }}-${{ env.CONFIGURATION }}-${{ github.sha }} | |
| path: ${{ steps.locate.outputs.binPath }} | |
| if-no-files-found: error | |
| retention-days: 30 | |
| # -------------------------------------------------------------- | |
| # jwksfetch: the network-capable companion CLI. Same matrix, | |
| # same gating — `dotnet test` above must pass before we get | |
| # here. Built, smoke-tested, attested, and uploaded as a | |
| # SEPARATE artifact so the offline jwtdecode binary remains | |
| # cleanly identifiable on its own. | |
| # -------------------------------------------------------------- | |
| - name: Restore (jwksfetch) | |
| shell: pwsh | |
| run: dotnet restore "$env:PROJECT_JWKSFETCH" -r ${{ matrix.rid }} | |
| - name: Publish jwksfetch (Native AOT) | |
| shell: pwsh | |
| run: > | |
| dotnet publish "$env:PROJECT_JWKSFETCH" | |
| -c $env:CONFIGURATION | |
| -r ${{ matrix.rid }} | |
| --no-restore | |
| - name: Locate jwksfetch binary | |
| id: locate_jwksfetch | |
| shell: pwsh | |
| run: | | |
| $publishDir = "src/JwksFetch/bin/$env:CONFIGURATION/net10.0/${{ matrix.rid }}/publish" | |
| $bin = Join-Path $publishDir '${{ matrix.jwksfetch_exe }}' | |
| if (-not (Test-Path $bin)) { | |
| Write-Error "Expected output not found: $bin" | |
| if (Test-Path $publishDir) { | |
| Get-ChildItem -Recurse $publishDir | Format-Table FullName, Length | |
| } | |
| exit 1 | |
| } | |
| if ($IsLinux -or $IsMacOS) { chmod +x $bin } | |
| $sizeMB = [math]::Round((Get-Item $bin).Length / 1MB, 2) | |
| Write-Host "Built ${{ matrix.jwksfetch_exe }} ($sizeMB MB) for ${{ matrix.rid }}" | |
| "binPath=$bin" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| # Sample-driven functional tests for jwksfetch. Exercises help/version, | |
| # argument validation, the --jwks-file happy path, an end-to-end pipe | |
| # into jwtdecode --key-file -, HTTPS-only refusal, and kid-mismatch | |
| # exit-code mapping. The artifact upload below only runs if every | |
| # case passes. | |
| - name: Functional tests (jwksfetch) | |
| shell: pwsh | |
| run: > | |
| ./tools/Test-JwksFetchBuild.ps1 | |
| -BinPath "${{ steps.locate_jwksfetch.outputs.binPath }}" | |
| -JwtDecodeBinPath "${{ steps.locate.outputs.binPath }}" | |
| - name: Attest build provenance (jwksfetch) | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: ${{ steps.locate_jwksfetch.outputs.binPath }} | |
| - name: Upload artifact (jwksfetch) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: jwksfetch-${{ matrix.rid }}-${{ env.CONFIGURATION }}-${{ github.sha }} | |
| path: ${{ steps.locate_jwksfetch.outputs.binPath }} | |
| if-no-files-found: error | |
| retention-days: 30 |