Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ name: CI
# 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):
# nuget-packages build-test (Release) → sonar, release.yml
# 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-<configuration> build-test → humans
# test-dumps-<configuration>, binlogs-* on failure only → humans
Expand All @@ -34,6 +35,12 @@ on:
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.
Expand All @@ -58,11 +65,14 @@ jobs:
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
Expand All @@ -85,7 +95,7 @@ jobs:
# release names the exact version, every file must be named "<Id>.<version>.nupkg" / ".snupkg".
- name: Pack
id: pack
if: matrix.configuration == 'Release'
if: inputs.production-packages && matrix.configuration == 'Release'
env:
PACKAGE_VERSION: ${{ inputs.package-version }}
run: |
Expand Down Expand Up @@ -156,8 +166,8 @@ jobs:
"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 leaves the package-consumption tests to Release by trait filter, never Skip.
# Release hands those tests the packages the Pack step produced. Both legs exclude the live qualification tests
# 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
Expand All @@ -171,10 +181,13 @@ jobs:
'--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', '--filter-not-trait', 'Category=PackageConsumption'
$options += '--coverage', '--coverage-output-format', 'xml'
}
else {
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.'
Expand All @@ -187,7 +200,7 @@ jobs:
}

- name: Upload packages
if: matrix.configuration == 'Release'
if: inputs.production-packages && matrix.configuration == 'Release'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nuget-packages
Expand Down Expand Up @@ -241,11 +254,14 @@ jobs:
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
Expand Down Expand Up @@ -353,7 +369,7 @@ jobs:
# Style rules are enforced by the build (EnforceCodeStyleInBuild).
- name: Verify whitespace formatting
run: |
dotnet format whitespace . --folder --verify-no-changes --exclude artifacts
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 <path> --folder"
}
Expand Down Expand Up @@ -386,10 +402,13 @@ jobs:
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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # MinVer computes the package version from the tag history
submodules: recursive
persist-credentials: false

- name: Set up .NET and restore the product graph (locked)
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/dependency-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: recursive
persist-credentials: false

- name: Set up .NET and restore (locked)
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ jobs:
with:
package-version: ${{ needs.verify.outputs.version }}
package-retention-days: 90
production-packages: true

# Every run, dry runs included, with a read-only token and no OIDC token: extracts the SPDX SBOM each package embeds
# and writes SHA256SUMS over the packages, the symbol packages and the SBOMs. A dry run ends here, with those files in
Expand Down
33 changes: 16 additions & 17 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Sonar

# Reusable SonarQube Cloud analysis, called by ci.yml after build-test when SONAR_EXPECTED is true. It reuses the
# coverage and nuget-packages artifacts of the same run; only the instrumented build is repeated, because the scanner
# must observe a compilation.
# coverage artifacts of the same source-mode run; only the instrumented build is repeated, because the scanner must
# observe a compilation.
#
# CI-based analysis only: Automatic Analysis must stay OFF for the project in SonarQube Cloud (Administration > Analysis
# Method). SonarQube Cloud exposes no API to read that setting; with both methods on, scanner begin fails, which is the
Expand Down Expand Up @@ -47,6 +47,7 @@ jobs:
SONAR_PROJECT_KEY: ${{ inputs.project-key }}
SONAR_ORGANIZATION: ${{ inputs.organization }}
SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }}
CheatEngineImportSourceSettings: 'true'
steps:
# ci.yml never calls this workflow for forks or Dependabot, so an empty token is a configuration error. Fail
# before the instrumented build instead of after it.
Expand All @@ -63,6 +64,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # SCM blame for new-code detection
submodules: recursive # analyse the same pinned SDK source graph as CI
persist-credentials: false

# SonarScanner for .NET uses Java. The same JDK 21 distribution as CheatEngine.SDK keeps scanner behaviour
Expand Down Expand Up @@ -92,12 +94,6 @@ jobs:
name: coverage
path: ${{ runner.temp }}/coverage

- name: Download packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nuget-packages
path: ${{ runner.temp }}/packages

