From 76fb034f5123ac814c896fd22580efda886108ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:37:19 +0000 Subject: [PATCH] Bump the actions-minor-and-patch group across 2 directories with 2 updates Bumps the actions-minor-and-patch group with 1 update in the / directory: [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action). Bumps the actions-minor-and-patch group with 1 update in the /.github/actions/setup-dotnet directory: [actions/setup-dotnet](https://github.com/actions/setup-dotnet). Updates `zizmorcore/zizmor-action` from 0.8.0 to 0.8.1 - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/53290badfa369cb0bf7ad16342cb62c2fa47ac7b...45d7276c9ca790aabd705e31ca99c6185c3e76dd) Updates `actions/setup-dotnet` from 6.0.0 to 6.1.0 - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/a98b56852c35b8e3190ac28c8c2271da59106c68...0f4a354dd5547a28c70b7853dbd95d5b67886e73) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.8.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-and-patch - dependency-name: actions/setup-dotnet dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-and-patch ... Signed-off-by: dependabot[bot] --- .github/actions/setup-dotnet/action.yml | 102 +-- .github/workflows/ci.yml | 956 ++++++++++++------------ .github/workflows/zizmor-online.yml | 94 +-- 3 files changed, 576 insertions(+), 576 deletions(-) diff --git a/.github/actions/setup-dotnet/action.yml b/.github/actions/setup-dotnet/action.yml index 2e364fb1..b0855892 100644 --- a/.github/actions/setup-dotnet/action.yml +++ b/.github/actions/setup-dotnet/action.yml @@ -1,51 +1,51 @@ -name: Setup .NET -description: >- - Installs the exact .NET SDK pinned by global.json, quiets the CLI and restores the listed solutions or projects in - locked mode. - -inputs: - restore: - description: >- - Newline-separated solutions or projects, each restored with 'dotnet restore --locked-mode'. Empty skips - the restore. - required: false - default: '' - cache: - description: >- - Cache the NuGet global-packages folder, keyed on every packages.lock.json. Keep 'false' in every workflow a - release, Sonar or CodeQL run can reach (a poisoned cache would feed the published packages). - required: false - default: 'false' - -runs: - using: composite - steps: - # global.json sets rollForward: disable, and runner images do not ship the pinned feature band, so every job that - # runs dotnet installs it here. - - name: Install SDK - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - global-json-file: global.json - cache: ${{ inputs.cache }} - cache-dependency-path: '**/packages.lock.json' - - - name: Configure CLI - shell: pwsh - run: | - 'DOTNET_NOLOGO=1', 'DOTNET_CLI_TELEMETRY_OPTOUT=1', 'MSBUILDDISABLENODEREUSE=1' | - Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - - name: Restore (locked) - if: inputs.restore != '' - shell: pwsh - env: - RESTORE_TARGETS: ${{ inputs.restore }} - run: | - $ErrorActionPreference = 'Stop' - $targets = @($env:RESTORE_TARGETS -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - foreach ($target in $targets) { - dotnet restore $target --locked-mode - if ($LASTEXITCODE -ne 0) { - throw "Locked restore of '$target' failed with exit code $LASTEXITCODE. The committed packages.lock.json files do not match the project graph: regenerate them with 'dotnet restore --force-evaluate' (Windows, the .NET SDK of global.json) and commit the result." - } - } +name: Setup .NET +description: >- + Installs the exact .NET SDK pinned by global.json, quiets the CLI and restores the listed solutions or projects in + locked mode. + +inputs: + restore: + description: >- + Newline-separated solutions or projects, each restored with 'dotnet restore --locked-mode'. Empty skips + the restore. + required: false + default: '' + cache: + description: >- + Cache the NuGet global-packages folder, keyed on every packages.lock.json. Keep 'false' in every workflow a + release, Sonar or CodeQL run can reach (a poisoned cache would feed the published packages). + required: false + default: 'false' + +runs: + using: composite + steps: + # global.json sets rollForward: disable, and runner images do not ship the pinned feature band, so every job that + # runs dotnet installs it here. + - name: Install SDK + uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 + with: + global-json-file: global.json + cache: ${{ inputs.cache }} + cache-dependency-path: '**/packages.lock.json' + + - name: Configure CLI + shell: pwsh + run: | + 'DOTNET_NOLOGO=1', 'DOTNET_CLI_TELEMETRY_OPTOUT=1', 'MSBUILDDISABLENODEREUSE=1' | + Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + + - name: Restore (locked) + if: inputs.restore != '' + shell: pwsh + env: + RESTORE_TARGETS: ${{ inputs.restore }} + run: | + $ErrorActionPreference = 'Stop' + $targets = @($env:RESTORE_TARGETS -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + foreach ($target in $targets) { + dotnet restore $target --locked-mode + if ($LASTEXITCODE -ne 0) { + throw "Locked restore of '$target' failed with exit code $LASTEXITCODE. The committed packages.lock.json files do not match the project graph: regenerate them with 'dotnet restore --force-evaluate' (Windows, the .NET SDK of global.json) and commit the result." + } + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7d60e9b..e5742abf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,478 +1,478 @@ -name: CI - -# Reusable pipeline for pull-request-ci.yml, main-ci.yml and release.yml. Callers use the job id `ci` named `CI`, so -# the only required check is "CI / Gate". -# -# build-test (Debug, Release) ─► sonar ─┐ -# aot ──────────────────────────────────┤ -# lint ─────────────────────────────────┤ -# format ───────────────────────────────┼─► gate -# dependency-review ────────────────────┤ -# lock-files ───────────────────────────┘ -# -# The Release leg packs before it tests: the package-consumption tests install the very files CI uploads. Jobs exchange -# artifacts instead of rebuilding (names reserved, nothing else is uploaded). Normal CI compiles and tests the pinned -# SDK submodule; only the production release path packs and consumes NuGet packages: -# nuget-packages build-test (production Release) → release.yml -# coverage build-test (Debug) → sonar -# test-results- build-test → humans -# test-dumps-, binlogs-* on failure only → humans -# -# No job here uses a NuGet cache: release.yml reaches every job. - -on: - workflow_call: - inputs: - sonar: - description: Run the SonarQube Cloud analysis. Fork pull requests and Dependabot always skip it (SONAR_EXPECTED). - type: boolean - default: false - package-version: - description: When set (release), the Release leg must produce exactly ..nupkg for every Client package. - type: string - default: '' - package-retention-days: - description: Days to keep the nuget-packages artifact. - type: number - default: 7 - production-packages: - description: >- - Build, pack and consume the reviewed NuGet graph for a production release. Ordinary CI builds and tests the - pinned SDK submodule instead. - type: boolean - default: false - secrets: - SONAR_TOKEN: - description: SonarQube Cloud analysis token. Needed only when sonar is true. - required: false - -permissions: - contents: read - -defaults: - run: - shell: pwsh - -jobs: - build-test: - name: Build and test (${{ matrix.configuration }}) - runs-on: windows-2025 - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - configuration: [ Debug, Release ] - env: - CONFIGURATION: ${{ matrix.configuration }} - RESULTS: artifacts/test-results/${{ matrix.configuration }} - PRODUCTION_PACKAGES: ${{ inputs.production-packages }} - CheatEngineSdkSourceMode: ${{ inputs.production-packages && 'false' || 'true' }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 # the package version comes from tags and history - submodules: recursive # normal builds compile the pinned SDK source - persist-credentials: false - - - name: Setup .NET - uses: ./.github/actions/setup-dotnet - with: - restore: CheatEngine.Client.slnx - - # No --warnaserror: it would also promote the NuGet audit warnings replayed at build (NU1901/NU1902) and override - # the audit policy. TreatWarningsAsErrors (Directory.Build.props) keeps compiler and analyzer strictness. - - name: Build - run: | - dotnet build CheatEngine.Client.slnx -c $env:CONFIGURATION --no-restore "-bl:artifacts/logs/build-test-$env:CONFIGURATION.binlog" - if ($LASTEXITCODE -ne 0) { - throw "$env:CONFIGURATION solution build failed with exit code $LASTEXITCODE." - } - - # Every .nupkg must embed its SPDX SBOM (_manifest/spdx_2.2/manifest.spdx.json): the pack generates it - # unconditionally (eng/Shipping.props, eng/Templates.props) and release.yml attests it. The seven ids produce - # exactly one nupkg each and a snupkg for every one but the two with no build output (Client, Templates); when a - # release names the exact version, every file must be named "..nupkg" / ".snupkg". - - name: Pack - id: pack - if: inputs.production-packages && matrix.configuration == 'Release' - env: - PACKAGE_VERSION: ${{ inputs.package-version }} - run: | - $ErrorActionPreference = 'Stop' - dotnet pack CheatEngine.Client.slnx -c Release --no-build -o artifacts/nuget -bl:artifacts/logs/pack-Release.binlog - if ($LASTEXITCODE -ne 0) { - throw "Client package creation failed with exit code $LASTEXITCODE." - } - - # The seven ids, each declared once with whether it ships a symbol package (release.yml lists the same ids in - # PACKAGE_IDS). - $shipsSymbols = [ordered]@{ - 'CheatEngine.Client' = $false - 'CheatEngine.Client.Abstractions' = $true - 'CheatEngine.Client.Core' = $true - 'CheatEngine.Client.Extensions.DependencyInjection' = $true - 'CheatEngine.Client.Fluent' = $true - 'CheatEngine.Client.Hosting' = $true - 'CheatEngine.Client.Templates' = $false - } - $ids = @($shipsSymbols.Keys) - $noSymbols = @($ids | Where-Object { -not $shipsSymbols[$_] }) - $nupkgs = @(Get-ChildItem artifacts/nuget -Filter '*.nupkg') - $snupkgs = @(Get-ChildItem artifacts/nuget -Filter '*.snupkg') - if ($nupkgs.Count -ne $ids.Count -or $snupkgs.Count -ne ($ids.Count - $noSymbols.Count)) { - throw "Expected $($ids.Count) nupkg and $($ids.Count - $noSymbols.Count) snupkg in artifacts/nuget, found $($nupkgs.Count) nupkg and $($snupkgs.Count) snupkg." - } - - Add-Type -AssemblyName System.IO.Compression.FileSystem - foreach ($id in $ids) { - $expectedName = if ($env:PACKAGE_VERSION) { "$id.$env:PACKAGE_VERSION.nupkg" } else { $null } - $nupkg = if ($expectedName) { Join-Path artifacts/nuget $expectedName } else { ($nupkgs | Where-Object BaseName -Match "^$([regex]::Escape($id))\.\d") | Select-Object -First 1 -ExpandProperty FullName } - if (-not $nupkg -or -not (Test-Path -LiteralPath $nupkg -PathType Leaf)) { - throw "artifacts/nuget has no package for $id$(if ($expectedName) { " named $expectedName" })." - } - - $archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $nupkg).Path) - try { - if ($null -eq $archive.GetEntry('_manifest/spdx_2.2/manifest.spdx.json')) { - throw "$nupkg has no embedded SBOM at _manifest/spdx_2.2/manifest.spdx.json." - } - } - finally { - $archive.Dispose() - } - } - - Write-Output "Verified $($nupkgs.Count) nupkg and $($snupkgs.Count) snupkg, each embedding its SBOM." - - # The Release Test step consumes these exact packages (CHEATENGINE_CLIENT_PACKAGE_SOURCE), never a package it - # packs itself: a self-pack would test different bits than the ones this run uploads. - $absolute = (Resolve-Path artifacts/nuget).Path - "package-source=$absolute" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - - # The solution build compiles the benchmarks; listing them proves discovery. Benchmarks never run in the Gate. - - name: Benchmarks discovery - if: matrix.configuration == 'Release' - run: | - $ErrorActionPreference = 'Stop' - $listed = @(dotnet run --project tests/CheatEngine.Client.Benchmarks/CheatEngine.Client.Benchmarks.csproj -c Release --no-build -- --list flat --artifacts artifacts/benchmarks) - if ($LASTEXITCODE -ne 0) { - throw "Benchmark discovery failed with exit code $LASTEXITCODE." - } - $benchmarks = @($listed | Where-Object { $_ -match '^CheatEngine\.Client\.Benchmarks\.' }) - if ($benchmarks.Count -eq 0) { - throw "The benchmark switcher listed no benchmark:`n$($listed -join "`n")" - } - "Discovered $($benchmarks.Count) benchmarks." | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - - # One parallel run over every tests/**/*.Tests module. A skip fails the run (--fail-skips on). Debug collects one - # coverage report per module and excludes package consumption except in the production Release job, never Skip. - # That one job hands the package tests the exact packages the Pack step produced. Both legs exclude live tests - # by trait: they start a sandboxed Cheat Engine and run only on a maintainer workstation that opts in. The hang - # dump fires after 15 minutes without test activity, well below the job timeout. - - name: Test - env: - PACKAGE_SOURCE: ${{ steps.pack.outputs.package-source }} - run: | - $ErrorActionPreference = 'Stop' - $options = @( - '--solution', 'CheatEngine.Client.slnx', '-c', $env:CONFIGURATION, '--no-build', '--results-directory', $env:RESULTS, - '--fail-skips', 'on', '--report-trx', '--report-gh', '--report-gh-groups', 'off', - '--hangdump', '--hangdump-timeout', '15m', '--crashdump', - '--filter-not-trait', 'Category=LiveQualification' - ) - if ($env:PRODUCTION_PACKAGES -ne 'true' -or $env:CONFIGURATION -eq 'Debug') { - $options += '--filter-not-trait', 'Category=PackageConsumption' - } - if ($env:CONFIGURATION -eq 'Debug') { - $options += '--coverage', '--coverage-output-format', 'xml' - } - elseif ($env:PRODUCTION_PACKAGES -eq 'true') { - # Set only here: the smoke tests fail on a set-but-empty value, and the Debug leg has no packages. - if ([string]::IsNullOrEmpty($env:PACKAGE_SOURCE)) { - throw 'The Pack step exported no package-source.' - } - $env:CHEATENGINE_CLIENT_PACKAGE_SOURCE = $env:PACKAGE_SOURCE - } - dotnet test @options - if ($LASTEXITCODE -ne 0) { - throw "$env:CONFIGURATION tests failed with exit code $LASTEXITCODE." - } - - - name: Upload packages - if: inputs.production-packages && matrix.configuration == 'Release' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: nuget-packages - path: | - artifacts/nuget/*.nupkg - artifacts/nuget/*.snupkg - if-no-files-found: error - retention-days: ${{ inputs.package-retention-days }} - - - name: Upload coverage - if: matrix.configuration == 'Debug' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: coverage - path: ${{ env.RESULTS }}/*.xml - if-no-files-found: error - retention-days: 7 - - - name: Upload test results - if: ${{ !cancelled() }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: test-results-${{ matrix.configuration }} - path: ${{ env.RESULTS }}/*.trx - if-no-files-found: warn - retention-days: 7 - - # Crash dumps, hang dumps and the test sequence logs that name the tests running at the time. - - name: Upload test dumps - if: failure() - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: test-dumps-${{ matrix.configuration }} - path: | - ${{ env.RESULTS }}/**/*.dmp - ${{ env.RESULTS }}/**/*.log - if-no-files-found: ignore - retention-days: 5 - - - name: Upload binary logs - if: failure() - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: binlogs-build-test-${{ matrix.configuration }} - path: artifacts/logs/*.binlog - if-no-files-found: ignore - retention-days: 5 - - # A trim and Native AOT graph probe only: it never loads anything into Cheat Engine. - aot: - name: Native AOT publication probe - runs-on: windows-2025 - timeout-minutes: 25 - env: - CheatEngineSdkSourceMode: ${{ inputs.production-packages && 'false' || 'true' }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 # the package version comes from tags and history - submodules: recursive # the AOT graph uses the pinned SDK source - persist-credentials: false - - - name: Setup .NET - uses: ./.github/actions/setup-dotnet - with: - restore: tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj - - # The project pins its RuntimeIdentifier (win-x64), so publish needs no --runtime. - - name: Publish and run Native AOT probe - run: | - $ErrorActionPreference = 'Stop' - $output = Join-Path $PWD 'artifacts/aot-probe' - dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj -c Release --no-restore -o $output -bl:artifacts/logs/aot-publish.binlog - if ($LASTEXITCODE -ne 0) { - throw "Native AOT probe publish failed with exit code $LASTEXITCODE." - } - $probe = Join-Path $output 'CheatEngine.Client.AotProbe.exe' - if (-not (Test-Path -LiteralPath $probe -PathType Leaf)) { - throw "Native AOT publish did not produce '$probe'." - } - & $probe - if ($LASTEXITCODE -ne 0) { - throw "Native AOT probe exited with code $LASTEXITCODE." - } - - - name: Upload binary logs - if: failure() - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: binlogs-aot - path: artifacts/logs/*.binlog - if-no-files-found: ignore - retention-days: 5 - - # Pull requests fail on the quality gate; main only reports it. SONAR_EXPECTED below must stay identical to the gate's. - sonar: - name: Sonar - needs: build-test - if: >- - inputs.sonar - && github.event_name != 'merge_group' - && github.actor != 'dependabot[bot]' - && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - uses: ./.github/workflows/sonar.yml - with: - wait-quality-gate: ${{ github.event_name != 'push' }} - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - lint: - name: Lint - runs-on: ubuntu-24.04 - timeout-minutes: 10 - env: - ACTIONLINT_VERSION: 1.7.12 - ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 # linux_amd64, official checksums file - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Run actionlint - run: | - $ErrorActionPreference = 'Stop' - $archive = Join-Path $env:RUNNER_TEMP 'actionlint.tar.gz' - $url = "https://github.com/rhysd/actionlint/releases/download/v$env:ACTIONLINT_VERSION/actionlint_$($env:ACTIONLINT_VERSION)_linux_amd64.tar.gz" - Invoke-WebRequest -Uri $url -OutFile $archive -MaximumRetryCount 3 -RetryIntervalSec 5 - $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $env:ACTIONLINT_SHA256) { - throw "actionlint $env:ACTIONLINT_VERSION has SHA-256 $actual, expected $env:ACTIONLINT_SHA256." - } - tar -xzf $archive -C $env:RUNNER_TEMP actionlint - if ($LASTEXITCODE -ne 0) { - throw "Extracting actionlint failed with exit code $LASTEXITCODE." - } - & (Join-Path $env:RUNNER_TEMP 'actionlint') -color - if ($LASTEXITCODE -ne 0) { - throw "actionlint failed with exit code $LASTEXITCODE." - } - - # Offline audits only, so the result depends on the commit alone; the online audits run in zizmor-online.yml. - - name: Run zizmor - uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 - with: - version: 1.30.1 - online-audits: false - advanced-security: false - config: .github/zizmor.yml - - format: - name: Format - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup .NET - uses: ./.github/actions/setup-dotnet - - # Whitespace needs no restore or MSBuild workspace, and the CRLF checkout (.gitattributes) matches .editorconfig. - # Style rules are enforced by the build (EnforceCodeStyleInBuild). - - name: Verify whitespace formatting - run: | - dotnet format whitespace . --folder --verify-no-changes --exclude artifacts externals - if ($LASTEXITCODE -ne 0) { - throw "Whitespace formatting differs (exit code $LASTEXITCODE). Fix it with: dotnet format whitespace --folder" - } - - # Always runs, so the Gate never has to accept a skip: the review itself runs on pull requests only. - dependency-review: - name: Dependency review - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Review dependency changes - if: github.event_name == 'pull_request' - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - with: - config-file: ./.github/dependency-review-config.yml - comment-summary-in-pr: never - - - name: Nothing to review - if: github.event_name != 'pull_request' - env: - EVENT: ${{ github.event_name }} - run: Write-Host "::notice title=Dependency review::Dependency review compares a pull request with its base; a $env:EVENT run has nothing to compare." - - lock-files: - name: Lock files - runs-on: windows-2025 - timeout-minutes: 15 - env: - CheatEngineSdkSourceMode: ${{ inputs.production-packages && 'false' || 'true' }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: recursive # locked restore follows the normal source graph - persist-credentials: false - - - name: Setup .NET - uses: ./.github/actions/setup-dotnet - - # Windows only: Native AOT lock sections record host-RID ILCompiler packages. A locked restore fails outright - # when a committed packages.lock.json no longer matches its project graph. - - name: Verify lock files - run: | - dotnet restore CheatEngine.Client.slnx --locked-mode - if ($LASTEXITCODE -ne 0) { - throw "Locked restore failed with exit code $LASTEXITCODE. The committed packages.lock.json files do not match the project graph: regenerate the affected ones with 'dotnet restore --force-evaluate' and commit the result." - } - - gate: - name: Gate - # always(): a failed or cancelled job must turn the required check red instead of skipping it. - if: always() - needs: [ build-test, aot, sonar, lint, format, dependency-review, lock-files ] - runs-on: ubuntu-24.04 - timeout-minutes: 5 - permissions: {} - steps: - - name: Check results - env: - NEEDS: ${{ toJSON(needs) }} - SONAR_EXPECTED: >- - ${{ inputs.sonar - && github.event_name != 'merge_group' - && github.actor != 'dependabot[bot]' - && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} - EVENT: ${{ github.event_name }} - run: | - # Every job must succeed. Only sonar has a required result that depends on the event: success when - # SONAR_EXPECTED is true, skipped otherwise (fork, Dependabot, release call). A sonar run that was not - # expected means its condition drifted from SONAR_EXPECTED, which fails too. - $ErrorActionPreference = 'Stop' - $needs = $env:NEEDS | ConvertFrom-Json -AsHashtable - $sonarExpected = $env:SONAR_EXPECTED -eq 'true' - $failed = [Collections.Generic.List[string]]::new() - $rows = foreach ($job in @($needs.Keys | Sort-Object)) { - $result = $needs[$job].result - $required = 'success' - $reason = 'every job must succeed' - if ($job -eq 'sonar') { - if ($sonarExpected) { - $reason = 'SONAR_EXPECTED=true' - if ($result -eq 'skipped' -and $needs['build-test'].result -ne 'success') { - $reason = 'SONAR_EXPECTED=true; skipped because build-test did not succeed' - } - } - else { - $required = 'skipped' - $reason = "SONAR_EXPECTED=false ($env:EVENT run: fork, Dependabot or release call)" - } - } - if ($result -ne $required) { - $failed.Add("$job ($result, required $required)") - } - "| $job | $result | $required | $reason |" - } - @('### Gate', '', '| Job | Result | Required | Reason |', '| --- | --- | --- | --- |') + $rows | - Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - if ($failed.Count -gt 0) { - Write-Host "::error::Not as required: $($failed -join ', ')." - exit 1 - } +name: CI + +# Reusable pipeline for pull-request-ci.yml, main-ci.yml and release.yml. Callers use the job id `ci` named `CI`, so +# the only required check is "CI / Gate". +# +# build-test (Debug, Release) ─► sonar ─┐ +# aot ──────────────────────────────────┤ +# lint ─────────────────────────────────┤ +# format ───────────────────────────────┼─► gate +# dependency-review ────────────────────┤ +# lock-files ───────────────────────────┘ +# +# The Release leg packs before it tests: the package-consumption tests install the very files CI uploads. Jobs exchange +# artifacts instead of rebuilding (names reserved, nothing else is uploaded). Normal CI compiles and tests the pinned +# SDK submodule; only the production release path packs and consumes NuGet packages: +# nuget-packages build-test (production Release) → release.yml +# coverage build-test (Debug) → sonar +# test-results- build-test → humans +# test-dumps-, binlogs-* on failure only → humans +# +# No job here uses a NuGet cache: release.yml reaches every job. + +on: + workflow_call: + inputs: + sonar: + description: Run the SonarQube Cloud analysis. Fork pull requests and Dependabot always skip it (SONAR_EXPECTED). + type: boolean + default: false + package-version: + description: When set (release), the Release leg must produce exactly ..nupkg for every Client package. + type: string + default: '' + package-retention-days: + description: Days to keep the nuget-packages artifact. + type: number + default: 7 + production-packages: + description: >- + Build, pack and consume the reviewed NuGet graph for a production release. Ordinary CI builds and tests the + pinned SDK submodule instead. + type: boolean + default: false + secrets: + SONAR_TOKEN: + description: SonarQube Cloud analysis token. Needed only when sonar is true. + required: false + +permissions: + contents: read + +defaults: + run: + shell: pwsh + +jobs: + build-test: + name: Build and test (${{ matrix.configuration }}) + runs-on: windows-2025 + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + configuration: [ Debug, Release ] + env: + CONFIGURATION: ${{ matrix.configuration }} + RESULTS: artifacts/test-results/${{ matrix.configuration }} + PRODUCTION_PACKAGES: ${{ inputs.production-packages }} + CheatEngineSdkSourceMode: ${{ inputs.production-packages && 'false' || 'true' }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # the package version comes from tags and history + submodules: recursive # normal builds compile the pinned SDK source + persist-credentials: false + + - name: Setup .NET + uses: ./.github/actions/setup-dotnet + with: + restore: CheatEngine.Client.slnx + + # No --warnaserror: it would also promote the NuGet audit warnings replayed at build (NU1901/NU1902) and override + # the audit policy. TreatWarningsAsErrors (Directory.Build.props) keeps compiler and analyzer strictness. + - name: Build + run: | + dotnet build CheatEngine.Client.slnx -c $env:CONFIGURATION --no-restore "-bl:artifacts/logs/build-test-$env:CONFIGURATION.binlog" + if ($LASTEXITCODE -ne 0) { + throw "$env:CONFIGURATION solution build failed with exit code $LASTEXITCODE." + } + + # Every .nupkg must embed its SPDX SBOM (_manifest/spdx_2.2/manifest.spdx.json): the pack generates it + # unconditionally (eng/Shipping.props, eng/Templates.props) and release.yml attests it. The seven ids produce + # exactly one nupkg each and a snupkg for every one but the two with no build output (Client, Templates); when a + # release names the exact version, every file must be named "..nupkg" / ".snupkg". + - name: Pack + id: pack + if: inputs.production-packages && matrix.configuration == 'Release' + env: + PACKAGE_VERSION: ${{ inputs.package-version }} + run: | + $ErrorActionPreference = 'Stop' + dotnet pack CheatEngine.Client.slnx -c Release --no-build -o artifacts/nuget -bl:artifacts/logs/pack-Release.binlog + if ($LASTEXITCODE -ne 0) { + throw "Client package creation failed with exit code $LASTEXITCODE." + } + + # The seven ids, each declared once with whether it ships a symbol package (release.yml lists the same ids in + # PACKAGE_IDS). + $shipsSymbols = [ordered]@{ + 'CheatEngine.Client' = $false + 'CheatEngine.Client.Abstractions' = $true + 'CheatEngine.Client.Core' = $true + 'CheatEngine.Client.Extensions.DependencyInjection' = $true + 'CheatEngine.Client.Fluent' = $true + 'CheatEngine.Client.Hosting' = $true + 'CheatEngine.Client.Templates' = $false + } + $ids = @($shipsSymbols.Keys) + $noSymbols = @($ids | Where-Object { -not $shipsSymbols[$_] }) + $nupkgs = @(Get-ChildItem artifacts/nuget -Filter '*.nupkg') + $snupkgs = @(Get-ChildItem artifacts/nuget -Filter '*.snupkg') + if ($nupkgs.Count -ne $ids.Count -or $snupkgs.Count -ne ($ids.Count - $noSymbols.Count)) { + throw "Expected $($ids.Count) nupkg and $($ids.Count - $noSymbols.Count) snupkg in artifacts/nuget, found $($nupkgs.Count) nupkg and $($snupkgs.Count) snupkg." + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem + foreach ($id in $ids) { + $expectedName = if ($env:PACKAGE_VERSION) { "$id.$env:PACKAGE_VERSION.nupkg" } else { $null } + $nupkg = if ($expectedName) { Join-Path artifacts/nuget $expectedName } else { ($nupkgs | Where-Object BaseName -Match "^$([regex]::Escape($id))\.\d") | Select-Object -First 1 -ExpandProperty FullName } + if (-not $nupkg -or -not (Test-Path -LiteralPath $nupkg -PathType Leaf)) { + throw "artifacts/nuget has no package for $id$(if ($expectedName) { " named $expectedName" })." + } + + $archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $nupkg).Path) + try { + if ($null -eq $archive.GetEntry('_manifest/spdx_2.2/manifest.spdx.json')) { + throw "$nupkg has no embedded SBOM at _manifest/spdx_2.2/manifest.spdx.json." + } + } + finally { + $archive.Dispose() + } + } + + Write-Output "Verified $($nupkgs.Count) nupkg and $($snupkgs.Count) snupkg, each embedding its SBOM." + + # The Release Test step consumes these exact packages (CHEATENGINE_CLIENT_PACKAGE_SOURCE), never a package it + # packs itself: a self-pack would test different bits than the ones this run uploads. + $absolute = (Resolve-Path artifacts/nuget).Path + "package-source=$absolute" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + # The solution build compiles the benchmarks; listing them proves discovery. Benchmarks never run in the Gate. + - name: Benchmarks discovery + if: matrix.configuration == 'Release' + run: | + $ErrorActionPreference = 'Stop' + $listed = @(dotnet run --project tests/CheatEngine.Client.Benchmarks/CheatEngine.Client.Benchmarks.csproj -c Release --no-build -- --list flat --artifacts artifacts/benchmarks) + if ($LASTEXITCODE -ne 0) { + throw "Benchmark discovery failed with exit code $LASTEXITCODE." + } + $benchmarks = @($listed | Where-Object { $_ -match '^CheatEngine\.Client\.Benchmarks\.' }) + if ($benchmarks.Count -eq 0) { + throw "The benchmark switcher listed no benchmark:`n$($listed -join "`n")" + } + "Discovered $($benchmarks.Count) benchmarks." | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + + # One parallel run over every tests/**/*.Tests module. A skip fails the run (--fail-skips on). Debug collects one + # coverage report per module and excludes package consumption except in the production Release job, never Skip. + # That one job hands the package tests the exact packages the Pack step produced. Both legs exclude live tests + # by trait: they start a sandboxed Cheat Engine and run only on a maintainer workstation that opts in. The hang + # dump fires after 15 minutes without test activity, well below the job timeout. + - name: Test + env: + PACKAGE_SOURCE: ${{ steps.pack.outputs.package-source }} + run: | + $ErrorActionPreference = 'Stop' + $options = @( + '--solution', 'CheatEngine.Client.slnx', '-c', $env:CONFIGURATION, '--no-build', '--results-directory', $env:RESULTS, + '--fail-skips', 'on', '--report-trx', '--report-gh', '--report-gh-groups', 'off', + '--hangdump', '--hangdump-timeout', '15m', '--crashdump', + '--filter-not-trait', 'Category=LiveQualification' + ) + if ($env:PRODUCTION_PACKAGES -ne 'true' -or $env:CONFIGURATION -eq 'Debug') { + $options += '--filter-not-trait', 'Category=PackageConsumption' + } + if ($env:CONFIGURATION -eq 'Debug') { + $options += '--coverage', '--coverage-output-format', 'xml' + } + elseif ($env:PRODUCTION_PACKAGES -eq 'true') { + # Set only here: the smoke tests fail on a set-but-empty value, and the Debug leg has no packages. + if ([string]::IsNullOrEmpty($env:PACKAGE_SOURCE)) { + throw 'The Pack step exported no package-source.' + } + $env:CHEATENGINE_CLIENT_PACKAGE_SOURCE = $env:PACKAGE_SOURCE + } + dotnet test @options + if ($LASTEXITCODE -ne 0) { + throw "$env:CONFIGURATION tests failed with exit code $LASTEXITCODE." + } + + - name: Upload packages + if: inputs.production-packages && matrix.configuration == 'Release' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: nuget-packages + path: | + artifacts/nuget/*.nupkg + artifacts/nuget/*.snupkg + if-no-files-found: error + retention-days: ${{ inputs.package-retention-days }} + + - name: Upload coverage + if: matrix.configuration == 'Debug' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage + path: ${{ env.RESULTS }}/*.xml + if-no-files-found: error + retention-days: 7 + + - name: Upload test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: test-results-${{ matrix.configuration }} + path: ${{ env.RESULTS }}/*.trx + if-no-files-found: warn + retention-days: 7 + + # Crash dumps, hang dumps and the test sequence logs that name the tests running at the time. + - name: Upload test dumps + if: failure() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: test-dumps-${{ matrix.configuration }} + path: | + ${{ env.RESULTS }}/**/*.dmp + ${{ env.RESULTS }}/**/*.log + if-no-files-found: ignore + retention-days: 5 + + - name: Upload binary logs + if: failure() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: binlogs-build-test-${{ matrix.configuration }} + path: artifacts/logs/*.binlog + if-no-files-found: ignore + retention-days: 5 + + # A trim and Native AOT graph probe only: it never loads anything into Cheat Engine. + aot: + name: Native AOT publication probe + runs-on: windows-2025 + timeout-minutes: 25 + env: + CheatEngineSdkSourceMode: ${{ inputs.production-packages && 'false' || 'true' }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # the package version comes from tags and history + submodules: recursive # the AOT graph uses the pinned SDK source + persist-credentials: false + + - name: Setup .NET + uses: ./.github/actions/setup-dotnet + with: + restore: tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj + + # The project pins its RuntimeIdentifier (win-x64), so publish needs no --runtime. + - name: Publish and run Native AOT probe + run: | + $ErrorActionPreference = 'Stop' + $output = Join-Path $PWD 'artifacts/aot-probe' + dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj -c Release --no-restore -o $output -bl:artifacts/logs/aot-publish.binlog + if ($LASTEXITCODE -ne 0) { + throw "Native AOT probe publish failed with exit code $LASTEXITCODE." + } + $probe = Join-Path $output 'CheatEngine.Client.AotProbe.exe' + if (-not (Test-Path -LiteralPath $probe -PathType Leaf)) { + throw "Native AOT publish did not produce '$probe'." + } + & $probe + if ($LASTEXITCODE -ne 0) { + throw "Native AOT probe exited with code $LASTEXITCODE." + } + + - name: Upload binary logs + if: failure() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: binlogs-aot + path: artifacts/logs/*.binlog + if-no-files-found: ignore + retention-days: 5 + + # Pull requests fail on the quality gate; main only reports it. SONAR_EXPECTED below must stay identical to the gate's. + sonar: + name: Sonar + needs: build-test + if: >- + inputs.sonar + && github.event_name != 'merge_group' + && github.actor != 'dependabot[bot]' + && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + uses: ./.github/workflows/sonar.yml + with: + wait-quality-gate: ${{ github.event_name != 'push' }} + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + lint: + name: Lint + runs-on: ubuntu-24.04 + timeout-minutes: 10 + env: + ACTIONLINT_VERSION: 1.7.12 + ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 # linux_amd64, official checksums file + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run actionlint + run: | + $ErrorActionPreference = 'Stop' + $archive = Join-Path $env:RUNNER_TEMP 'actionlint.tar.gz' + $url = "https://github.com/rhysd/actionlint/releases/download/v$env:ACTIONLINT_VERSION/actionlint_$($env:ACTIONLINT_VERSION)_linux_amd64.tar.gz" + Invoke-WebRequest -Uri $url -OutFile $archive -MaximumRetryCount 3 -RetryIntervalSec 5 + $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $env:ACTIONLINT_SHA256) { + throw "actionlint $env:ACTIONLINT_VERSION has SHA-256 $actual, expected $env:ACTIONLINT_SHA256." + } + tar -xzf $archive -C $env:RUNNER_TEMP actionlint + if ($LASTEXITCODE -ne 0) { + throw "Extracting actionlint failed with exit code $LASTEXITCODE." + } + & (Join-Path $env:RUNNER_TEMP 'actionlint') -color + if ($LASTEXITCODE -ne 0) { + throw "actionlint failed with exit code $LASTEXITCODE." + } + + # Offline audits only, so the result depends on the commit alone; the online audits run in zizmor-online.yml. + - name: Run zizmor + uses: zizmorcore/zizmor-action@45d7276c9ca790aabd705e31ca99c6185c3e76dd # v0.8.1 + with: + version: 1.30.1 + online-audits: false + advanced-security: false + config: .github/zizmor.yml + + format: + name: Format + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup .NET + uses: ./.github/actions/setup-dotnet + + # Whitespace needs no restore or MSBuild workspace, and the CRLF checkout (.gitattributes) matches .editorconfig. + # Style rules are enforced by the build (EnforceCodeStyleInBuild). + - name: Verify whitespace formatting + run: | + dotnet format whitespace . --folder --verify-no-changes --exclude artifacts externals + if ($LASTEXITCODE -ne 0) { + throw "Whitespace formatting differs (exit code $LASTEXITCODE). Fix it with: dotnet format whitespace --folder" + } + + # Always runs, so the Gate never has to accept a skip: the review itself runs on pull requests only. + dependency-review: + name: Dependency review + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Review dependency changes + if: github.event_name == 'pull_request' + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + config-file: ./.github/dependency-review-config.yml + comment-summary-in-pr: never + + - name: Nothing to review + if: github.event_name != 'pull_request' + env: + EVENT: ${{ github.event_name }} + run: Write-Host "::notice title=Dependency review::Dependency review compares a pull request with its base; a $env:EVENT run has nothing to compare." + + lock-files: + name: Lock files + runs-on: windows-2025 + timeout-minutes: 15 + env: + CheatEngineSdkSourceMode: ${{ inputs.production-packages && 'false' || 'true' }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive # locked restore follows the normal source graph + persist-credentials: false + + - name: Setup .NET + uses: ./.github/actions/setup-dotnet + + # Windows only: Native AOT lock sections record host-RID ILCompiler packages. A locked restore fails outright + # when a committed packages.lock.json no longer matches its project graph. + - name: Verify lock files + run: | + dotnet restore CheatEngine.Client.slnx --locked-mode + if ($LASTEXITCODE -ne 0) { + throw "Locked restore failed with exit code $LASTEXITCODE. The committed packages.lock.json files do not match the project graph: regenerate the affected ones with 'dotnet restore --force-evaluate' and commit the result." + } + + gate: + name: Gate + # always(): a failed or cancelled job must turn the required check red instead of skipping it. + if: always() + needs: [ build-test, aot, sonar, lint, format, dependency-review, lock-files ] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: {} + steps: + - name: Check results + env: + NEEDS: ${{ toJSON(needs) }} + SONAR_EXPECTED: >- + ${{ inputs.sonar + && github.event_name != 'merge_group' + && github.actor != 'dependabot[bot]' + && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + EVENT: ${{ github.event_name }} + run: | + # Every job must succeed. Only sonar has a required result that depends on the event: success when + # SONAR_EXPECTED is true, skipped otherwise (fork, Dependabot, release call). A sonar run that was not + # expected means its condition drifted from SONAR_EXPECTED, which fails too. + $ErrorActionPreference = 'Stop' + $needs = $env:NEEDS | ConvertFrom-Json -AsHashtable + $sonarExpected = $env:SONAR_EXPECTED -eq 'true' + $failed = [Collections.Generic.List[string]]::new() + $rows = foreach ($job in @($needs.Keys | Sort-Object)) { + $result = $needs[$job].result + $required = 'success' + $reason = 'every job must succeed' + if ($job -eq 'sonar') { + if ($sonarExpected) { + $reason = 'SONAR_EXPECTED=true' + if ($result -eq 'skipped' -and $needs['build-test'].result -ne 'success') { + $reason = 'SONAR_EXPECTED=true; skipped because build-test did not succeed' + } + } + else { + $required = 'skipped' + $reason = "SONAR_EXPECTED=false ($env:EVENT run: fork, Dependabot or release call)" + } + } + if ($result -ne $required) { + $failed.Add("$job ($result, required $required)") + } + "| $job | $result | $required | $reason |" + } + @('### Gate', '', '| Job | Result | Required | Reason |', '| --- | --- | --- | --- |') + $rows | + Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + if ($failed.Count -gt 0) { + Write-Host "::error::Not as required: $($failed -join ', ')." + exit 1 + } diff --git a/.github/workflows/zizmor-online.yml b/.github/workflows/zizmor-online.yml index 3bed311e..b6ec72c2 100644 --- a/.github/workflows/zizmor-online.yml +++ b/.github/workflows/zizmor-online.yml @@ -1,47 +1,47 @@ -# Advisory online workflow audits (known-vulnerable-actions, impostor-commit, ref-version-mismatch, stale-action-refs) -# uploaded to code scanning. Not part of CI / Gate: with advanced-security the action reports through SARIF and does -# not fail on findings. The blocking, offline zizmor run lives in ci.yml (job lint) and pins the same version. -name: Workflow security (online) - -on: - pull_request: - paths: - - '.github/**' - push: - branches: [ main ] - schedule: - - cron: '11 6 * * 1' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -permissions: - contents: read - -jobs: - zizmor: - name: zizmor (online audits) - # Fork pull requests get a read-only token and cannot upload SARIF. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - security-events: write # upload the SARIF results - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - # Inputs: https://github.com/zizmorcore/zizmor-action (v0.6.4 action.yml). - - name: Run zizmor - uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 - with: - version: 1.30.1 - online-audits: true - advanced-security: true - persona: regular - config: .github/zizmor.yml +# Advisory online workflow audits (known-vulnerable-actions, impostor-commit, ref-version-mismatch, stale-action-refs) +# uploaded to code scanning. Not part of CI / Gate: with advanced-security the action reports through SARIF and does +# not fail on findings. The blocking, offline zizmor run lives in ci.yml (job lint) and pins the same version. +name: Workflow security (online) + +on: + pull_request: + paths: + - '.github/**' + push: + branches: [ main ] + schedule: + - cron: '11 6 * * 1' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + zizmor: + name: zizmor (online audits) + # Fork pull requests get a read-only token and cannot upload SARIF. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + security-events: write # upload the SARIF results + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Inputs: https://github.com/zizmorcore/zizmor-action (v0.6.4 action.yml). + - name: Run zizmor + uses: zizmorcore/zizmor-action@45d7276c9ca790aabd705e31ca99c6185c3e76dd # v0.8.1 + with: + version: 1.30.1 + online-audits: true + advanced-security: true + persona: regular + config: .github/zizmor.yml