# Do not use the checked-out nuget.config to resolve tooling. The scanner package and every restored dependency
# must come only from nuget.org, not a source a pull request can redirect.
- name: Create NuGet.org-only configuration
Expand Down Expand Up @@ -149,15 +145,18 @@ jobs:
Write-Host '::error::The coverage artifact holds no report.'
exit 1
}
# The new-code period follows sonar.projectVersion: the core version of the facade package built in this run
# (0.1.0 for 0.1.0-alpha.0.37). The digit right after "CheatEngine.Client." excludes the other package ids.
$packages = @(Get-ChildItem -Path "$env:RUNNER_TEMP/packages" -Filter 'CheatEngine.Client.*.nupkg' -File |
Where-Object { $_.Name -match '^CheatEngine\.Client\.\d' })
if ($packages.Count -ne 1 -or $packages[0].Name -notmatch '^CheatEngine\.Client\.(?<version>\d+\.\d+\.\d+)') {
Write-Host '::error::The nuget-packages artifact must hold exactly one CheatEngine.Client facade package.'
# Run MinVer before reading its version, then retain the core version used by the previous package-based
# calculation. A different prerelease commit must not reset Sonar's version-based new-code period.
$versionOutput = @(dotnet msbuild src/CheatEngine.Client/CheatEngine.Client.csproj -target:MinVer -getProperty:MinVerVersion --nologo)
if ($LASTEXITCODE -ne 0) {
throw "Reading the source version failed with exit code $LASTEXITCODE."
}
$version = ($versionOutput -join "`n").Trim()
if ($version -notmatch '^(?<core>\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?$') {
Write-Host "::error::Could not read the MinVer source version from:`n$($versionOutput -join "`n")"
exit 1
}
$version = $Matches['version']
$version = $Matches['core']
Write-Host "Coverage reports: $($reports.Count); project version: $version"
# These findings conflict with deliberate repository contracts. Keep them in the scanner configuration so the
# sources stay free of Sonar-only attributes and suppressions.
Expand All @@ -176,7 +175,7 @@ jobs:
"/k:$env:SONAR_PROJECT_KEY"
"/o:$env:SONAR_ORGANIZATION"
"/v:$version"
'/d:sonar.exclusions=artifacts/**,tests/CheatEngine.Client.Benchmarks/**'
'/d:sonar.exclusions=artifacts/**,externals/**,tests/CheatEngine.Client.Benchmarks/**'
# CI publishes managed coverage of the shipping code only: tests and build tooling stay out of the coverage
# metric instead of presenting an incomplete report as if it covered them. Every excluded path exists
# (the repository has no documentation folder; its Markdown files are not analysed code).
Expand All @@ -185,7 +184,7 @@ jobs:
# (xUnit has no attached Cheat Engine process or Lua state): its lines are untestable, not merely
# untested, and its non-SDK branches keep the tests they allow next to it (for example
# SdkTableRecordMutationPortTests). A lot that adds an Sdk*Port.cs adds its own entry here.
'/d:sonar.coverage.exclusions=tests/**,eng/**,libs/CheatEngine.Client.Core/Domains/SdkTableRecordMutationPort.cs,libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs,libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs,libs/CheatEngine.Client.Core/Domains/SdkRuntimeObservationPort.cs,libs/CheatEngine.Client.Core/Domains/SdkProcessSelectionPort.cs,libs/CheatEngine.Client.Core/Domains/SdkInspectionPort.cs,libs/CheatEngine.Client.Core/Domains/SdkTableFilePort.cs,libs/CheatEngine.Client.Core/Domains/SdkTableRecordLookupPort.cs,libs/CheatEngine.Client.Core/Domains/ValueScanning/SdkValueScanPort.cs,libs/CheatEngine.Client.Core/Domains/Allocations/SdkAllocationPort.cs,libs/CheatEngine.Client.Core/Domains/Assembly/SdkAutoAssemblerPort.cs,libs/CheatEngine.Client.Core/Domains/Assembly/SdkInstructionPort.cs'
'/d:sonar.coverage.exclusions=externals/**,tests/**,eng/**,libs/CheatEngine.Client.Core/Domains/SdkTableRecordMutationPort.cs,libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs,libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs,libs/CheatEngine.Client.Core/Domains/SdkRuntimeObservationPort.cs,libs/CheatEngine.Client.Core/Domains/SdkProcessSelectionPort.cs,libs/CheatEngine.Client.Core/Domains/SdkInspectionPort.cs,libs/CheatEngine.Client.Core/Domains/SdkTableFilePort.cs,libs/CheatEngine.Client.Core/Domains/SdkTableRecordLookupPort.cs,libs/CheatEngine.Client.Core/Domains/ValueScanning/SdkValueScanPort.cs,libs/CheatEngine.Client.Core/Domains/Allocations/SdkAllocationPort.cs,libs/CheatEngine.Client.Core/Domains/Assembly/SdkAutoAssemblerPort.cs,libs/CheatEngine.Client.Core/Domains/Assembly/SdkInstructionPort.cs'
# Never set sonar.dotnet.excludeTestProjects: the scanner then strips every analyzer, including source
# generators, from test projects, and their [GeneratedRegex]/[LoggerMessage] partial methods fail with
# CS8795 (https://github.com/SonarSource/sonar-scanner-msbuild/issues/1469). Test projects are analysed as
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "externals/CheatEngine.SDK"]
path = externals/CheatEngine.SDK
url = https://github.com/CheatEngineNet/CheatEngine.SDK.git
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ ship.

### Added

- **Source SDK builds.** Workspace consumers can opt into `CheatEngineSdkSourceRoot` to build Client and SDK together
through project references. Source builds record the SDK Engine version and Git commit from its PE metadata,
preserve plugin profile checks, and reject mismatched runtime identities. Normal NuGet builds retain their existing
package identity checks.
- **Pinned SDK submodule.** Repository builds, tests, plugin fixtures and PR CI build `externals/CheatEngine.SDK`
through project references, with separate source lock files. Parent workspaces can override `CheatEngineSdkSourceRoot`.
Source builds record the SDK Engine version and Git commit from its PE metadata, preserve plugin profile checks,
and reject mismatched runtime identities. NuGet packaging and package-consumer checks run on the production release
path with `CheatEngineSdkSourceMode=false`; SDK remains a separate published dependency.

### Changed

Expand Down
Loading
Loading