From 95cd6a7ce5ffdb537851d461fd14b96a359a1201 Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 17:04:18 +0200 Subject: [PATCH 1/9] Harden pull request dependency review gate Add a pinned dependency-review job to the reusable CI workflow for non-draft pull requests. Include its result in the gate summary and require it only when the job is expected to run, so push and draft workflows remain valid while high-severity dependency changes fail the PR gate. --- .github/workflows/ci.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb9e046b..906353d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -477,6 +477,18 @@ jobs: secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + dependency-review: + name: Dependency review + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Review dependency changes + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + with: + fail-on-severity: high lint-workflows: name: Lint workflows if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} @@ -512,7 +524,7 @@ jobs: gate: name: Gate if: ${{ always() }} - needs: [ discover, native, build, test, pack, aot, sonar, lint-workflows ] + needs: [ discover, native, build, test, pack, aot, sonar, dependency-review, lint-workflows ] runs-on: windows-latest timeout-minutes: 5 permissions: { } @@ -527,6 +539,8 @@ jobs: AOT_RESULT: ${{ needs.aot.result }} SONAR_RESULT: ${{ needs.sonar.result }} SONAR_REQUIRED: ${{ inputs.collect-coverage }} + DEPENDENCY_RESULT: ${{ needs.dependency-review.result }} + DEPENDENCY_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} LINT_RESULT: ${{ needs.lint-workflows.result }} run: | $results = [ordered]@{ @@ -537,6 +551,7 @@ jobs: pack = $env:PACK_RESULT aot = $env:AOT_RESULT sonar = $env:SONAR_RESULT + 'dependency-review' = $env:DEPENDENCY_RESULT 'lint-workflows' = $env:LINT_RESULT } $rows = $results.GetEnumerator() | ForEach-Object { "| $($_.Key) | $($_.Value) |" } @@ -544,6 +559,7 @@ jobs: $failed = @($results.GetEnumerator() | Where-Object { $_.Key -in 'discover', 'native', 'build', 'test', 'pack', 'aot' -and $_.Value -ne 'success' -or $_.Key -eq 'sonar' -and $env:SONAR_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or + $_.Key -eq 'dependency-review' -and $env:DEPENDENCY_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or $_.Key -eq 'lint-workflows' -and $_.Value -ne 'success' } | ForEach-Object Key) if ($failed.Count -gt 0) { From 87ce73546c76000cb380b1b8463fe873543d379f Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 17:33:40 +0200 Subject: [PATCH 2/9] Remove `AGENTS.md`, update attribute inheritance restrictions, and replace `Volatile` with `System.Threading.Volatile` static usage for Lua runtime lifecycle operations. --- AGENTS.md | 59 ------------------- .../Lua/LuaClassAttribute.cs | 2 +- .../Lua/LuaMarshallerAttribute.cs | 2 +- .../Lua/LuaPropertyAttribute.cs | 2 +- .../AddressList/AddressListMutations.cs | 2 +- .../Inspection/SymbolRegistry.cs | 3 +- .../Scanning/Values/MemoryScanSession.cs | 3 +- .../CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs | 42 ++++++------- .../Support/Layout.cs | 1 - .../LiveProbeAuthorization.cs | 3 +- 10 files changed, 32 insertions(+), 87 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index b283127b..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,59 +0,0 @@ -# Repository Guidelines - -## Project Structure & Module Organization - -CheatEngine.SDK is a Windows x64, .NET 10 SDK for Cheat Engine 7.7 plugins. - -- `libs/`: layered assemblies for annotations, ABI definitions, Lua interop, Lua operations, engine APIs, and hosting. -- `src/CheatEngine.SDK/`: umbrella NuGet package and consumer build properties. -- `source-generators/` and `analyzers/`: generated bindings, entry points, diagnostics, and code fixes. -- `native/`: bundled Cheat Engine Lua test DLL and the source plus prebuilt Windows x64 Lua protection bridge. -- `tests/`: matching test projects, shared native fixtures, benchmarks, and `CheatEngine.SDK.LivePlugin`. -- `exemples/`: guides, recipes, and API documentation; preserve this directory spelling. -- `eng/` and `.github/`: shared build configuration and CI. Treat `artifacts/` as generated output. - -## Build, Test, and Development Commands - -Use the SDK selected by `global.json` (10.0.401, `latestFeature`). Ordinary managed builds use the checked-in bridge -binary and need no C toolchain. Only bridge maintainers and CI rebuild it, using xmake and a Windows x64 C compiler. - -```powershell -dotnet restore CheatEngine.SDK.slnx -dotnet build CheatEngine.SDK.slnx -c Debug --no-restore -dotnet test --solution CheatEngine.SDK.slnx -c Debug --fail-skips on -dotnet test --solution CheatEngine.SDK.slnx -c Release -dotnet pack src/CheatEngine.SDK -c Release -o artifacts/nuget -``` - -These restore dependencies, compile the solution, validate both configurations, and produce the package. For host -testing, follow `tests/CheatEngine.SDK.LivePlugin/README.md`; keep the complete plugin output together, including -`cheatengine-sdk-lua-bridge.dll`. Configure Cheat Engine to use .NET 10 explicitly. - -## Coding Style & Naming Conventions - -Follow `.editorconfig`: UTF-8; C# uses tabs for logical nesting and spaces only for continuation alignment; -configuration/project files use two spaces. `.gitattributes` owns line-ending normalization for the working tree. Use -file-scoped namespaces, explicit accessibility, PascalCase public members, and existing local naming patterns. Preserve -native Lua identifiers and ABI layouts. - -Builds enforce compiler and analyzer diagnostics as errors, with configured exceptions. Document public APIs and provide -a README beside every project. - -**LINQ is forbidden**, including query expressions and `System.Linq` operators. Use explicit loops and collection APIs -to control allocations and iteration costs. Avoid unrelated refactors. - -## Testing Guidelines - -Tests use xUnit v3 with Microsoft.Testing.Platform. Name tests `Subject_condition_expected`; add focused regressions for -behavioral fixes. Debug validation rejects skips; Release permits the existing Debug-only guard skip. Coverage can be -collected with `--coverage --coverage-output-format xml`. - -Native tests use the bundled Lua DLL. Preserve stack balance, callback lifetimes, ownership, and native error -boundaries. Distinguish fixture results from live Cheat Engine verification. - -## Commit & Pull Request Guidelines - -History uses imperative subjects such as `Fix CI validation findings`; no conventional-commit prefix is required. Do not -add `Co-authored-by` trailers to commits. Keep commits focused. PR descriptions should explain the problem, resulting -behavior, relevant issues, validation commands/results, and remaining live-host limitations. Update affected -documentation and report build, test, and package results before requesting review. diff --git a/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs b/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs index f79aa8c6..20db2c42 100644 --- a/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs +++ b/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs @@ -27,7 +27,7 @@ namespace CheatEngine.SDK.Annotations.Lua; /// a wrapper type stands for exactly one Cheat Engine class. /// /// -[AttributeUsage(AttributeTargets.Struct, Inherited = false)] +[AttributeUsage(AttributeTargets.Struct)] public sealed class LuaClassAttribute : Attribute { /// diff --git a/libs/CheatEngine.SDK.Annotations/Lua/LuaMarshallerAttribute.cs b/libs/CheatEngine.SDK.Annotations/Lua/LuaMarshallerAttribute.cs index 8df6b4c5..f7575dd6 100644 --- a/libs/CheatEngine.SDK.Annotations/Lua/LuaMarshallerAttribute.cs +++ b/libs/CheatEngine.SDK.Annotations/Lua/LuaMarshallerAttribute.cs @@ -20,7 +20,7 @@ namespace CheatEngine.SDK.Annotations.Lua; /// generated wrapper restores the Lua stack before returning. /// /// -[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)] public sealed class LuaMarshallerAttribute : Attribute { /// Initializes the attribute with the concrete static marshaller type. diff --git a/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs b/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs index 17145ed5..aa19ed7a 100644 --- a/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs +++ b/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs @@ -26,7 +26,7 @@ namespace CheatEngine.SDK.Annotations.Lua; /// one Cheat Engine property. /// /// -[AttributeUsage(AttributeTargets.Property, Inherited = false)] +[AttributeUsage(AttributeTargets.Property)] public sealed class LuaPropertyAttribute : Attribute { /// diff --git a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs index 78da8071..2a3f924c 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs @@ -1,6 +1,6 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using CheatEngine.SDK.Annotations.Lifetime; using CheatEngine.SDK.Engine.Objects; diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs index b77904e1..f138f20b 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs @@ -172,7 +172,8 @@ private static partial LuaOperationStatus RegisterCore([LuaMarshaller(typeof(Sym private static partial LuaOperationStatus UnregisterCore([LuaMarshaller(typeof(SymbolName))] SymbolName name); [SuppressMessage("Meziantou.Analyzer", "MA0051:Method is too long", - Justification = "ReleaseOwned is the single atomic lease-cleanup transaction and must preserve its state ordering.")] + Justification = + "ReleaseOwned is the single atomic lease-cleanup transaction and must preserve its state ordering.")] internal static SymbolRegistrationReleaseOutcome ReleaseOwned(SymbolRegistrationLease lease) { lock (SOwnedRegistrationGate) diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs index 2a21df26..7ae6d686 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs @@ -603,7 +603,8 @@ public MemoryScanMaterializationStatus TryCopyResults(Span des [MainThreadOnly] [RequiresPluginEnabled] [SuppressMessage("Meziantou.Analyzer", "MA0051:Method is too long", - Justification = "This bounded materialization operation keeps its cancellation and ownership milestones together.")] + Justification = + "This bounded materialization operation keeps its cancellation and ownership milestones together.")] public MemoryScanMaterializationStatus TryCopyResultsCancellable(Span destination, out ulong totalCount, out int written, CancellationToken cancellationToken) { diff --git a/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs b/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs index 0f5119bf..3de7baa4 100644 --- a/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs +++ b/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs @@ -15,6 +15,8 @@ using CheatEngine.SDK.Lua.References; using CheatEngine.SDK.Lua.State; +using static System.Threading.Volatile; + namespace CheatEngine.SDK.Lua.Runtime; /// @@ -89,7 +91,7 @@ public static unsafe class LuaRuntime internal static Action? OperationAdmissionClosedForTesting; /// Gets a value indicating whether a host binding is attached. Lock-free; any thread. - public static bool IsAttached => Volatile.Read(ref s_services) is not null; + public static bool IsAttached => Read(ref s_services) is not null; /// /// Gets whether the calling thread currently owns an admitted Lua operation. Internal lifecycle code uses this @@ -127,7 +129,7 @@ public static int StateGeneration public static LuaStateIdentity CurrentStateIdentity { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => UnpackIdentity(Volatile.Read(ref s_identity)); + get => UnpackIdentity(Read(ref s_identity)); } /// @@ -138,13 +140,13 @@ public static bool IsMainThread { get { - LuaHostServices? services = Volatile.Read(ref s_services); + LuaHostServices? services = Read(ref s_services); return services is not null && services.MainThreadId == Environment.CurrentManagedThreadId; } } /// Gets the attached binding, or while detached. - public static LuaHostBinding CurrentBinding => Volatile.Read(ref s_services)?.Binding ?? default; + public static LuaHostBinding CurrentBinding => Read(ref s_services)?.Binding ?? default; /// /// Acquires a Lua state together with a lifecycle admission that spans the whole synchronous operation. @@ -167,7 +169,7 @@ public static LuaRuntimeOperation AcquireOperation() return new LuaRuntimeOperation(state, true); } - if (Volatile.Read(ref s_services) is null) + if (Read(ref s_services) is null) { ThrowDetached(); } @@ -242,7 +244,7 @@ public static LuaRuntimeOperation AcquireOperation(LuaState state) throw new ArgumentException("A supplied Lua operation state cannot be null.", nameof(state)); } - if (Volatile.Read(ref s_services) is null) + if (Read(ref s_services) is null) { ThrowDetached(); } @@ -288,7 +290,7 @@ public static LuaStatus TryPushGeneratedFunction(LuaState state, LuaNativeFuncti { using LuaRuntimeOperation operation = EnterStateOperation(state); LuaStateIdentity identity = CurrentStateIdentity; - bool requiresAttachedRuntime = Volatile.Read(ref s_services) is not null; + bool requiresAttachedRuntime = Read(ref s_services) is not null; return state.TryPushGeneratedFunction(thunk, identity, requiresAttachedRuntime); } @@ -331,7 +333,7 @@ public static void Attach(in LuaHostBinding binding) { LuaStateIdentity identity = CurrentStateIdentity; PublishIdentity(unchecked(identity.AttachEpoch + 1), identity.StateGeneration); - Volatile.Write(ref s_services, new LuaHostServices(binding)); + Write(ref s_services, new LuaHostServices(binding)); LuaHostSubscriptionRegistry.OpenRegistrationAdmission(); } } @@ -341,7 +343,7 @@ public static void Attach(in LuaHostBinding binding) // If replacement cleanup failed, s_services still names the previous usable binding. Reopen it rather // than stranding every caller behind the admission gate until a later lifecycle call happens to retry. OpenOperationAdmission(); - if (Volatile.Read(ref s_services) is not null) + if (s_services != null) { LuaHostSubscriptionRegistry.OpenRegistrationAdmission(); } @@ -422,7 +424,7 @@ public static void Detach() { LuaHostSubscriptionRegistry.DetachAll(new LuaState(services.Provider())); LuaCallbackRegistry.DetachAll(services); - Volatile.Write(ref s_services, null); + Write(ref s_services, null); detachSucceeded = true; } finally @@ -503,7 +505,7 @@ public static bool TryAcquireState(out LuaState state) public static void PushHostObject(LuaState state, nint nativeObject) { using LuaRuntimeOperation operation = EnterStateOperation(state); - LuaHostServices? services = Volatile.Read(ref s_services); + LuaHostServices? services = Read(ref s_services); if (services is null) { ThrowDetached(); @@ -530,7 +532,7 @@ public static void PushHostObject(LuaState state, nint nativeObject) /// internal static LuaRuntimeOperation EnterStateOperation(LuaState state) { - if (t_transitionDepth != 0 || t_operationDepth != 0 || Volatile.Read(ref s_services) is null) + if (t_transitionDepth != 0 || t_operationDepth != 0 || Read(ref s_services) is null) { return default; } @@ -558,13 +560,13 @@ internal static bool TryEnterCallbackOperation(out LuaRuntimeOperation operation // A callback that is re-entered by an already admitted Lua operation shares that outer lease. The lifecycle // transition owner is different: admitting plugin code there would let a finalizer/metamethod re-enter after // CloseOperationAdmissionAndDrain has established exclusive cleanup. - if (t_transitionDepth != 0 && Volatile.Read(ref s_services) is not null) + if (t_transitionDepth != 0 && Read(ref s_services) is not null) { operation = default; return false; } - if (t_operationDepth != 0 || Volatile.Read(ref s_services) is null) + if (t_operationDepth != 0 || Read(ref s_services) is null) { operation = default; return true; @@ -588,7 +590,7 @@ internal static bool TryEnterCallbackOperation(out LuaRuntimeOperation operation internal static void CloseHostSubscriptionAdmissionAndDrain() { ThrowIfTransitionFromCurrentOperation(); - if (Volatile.Read(ref s_services) is null) + if (Read(ref s_services) is null) { return; } @@ -601,8 +603,8 @@ internal static void CloseHostSubscriptionAdmissionAndDrain() // before user code; one admitted before the boundary retains its lease until its unmanaged thunk returns. internal static bool IsGeneratedFunctionRegistrationCurrent(int attachEpoch, int stateGeneration) { - return Volatile.Read(ref s_services) is not null - && Volatile.Read(ref s_identity) == PackIdentity(attachEpoch, stateGeneration) + return Read(ref s_services) is not null + && Read(ref s_identity) == PackIdentity(attachEpoch, stateGeneration) && IsOperationAdmissionOpen(); } @@ -623,7 +625,7 @@ internal static void CloseOperationAdmissionAndDrain() s_acceptOperations = false; } - Volatile.Read(ref OperationAdmissionClosedForTesting)?.Invoke(); + Read(ref OperationAdmissionClosedForTesting)?.Invoke(); SOperationsDrained.Wait(); } @@ -640,7 +642,7 @@ internal static void CompleteStateReset() s_resetTransitionActive = false; EndTransition(); OpenOperationAdmission(); - if (Volatile.Read(ref s_services) is not null) + if (Read(ref s_services) is not null) { LuaHostSubscriptionRegistry.OpenRegistrationAdmission(); } @@ -825,7 +827,7 @@ private static void ThrowIfTransitionFromCurrentOperation() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void PublishIdentity(int attachEpoch, int stateGeneration) { - Volatile.Write(ref s_identity, PackIdentity(attachEpoch, stateGeneration)); + Write(ref s_identity, PackIdentity(attachEpoch, stateGeneration)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs index 9bea073b..c475c0db 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs @@ -37,7 +37,6 @@ public static int AlignmentOf() private struct AlignmentProbe where T : unmanaged { - public byte Prefix; public T Value; } } diff --git a/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs b/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs index eba8512b..d10b460b 100644 --- a/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs +++ b/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs @@ -20,7 +20,8 @@ internal static class LiveProbeAuthorization private const string ManifestVariable = "CE_SDK_LIVE_PROBE_AUTHORIZATION_FILE"; [SuppressMessage("Meziantou.Analyzer", "MA0051:Method is too long", - Justification = "Authorization evaluation keeps all fail-closed checks and diagnostics in one auditable sequence.")] + Justification = + "Authorization evaluation keeps all fail-closed checks and diagnostics in one auditable sequence.")] internal static AuthorizationDecision Evaluate() { if (!TryAuthorizeManifest(out AuthorizationManifest manifest, out string manifestFailure)) From 8b4c75e076e62bfa8746742060aca0ae3b946cba Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 18:06:04 +0200 Subject: [PATCH 3/9] Fix SDK PR CI gates and contract regressions Restore the ABI alignment probe prefix so native MSVC fixture facts match managed x64 measurements. Align Lua annotation tests and documentation with the intentional framework-default inheritance contract. Make dependency review opt-in until GitHub Dependency graph is enabled, keep it blocking when enabled, and upgrade the pinned action to v5. --- .github/workflows/ci.yml | 10 +++++++--- .github/workflows/main-ci.yml | 1 + .github/workflows/pull-request-ci.yml | 1 + .../Lua/LuaClassAttribute.cs | 9 ++++----- .../Lua/LuaPropertyAttribute.cs | 10 ++++------ tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs | 1 + .../Generator/LuaObjectOutputTests.cs | 5 +++-- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 906353d9..fc79b335 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,10 @@ on: description: Also run every test project against the Release build, which is what ships. type: boolean default: false + dependency-review: + description: Run dependency-review-action for non-draft pull requests. Enable only after GitHub Dependency graph is enabled for the repository. + type: boolean + default: false secrets: SONAR_TOKEN: description: SonarQube Cloud token for protected same-repository analysis. @@ -479,14 +483,14 @@ jobs: dependency-review: name: Dependency review - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} + if: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read steps: - name: Review dependency changes - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-severity: high lint-workflows: @@ -540,7 +544,7 @@ jobs: SONAR_RESULT: ${{ needs.sonar.result }} SONAR_REQUIRED: ${{ inputs.collect-coverage }} DEPENDENCY_RESULT: ${{ needs.dependency-review.result }} - DEPENDENCY_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} + DEPENDENCY_REQUIRED: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} LINT_RESULT: ${{ needs.lint-workflows.result }} run: | $results = [ordered]@{ diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 08cc25a8..d645bf99 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -20,5 +20,6 @@ jobs: uses: ./.github/workflows/ci.yml with: collect-coverage: ${{ vars.SONAR_CI_ENABLED == 'true' && github.ref == 'refs/heads/main' }} + dependency-review: false secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 0b1f45e0..61d2d168 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -22,5 +22,6 @@ jobs: && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' }} + dependency-review: ${{ vars.DEPENDENCY_REVIEW_ENABLED == 'true' }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs b/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs index 20db2c42..da49b715 100644 --- a/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs +++ b/libs/CheatEngine.SDK.Annotations/Lua/LuaClassAttribute.cs @@ -20,11 +20,10 @@ namespace CheatEngine.SDK.Annotations.Lua; /// Cheat Engine class. Instances are immutable and may be used from any thread. /// /// -/// Usage. is : a derived wrapper -/// stands for a different, more derived Cheat Engine class and has to name it; inheriting the attribute would make -/// it claim the name of its base. is -/// : -/// a wrapper type stands for exactly one Cheat Engine class. +/// Usage. The attribute uses the framework default for +/// so metadata consumers can observe it through an inherited +/// wrapper when appropriate. is +/// : a wrapper type stands for exactly one Cheat Engine class. /// /// [AttributeUsage(AttributeTargets.Struct)] diff --git a/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs b/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs index aa19ed7a..5c75b0db 100644 --- a/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs +++ b/libs/CheatEngine.SDK.Annotations/Lua/LuaPropertyAttribute.cs @@ -18,12 +18,10 @@ namespace CheatEngine.SDK.Annotations.Lua; /// Instances are immutable and may be used from any thread. /// /// -/// Usage. is : the attribute asks -/// for -/// the accessors of the one declaration that carries it; an override is another declaration with accessors of its -/// own. is : one managed property -/// maps to -/// one Cheat Engine property. +/// Usage. The attribute uses the framework default for +/// so metadata consumers can observe it through an inherited +/// property when appropriate. is +/// : one managed property maps to one Cheat Engine property. /// /// [AttributeUsage(AttributeTargets.Property)] diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs index c475c0db..9bea073b 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/Layout.cs @@ -37,6 +37,7 @@ public static int AlignmentOf() private struct AlignmentProbe where T : unmanaged { + public byte Prefix; public T Value; } } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaObjectOutputTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaObjectOutputTests.cs index 96a0e112..7a50be69 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaObjectOutputTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaObjectOutputTests.cs @@ -36,11 +36,11 @@ public readonly partial struct Scan """; [Fact] - public void Lua_annotation_usage_is_explicit_and_excludes_global_properties() + public void Lua_annotation_usage_uses_default_inheritance_and_excludes_global_properties() { AttributeUsageAttribute luaClass = AttributeUsage(typeof(LuaClassAttribute)); Assert.Equal(AttributeTargets.Struct, luaClass.ValidOn); - Assert.False(luaClass.Inherited); + Assert.True(luaClass.Inherited); Assert.False(luaClass.AllowMultiple); AttributeUsageAttribute luaGlobal = AttributeUsage(typeof(LuaGlobalAttribute)); @@ -48,6 +48,7 @@ public void Lua_annotation_usage_is_explicit_and_excludes_global_properties() Assert.False(luaGlobal.Inherited); Assert.False(luaGlobal.AllowMultiple); + Assert.True(AttributeUsage(typeof(LuaPropertyAttribute)).Inherited); Assert.False(AttributeUsage(typeof(LuaMethodAttribute)).AllowMultiple); Assert.False(AttributeUsage(typeof(LuaPropertyAttribute)).AllowMultiple); } From 431a6ca5fdc94aa409ff27eb7f795f0118aa58ba Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 18:33:54 +0200 Subject: [PATCH 4/9] Remove unavailable Sonar CI integration Delete the Sonar reusable workflow and remove its secret, coverage, and gate wiring from the base CI path. Keep pull requests focused on the repository-owned build, tests, packaging, Native AOT, dependency review, workflow lint, and final gate. Remove the now-unused Microsoft Testing Platform coverage extension and align CodeRabbit workflow guidance with the remaining pipeline. --- .coderabbit.yaml | 7 +- .github/workflows/ci.yml | 35 +---- .github/workflows/main-ci.yml | 3 - .github/workflows/pull-request-ci.yml | 7 - .github/workflows/sonar.yml | 214 -------------------------- Directory.Packages.props | 1 - eng/Tests.props | 1 - 7 files changed, 4 insertions(+), 264 deletions(-) delete mode 100644 .github/workflows/sonar.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml index f238dc0e..5c77d6fb 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -106,10 +106,9 @@ reviews: package and NativeAOT probes from actual live Cheat Engine host qualification. - path: '.github/**' instructions: >- - Preserve the discover/native/build/test/pack/AOT/gate DAG and its bridge, coverage and NuGet artifact flows. - Require least-privilege permissions and pinned action SHAs. actionlint already runs in pull-request CI; do not - ask for a duplicate CodeRabbit actionlint run. Never use pull_request_target to check out or execute code from - forks. Sonar secrets must remain unavailable to forks. + Preserve the discover/native/build/test/pack/AOT/gate DAG and its bridge and NuGet artifact flows. Require + least-privilege permissions and pinned action SHAs. actionlint already runs in pull-request CI; do not ask for a + duplicate CodeRabbit actionlint run. Never use pull_request_target to check out or execute code from forks. tools: # CodeRabbit is used as the GitHub App; pipeline failures are surfaced through its GitHub Checks integration. github-checks: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc79b335..1d6bc9c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,6 @@ name: CI on: workflow_call: inputs: - collect-coverage: - description: Emit Visual Studio XML coverage from Debug test jobs and upload it as coverage-. - type: boolean - default: false upload-package: description: Upload the packed package as the nuget-package artifact. type: boolean @@ -19,10 +15,6 @@ on: description: Run dependency-review-action for non-draft pull requests. Enable only after GitHub Dependency graph is enabled for the repository. type: boolean default: false - secrets: - SONAR_TOKEN: - description: SonarQube Cloud token for protected same-repository analysis. - required: false permissions: contents: read @@ -292,11 +284,9 @@ jobs: PROJECT: ${{ matrix.project }} CONFIGURATION: ${{ matrix.configuration }} RESULTS: artifacts/test-results/${{ matrix.artifact }} - COLLECT_COVERAGE: ${{ inputs.collect-coverage }} run: | $options = '--project', $env:PROJECT, '-c', $env:CONFIGURATION, '--report-trx', '--results-directory', $env:RESULTS if ($env:CONFIGURATION -eq 'Debug') { $options += '--fail-skips', 'on' } - if ($env:CONFIGURATION -eq 'Debug' -and $env:COLLECT_COVERAGE -eq 'true') { $options += '--coverage', '--coverage-output-format', 'xml', '--coverage-output', 'coverage.xml' } dotnet test @options if ($LASTEXITCODE -ne 0) { throw "Test project '$env:PROJECT' failed with exit code $LASTEXITCODE." @@ -338,15 +328,6 @@ jobs: if-no-files-found: warn retention-days: 14 - - name: Upload coverage - if: ${{ inputs.collect-coverage && matrix.configuration == 'Debug' && !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: coverage-${{ matrix.name }} - path: artifacts/test-results/${{ matrix.artifact }}/coverage.xml - if-no-files-found: error - retention-days: 7 - pack: name: Pack needs: native @@ -471,16 +452,6 @@ jobs: & $harness --load --acknowledge-process-resident-load if ($LASTEXITCODE -ne 0) { throw "Native AOT loader harness load exited with code $LASTEXITCODE." } - sonar: - name: Sonar - needs: [ native, test ] - if: ${{ inputs.collect-coverage }} - uses: ./.github/workflows/sonar.yml - with: - wait-quality-gate: true - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - dependency-review: name: Dependency review if: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} @@ -528,7 +499,7 @@ jobs: gate: name: Gate if: ${{ always() }} - needs: [ discover, native, build, test, pack, aot, sonar, dependency-review, lint-workflows ] + needs: [ discover, native, build, test, pack, aot, dependency-review, lint-workflows ] runs-on: windows-latest timeout-minutes: 5 permissions: { } @@ -541,8 +512,6 @@ jobs: TEST_RESULT: ${{ needs.test.result }} PACK_RESULT: ${{ needs.pack.result }} AOT_RESULT: ${{ needs.aot.result }} - SONAR_RESULT: ${{ needs.sonar.result }} - SONAR_REQUIRED: ${{ inputs.collect-coverage }} DEPENDENCY_RESULT: ${{ needs.dependency-review.result }} DEPENDENCY_REQUIRED: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} LINT_RESULT: ${{ needs.lint-workflows.result }} @@ -554,7 +523,6 @@ jobs: test = $env:TEST_RESULT pack = $env:PACK_RESULT aot = $env:AOT_RESULT - sonar = $env:SONAR_RESULT 'dependency-review' = $env:DEPENDENCY_RESULT 'lint-workflows' = $env:LINT_RESULT } @@ -562,7 +530,6 @@ jobs: '| Job | Result |', '| --- | --- |', $rows | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 $failed = @($results.GetEnumerator() | Where-Object { $_.Key -in 'discover', 'native', 'build', 'test', 'pack', 'aot' -and $_.Value -ne 'success' -or - $_.Key -eq 'sonar' -and $env:SONAR_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or $_.Key -eq 'dependency-review' -and $env:DEPENDENCY_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or $_.Key -eq 'lint-workflows' -and $_.Value -ne 'success' } | ForEach-Object Key) diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index d645bf99..d3a8f682 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -19,7 +19,4 @@ jobs: name: CI uses: ./.github/workflows/ci.yml with: - collect-coverage: ${{ vars.SONAR_CI_ENABLED == 'true' && github.ref == 'refs/heads/main' }} dependency-review: false - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 61d2d168..df5683d1 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -17,11 +17,4 @@ jobs: if: github.event.pull_request.draft == false uses: ./.github/workflows/ci.yml with: - collect-coverage: >- - ${{ vars.SONAR_CI_ENABLED == 'true' - && github.event.pull_request.draft == false - && github.event.pull_request.head.repo.full_name == github.repository - && github.event.pull_request.user.login != 'dependabot[bot]' }} dependency-review: ${{ vars.DEPENDENCY_REVIEW_ENABLED == 'true' }} - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index db215816..00000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,214 +0,0 @@ -name: Sonar - -on: - workflow_call: - inputs: - project-key: - description: SonarQube Cloud project key. - type: string - default: CheatEngineNet_CheatEngine.SDK - organization: - description: SonarQube Cloud organization key. - type: string - default: cheatenginenet - wait-quality-gate: - description: Fail the job when the quality gate fails. - type: boolean - default: true - secrets: - SONAR_TOKEN: - description: SonarQube Cloud token. - required: true - -permissions: - contents: read - -env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - SONAR_SCANNER_VERSION: 11.3.0 - -defaults: - run: - shell: pwsh - -jobs: - analyze: - name: Analyze - runs-on: windows-latest - timeout-minutes: 30 - env: - SONAR_PROJECT_KEY: ${{ inputs.project-key }} - SONAR_ORGANIZATION: ${{ inputs.organization }} - SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }} - steps: - - name: Require token - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - $ErrorActionPreference = 'Stop' - if ([string]::IsNullOrWhiteSpace($env:SONAR_TOKEN)) { - throw 'SONAR_TOKEN is required when SONAR_CI_ENABLED is true.' - } - - # CI analysis and Automatic Analysis cannot run for the same SonarQube Cloud project. Keep this preflight before - # checkout so a PR cannot turn a configuration failure into arbitrary checkout code running in this tokened job. - - name: Require CI-based analysis - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - $ErrorActionPreference = 'Stop' - $uri = "https://sonarcloud.io/api/settings/values?component=$env:SONAR_PROJECT_KEY&keys=sonar.autoscan.enabled" - $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SONAR_TOKEN" } -TimeoutSec 30 -MaximumRetryCount 2 -RetryIntervalSec 3 - if (@($response.settings | Where-Object { $_.key -eq 'sonar.autoscan.enabled' -and $_.value -eq 'true' })) { - throw "Automatic Analysis is enabled on $env:SONAR_PROJECT_KEY. Disable it in SonarQube Cloud: Administration > Analysis Method before enabling SONAR_CI_ENABLED." - } - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - # SonarScanner for .NET uses Java. Pin the same JDK 21 distribution as CheatEngine.Client so scanner behavior is - # reproducible across the SDK and its high-level consumer. - - name: Set up JDK 21 - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - with: - distribution: zulu - java-version: '21' - - - name: Install pinned .NET SDK - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - global-json-file: global.json - - - name: Use CI-built native bridge - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: lua-protection-bridge - path: native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native - - - name: Download coverage - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: coverage-* - path: ${{ runner.temp }}/coverage - - # Do not use the checked-out nuget.config to resolve tooling. This job later uses SONAR_TOKEN, so 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 - id: nuget-config - run: | - $ErrorActionPreference = 'Stop' - $path = Join-Path $env:RUNNER_TEMP 'sonar-nuget.config' - @' - - - - - - - - '@ | Set-Content -LiteralPath $path -Encoding utf8NoBOM - "path=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - - # Restore before scanner begin: the subsequent analysis build is --no-restore, so no PR-supplied source can - # participate while scanner credentials are configured. - - name: Restore from NuGet.org - env: - NUGET_CONFIG: ${{ steps.nuget-config.outputs.path }} - run: | - dotnet restore CheatEngine.SDK.slnx --configfile $env:NUGET_CONFIG - if ($LASTEXITCODE -ne 0) { - throw "Sonar restore failed with exit code $LASTEXITCODE." - } - - - name: Install scanner - env: - NUGET_CONFIG: ${{ steps.nuget-config.outputs.path }} - run: | - dotnet tool install dotnet-sonarscanner --tool-path "$env:RUNNER_TEMP/sonar-scanner" --version $env:SONAR_SCANNER_VERSION --configfile $env:NUGET_CONFIG - if ($LASTEXITCODE -ne 0) { - throw "SonarScanner installation failed with exit code $LASTEXITCODE." - } - - # The begin step ignores the SONAR_TOKEN variable, so the token travels in SONARQUBE_SCANNER_PARAMS, which both scanner steps read. It stays off the command line. - - name: Begin analysis - env: - SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' - run: | - $coverage = "$env:RUNNER_TEMP/coverage" - $reports = @(Get-ChildItem -Path $coverage -Filter *.xml -Recurse -File -ErrorAction SilentlyContinue) - if ($reports.Count -eq 0) { - Write-Host '::error::No coverage report was downloaded. Call ci.yml with collect-coverage set to true.' - exit 1 - } - Write-Host "Coverage reports: $($reports.Count)" - # These findings conflict with deliberate repository contracts. Keep them in the scanner configuration so - # production and test sources do not need Sonar-only attributes or code changes. - $ignoredIssues = @( - @{ Key = 'noLinq'; Rule = 'csharpsquid:S3267'; Resource = '**/*.cs' } - @{ Key = 'unsafeInterop'; Rule = 'csharpsquid:S6640'; Resource = '**/*.cs' } - @{ Key = 'nullableFlow'; Rule = 'csharpsquid:S8970'; Resource = '**/*.cs' } - @{ Key = 'luaExportNames'; Rule = 'csharpsquid:S3218'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Api/**' } - @{ Key = 'luaTypeNames'; Rule = 'csharpsquid:S101'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Types/**' } - @{ Key = 'interopShape'; Rule = 'csharpsquid:S107'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Protected/LuaProtectedExports.cs' } - @{ Key = 'callbackDispose'; Rule = 'csharpsquid:S3881'; Resource = 'libs/CheatEngine.SDK.Lua/Callbacks/LuaCallback.cs' } - @{ Key = 'memoryProtectionName'; Rule = 'csharpsquid:S2342'; Resource = 'libs/CheatEngine.SDK.Engine/Enums/MemoryProtection.cs' } - @{ Key = 'descriptorDispatch'; Rule = 'csharpsquid:S1694'; Resource = 'libs/CheatEngine.SDK.Hosting/Bootstrap/PluginDescriptor.cs' } - @{ Key = 'apiOverloadLayout'; Rule = 'csharpsquid:S4136'; Resource = 'libs/**' } - @{ Key = 'roslynInstances'; Rule = 'csharpsquid:S2325'; Resource = 'analyzers/**' } - @{ Key = 'generatorInstances'; Rule = 'csharpsquid:S2325'; Resource = 'source-generators/**' } - @{ Key = 'emittedFragments'; Rule = 'csharpsquid:S1192'; Resource = 'source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaGlobalCallEmitter.cs' } - @{ Key = 'analyzerComments'; Rule = 'csharpsquid:S125'; Resource = 'analyzers/**' } - @{ Key = 'interopComments'; Rule = 'csharpsquid:S125'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Api/**' } - @{ Key = 'testReflection'; Rule = 'csharpsquid:S3011'; Resource = 'tests/**' } - @{ Key = 'testLiterals'; Rule = 'csharpsquid:S1192'; Resource = 'tests/**' } - @{ Key = 'testGc'; Rule = 'csharpsquid:S1215'; Resource = 'tests/**' } - @{ Key = 'testStaticHooks'; Rule = 'csharpsquid:S2696'; Resource = 'tests/**' } - @{ Key = 'testDoubleDispose'; Rule = 'csharpsquid:S3966'; Resource = 'tests/**' } - @{ Key = 'testBooleanTables'; Rule = 'csharpsquid:S1125'; Resource = 'tests/**' } - @{ Key = 'testComplexity'; Rule = 'csharpsquid:S3776'; Resource = 'tests/**' } - @{ Key = 'testParameters'; Rule = 'csharpsquid:S107'; Resource = 'tests/**' } - @{ Key = 'testMarkerClasses'; Rule = 'csharpsquid:S1118'; Resource = 'tests/**' } - @{ Key = 'testDuplicateScenarios'; Rule = 'csharpsquid:S4144'; Resource = 'tests/**' } - @{ Key = 'testFixedDoubles'; Rule = 'csharpsquid:S3400'; Resource = 'tests/**' } - @{ Key = 'testComments'; Rule = 'csharpsquid:S125'; Resource = 'tests/**' } - ) - $arguments = @( - "/k:$env:SONAR_PROJECT_KEY" - "/o:$env:SONAR_ORGANIZATION" - '/d:sonar.exclusions=artifacts/**,tests/CheatEngine.SDK.Benchmarks/**,tests/CheatEngine.SDK.LivePlugin/**' - # CI publishes managed coverage only. Keep build-time tooling and test-only sources out of the product - # coverage metric instead of presenting an incomplete report as if it covered those paths. - '/d:sonar.coverage.exclusions=tests/**,eng/**' - "/d:sonar.issue.ignore.multicriteria=$(($ignoredIssues.Key) -join ',')" - "/d:sonar.cs.vscoveragexml.reportsPaths=$coverage/**/*.xml" - "/d:sonar.qualitygate.wait=$env:SONAR_WAIT_QUALITY_GATE" - '/d:sonar.qualitygate.timeout=300' - ) - foreach ($issue in $ignoredIssues) { - $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).ruleKey=$($issue.Rule)" - $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).resourceKey=$($issue.Resource)" - } - & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" begin @arguments - if ($LASTEXITCODE -ne 0) { - throw "SonarScanner begin failed with exit code $LASTEXITCODE." - } - - - name: Build - run: | - dotnet build CheatEngine.SDK.slnx -c Debug --no-restore --no-incremental --disable-build-servers - if ($LASTEXITCODE -ne 0) { - throw "Sonar analysis build failed with exit code $LASTEXITCODE." - } - - - name: End analysis - env: - SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' - run: | - & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end - if ($LASTEXITCODE -ne 0) { - throw "SonarScanner end failed with exit code $LASTEXITCODE." - } diff --git a/Directory.Packages.props b/Directory.Packages.props index 21135554..4fbe27a0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,6 @@ - diff --git a/eng/Tests.props b/eng/Tests.props index a9d7ad9d..b709fe7a 100644 --- a/eng/Tests.props +++ b/eng/Tests.props @@ -20,7 +20,6 @@ - From 256854d98291077cac86b05a3c345dcb5f5cb143 Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 18:36:34 +0200 Subject: [PATCH 5/9] Revert "Remove unavailable Sonar CI integration" This reverts commit 431a6ca5fdc94aa409ff27eb7f795f0118aa58ba. --- .coderabbit.yaml | 7 +- .github/workflows/ci.yml | 35 ++++- .github/workflows/main-ci.yml | 3 + .github/workflows/pull-request-ci.yml | 7 + .github/workflows/sonar.yml | 214 ++++++++++++++++++++++++++ Directory.Packages.props | 1 + eng/Tests.props | 1 + 7 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/sonar.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 5c77d6fb..f238dc0e 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -106,9 +106,10 @@ reviews: package and NativeAOT probes from actual live Cheat Engine host qualification. - path: '.github/**' instructions: >- - Preserve the discover/native/build/test/pack/AOT/gate DAG and its bridge and NuGet artifact flows. Require - least-privilege permissions and pinned action SHAs. actionlint already runs in pull-request CI; do not ask for a - duplicate CodeRabbit actionlint run. Never use pull_request_target to check out or execute code from forks. + Preserve the discover/native/build/test/pack/AOT/gate DAG and its bridge, coverage and NuGet artifact flows. + Require least-privilege permissions and pinned action SHAs. actionlint already runs in pull-request CI; do not + ask for a duplicate CodeRabbit actionlint run. Never use pull_request_target to check out or execute code from + forks. Sonar secrets must remain unavailable to forks. tools: # CodeRabbit is used as the GitHub App; pipeline failures are surfaced through its GitHub Checks integration. github-checks: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d6bc9c5..fc79b335 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,10 @@ name: CI on: workflow_call: inputs: + collect-coverage: + description: Emit Visual Studio XML coverage from Debug test jobs and upload it as coverage-. + type: boolean + default: false upload-package: description: Upload the packed package as the nuget-package artifact. type: boolean @@ -15,6 +19,10 @@ on: description: Run dependency-review-action for non-draft pull requests. Enable only after GitHub Dependency graph is enabled for the repository. type: boolean default: false + secrets: + SONAR_TOKEN: + description: SonarQube Cloud token for protected same-repository analysis. + required: false permissions: contents: read @@ -284,9 +292,11 @@ jobs: PROJECT: ${{ matrix.project }} CONFIGURATION: ${{ matrix.configuration }} RESULTS: artifacts/test-results/${{ matrix.artifact }} + COLLECT_COVERAGE: ${{ inputs.collect-coverage }} run: | $options = '--project', $env:PROJECT, '-c', $env:CONFIGURATION, '--report-trx', '--results-directory', $env:RESULTS if ($env:CONFIGURATION -eq 'Debug') { $options += '--fail-skips', 'on' } + if ($env:CONFIGURATION -eq 'Debug' -and $env:COLLECT_COVERAGE -eq 'true') { $options += '--coverage', '--coverage-output-format', 'xml', '--coverage-output', 'coverage.xml' } dotnet test @options if ($LASTEXITCODE -ne 0) { throw "Test project '$env:PROJECT' failed with exit code $LASTEXITCODE." @@ -328,6 +338,15 @@ jobs: if-no-files-found: warn retention-days: 14 + - name: Upload coverage + if: ${{ inputs.collect-coverage && matrix.configuration == 'Debug' && !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-${{ matrix.name }} + path: artifacts/test-results/${{ matrix.artifact }}/coverage.xml + if-no-files-found: error + retention-days: 7 + pack: name: Pack needs: native @@ -452,6 +471,16 @@ jobs: & $harness --load --acknowledge-process-resident-load if ($LASTEXITCODE -ne 0) { throw "Native AOT loader harness load exited with code $LASTEXITCODE." } + sonar: + name: Sonar + needs: [ native, test ] + if: ${{ inputs.collect-coverage }} + uses: ./.github/workflows/sonar.yml + with: + wait-quality-gate: true + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + dependency-review: name: Dependency review if: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} @@ -499,7 +528,7 @@ jobs: gate: name: Gate if: ${{ always() }} - needs: [ discover, native, build, test, pack, aot, dependency-review, lint-workflows ] + needs: [ discover, native, build, test, pack, aot, sonar, dependency-review, lint-workflows ] runs-on: windows-latest timeout-minutes: 5 permissions: { } @@ -512,6 +541,8 @@ jobs: TEST_RESULT: ${{ needs.test.result }} PACK_RESULT: ${{ needs.pack.result }} AOT_RESULT: ${{ needs.aot.result }} + SONAR_RESULT: ${{ needs.sonar.result }} + SONAR_REQUIRED: ${{ inputs.collect-coverage }} DEPENDENCY_RESULT: ${{ needs.dependency-review.result }} DEPENDENCY_REQUIRED: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} LINT_RESULT: ${{ needs.lint-workflows.result }} @@ -523,6 +554,7 @@ jobs: test = $env:TEST_RESULT pack = $env:PACK_RESULT aot = $env:AOT_RESULT + sonar = $env:SONAR_RESULT 'dependency-review' = $env:DEPENDENCY_RESULT 'lint-workflows' = $env:LINT_RESULT } @@ -530,6 +562,7 @@ jobs: '| Job | Result |', '| --- | --- |', $rows | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 $failed = @($results.GetEnumerator() | Where-Object { $_.Key -in 'discover', 'native', 'build', 'test', 'pack', 'aot' -and $_.Value -ne 'success' -or + $_.Key -eq 'sonar' -and $env:SONAR_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or $_.Key -eq 'dependency-review' -and $env:DEPENDENCY_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or $_.Key -eq 'lint-workflows' -and $_.Value -ne 'success' } | ForEach-Object Key) diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index d3a8f682..d645bf99 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -19,4 +19,7 @@ jobs: name: CI uses: ./.github/workflows/ci.yml with: + collect-coverage: ${{ vars.SONAR_CI_ENABLED == 'true' && github.ref == 'refs/heads/main' }} dependency-review: false + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index df5683d1..61d2d168 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -17,4 +17,11 @@ jobs: if: github.event.pull_request.draft == false uses: ./.github/workflows/ci.yml with: + collect-coverage: >- + ${{ vars.SONAR_CI_ENABLED == 'true' + && github.event.pull_request.draft == false + && github.event.pull_request.head.repo.full_name == github.repository + && github.event.pull_request.user.login != 'dependabot[bot]' }} dependency-review: ${{ vars.DEPENDENCY_REVIEW_ENABLED == 'true' }} + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 00000000..db215816 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,214 @@ +name: Sonar + +on: + workflow_call: + inputs: + project-key: + description: SonarQube Cloud project key. + type: string + default: CheatEngineNet_CheatEngine.SDK + organization: + description: SonarQube Cloud organization key. + type: string + default: cheatenginenet + wait-quality-gate: + description: Fail the job when the quality gate fails. + type: boolean + default: true + secrets: + SONAR_TOKEN: + description: SonarQube Cloud token. + required: true + +permissions: + contents: read + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + SONAR_SCANNER_VERSION: 11.3.0 + +defaults: + run: + shell: pwsh + +jobs: + analyze: + name: Analyze + runs-on: windows-latest + timeout-minutes: 30 + env: + SONAR_PROJECT_KEY: ${{ inputs.project-key }} + SONAR_ORGANIZATION: ${{ inputs.organization }} + SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }} + steps: + - name: Require token + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + $ErrorActionPreference = 'Stop' + if ([string]::IsNullOrWhiteSpace($env:SONAR_TOKEN)) { + throw 'SONAR_TOKEN is required when SONAR_CI_ENABLED is true.' + } + + # CI analysis and Automatic Analysis cannot run for the same SonarQube Cloud project. Keep this preflight before + # checkout so a PR cannot turn a configuration failure into arbitrary checkout code running in this tokened job. + - name: Require CI-based analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + $ErrorActionPreference = 'Stop' + $uri = "https://sonarcloud.io/api/settings/values?component=$env:SONAR_PROJECT_KEY&keys=sonar.autoscan.enabled" + $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SONAR_TOKEN" } -TimeoutSec 30 -MaximumRetryCount 2 -RetryIntervalSec 3 + if (@($response.settings | Where-Object { $_.key -eq 'sonar.autoscan.enabled' -and $_.value -eq 'true' })) { + throw "Automatic Analysis is enabled on $env:SONAR_PROJECT_KEY. Disable it in SonarQube Cloud: Administration > Analysis Method before enabling SONAR_CI_ENABLED." + } + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + # SonarScanner for .NET uses Java. Pin the same JDK 21 distribution as CheatEngine.Client so scanner behavior is + # reproducible across the SDK and its high-level consumer. + - name: Set up JDK 21 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: zulu + java-version: '21' + + - name: Install pinned .NET SDK + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + + - name: Use CI-built native bridge + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: lua-protection-bridge + path: native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native + + - name: Download coverage + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: coverage-* + path: ${{ runner.temp }}/coverage + + # Do not use the checked-out nuget.config to resolve tooling. This job later uses SONAR_TOKEN, so 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 + id: nuget-config + run: | + $ErrorActionPreference = 'Stop' + $path = Join-Path $env:RUNNER_TEMP 'sonar-nuget.config' + @' + + + + + + + + '@ | Set-Content -LiteralPath $path -Encoding utf8NoBOM + "path=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + # Restore before scanner begin: the subsequent analysis build is --no-restore, so no PR-supplied source can + # participate while scanner credentials are configured. + - name: Restore from NuGet.org + env: + NUGET_CONFIG: ${{ steps.nuget-config.outputs.path }} + run: | + dotnet restore CheatEngine.SDK.slnx --configfile $env:NUGET_CONFIG + if ($LASTEXITCODE -ne 0) { + throw "Sonar restore failed with exit code $LASTEXITCODE." + } + + - name: Install scanner + env: + NUGET_CONFIG: ${{ steps.nuget-config.outputs.path }} + run: | + dotnet tool install dotnet-sonarscanner --tool-path "$env:RUNNER_TEMP/sonar-scanner" --version $env:SONAR_SCANNER_VERSION --configfile $env:NUGET_CONFIG + if ($LASTEXITCODE -ne 0) { + throw "SonarScanner installation failed with exit code $LASTEXITCODE." + } + + # The begin step ignores the SONAR_TOKEN variable, so the token travels in SONARQUBE_SCANNER_PARAMS, which both scanner steps read. It stays off the command line. + - name: Begin analysis + env: + SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' + run: | + $coverage = "$env:RUNNER_TEMP/coverage" + $reports = @(Get-ChildItem -Path $coverage -Filter *.xml -Recurse -File -ErrorAction SilentlyContinue) + if ($reports.Count -eq 0) { + Write-Host '::error::No coverage report was downloaded. Call ci.yml with collect-coverage set to true.' + exit 1 + } + Write-Host "Coverage reports: $($reports.Count)" + # These findings conflict with deliberate repository contracts. Keep them in the scanner configuration so + # production and test sources do not need Sonar-only attributes or code changes. + $ignoredIssues = @( + @{ Key = 'noLinq'; Rule = 'csharpsquid:S3267'; Resource = '**/*.cs' } + @{ Key = 'unsafeInterop'; Rule = 'csharpsquid:S6640'; Resource = '**/*.cs' } + @{ Key = 'nullableFlow'; Rule = 'csharpsquid:S8970'; Resource = '**/*.cs' } + @{ Key = 'luaExportNames'; Rule = 'csharpsquid:S3218'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Api/**' } + @{ Key = 'luaTypeNames'; Rule = 'csharpsquid:S101'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Types/**' } + @{ Key = 'interopShape'; Rule = 'csharpsquid:S107'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Protected/LuaProtectedExports.cs' } + @{ Key = 'callbackDispose'; Rule = 'csharpsquid:S3881'; Resource = 'libs/CheatEngine.SDK.Lua/Callbacks/LuaCallback.cs' } + @{ Key = 'memoryProtectionName'; Rule = 'csharpsquid:S2342'; Resource = 'libs/CheatEngine.SDK.Engine/Enums/MemoryProtection.cs' } + @{ Key = 'descriptorDispatch'; Rule = 'csharpsquid:S1694'; Resource = 'libs/CheatEngine.SDK.Hosting/Bootstrap/PluginDescriptor.cs' } + @{ Key = 'apiOverloadLayout'; Rule = 'csharpsquid:S4136'; Resource = 'libs/**' } + @{ Key = 'roslynInstances'; Rule = 'csharpsquid:S2325'; Resource = 'analyzers/**' } + @{ Key = 'generatorInstances'; Rule = 'csharpsquid:S2325'; Resource = 'source-generators/**' } + @{ Key = 'emittedFragments'; Rule = 'csharpsquid:S1192'; Resource = 'source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaGlobalCallEmitter.cs' } + @{ Key = 'analyzerComments'; Rule = 'csharpsquid:S125'; Resource = 'analyzers/**' } + @{ Key = 'interopComments'; Rule = 'csharpsquid:S125'; Resource = 'libs/CheatEngine.SDK.Lua.Interop/Api/**' } + @{ Key = 'testReflection'; Rule = 'csharpsquid:S3011'; Resource = 'tests/**' } + @{ Key = 'testLiterals'; Rule = 'csharpsquid:S1192'; Resource = 'tests/**' } + @{ Key = 'testGc'; Rule = 'csharpsquid:S1215'; Resource = 'tests/**' } + @{ Key = 'testStaticHooks'; Rule = 'csharpsquid:S2696'; Resource = 'tests/**' } + @{ Key = 'testDoubleDispose'; Rule = 'csharpsquid:S3966'; Resource = 'tests/**' } + @{ Key = 'testBooleanTables'; Rule = 'csharpsquid:S1125'; Resource = 'tests/**' } + @{ Key = 'testComplexity'; Rule = 'csharpsquid:S3776'; Resource = 'tests/**' } + @{ Key = 'testParameters'; Rule = 'csharpsquid:S107'; Resource = 'tests/**' } + @{ Key = 'testMarkerClasses'; Rule = 'csharpsquid:S1118'; Resource = 'tests/**' } + @{ Key = 'testDuplicateScenarios'; Rule = 'csharpsquid:S4144'; Resource = 'tests/**' } + @{ Key = 'testFixedDoubles'; Rule = 'csharpsquid:S3400'; Resource = 'tests/**' } + @{ Key = 'testComments'; Rule = 'csharpsquid:S125'; Resource = 'tests/**' } + ) + $arguments = @( + "/k:$env:SONAR_PROJECT_KEY" + "/o:$env:SONAR_ORGANIZATION" + '/d:sonar.exclusions=artifacts/**,tests/CheatEngine.SDK.Benchmarks/**,tests/CheatEngine.SDK.LivePlugin/**' + # CI publishes managed coverage only. Keep build-time tooling and test-only sources out of the product + # coverage metric instead of presenting an incomplete report as if it covered those paths. + '/d:sonar.coverage.exclusions=tests/**,eng/**' + "/d:sonar.issue.ignore.multicriteria=$(($ignoredIssues.Key) -join ',')" + "/d:sonar.cs.vscoveragexml.reportsPaths=$coverage/**/*.xml" + "/d:sonar.qualitygate.wait=$env:SONAR_WAIT_QUALITY_GATE" + '/d:sonar.qualitygate.timeout=300' + ) + foreach ($issue in $ignoredIssues) { + $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).ruleKey=$($issue.Rule)" + $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).resourceKey=$($issue.Resource)" + } + & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" begin @arguments + if ($LASTEXITCODE -ne 0) { + throw "SonarScanner begin failed with exit code $LASTEXITCODE." + } + + - name: Build + run: | + dotnet build CheatEngine.SDK.slnx -c Debug --no-restore --no-incremental --disable-build-servers + if ($LASTEXITCODE -ne 0) { + throw "Sonar analysis build failed with exit code $LASTEXITCODE." + } + + - name: End analysis + env: + SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' + run: | + & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end + if ($LASTEXITCODE -ne 0) { + throw "SonarScanner end failed with exit code $LASTEXITCODE." + } diff --git a/Directory.Packages.props b/Directory.Packages.props index 4fbe27a0..21135554 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ + diff --git a/eng/Tests.props b/eng/Tests.props index b709fe7a..a9d7ad9d 100644 --- a/eng/Tests.props +++ b/eng/Tests.props @@ -20,6 +20,7 @@ + From 7c1618018059d8fa99b488b64d5cb36809b576fd Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 18:39:35 +0200 Subject: [PATCH 6/9] Restore Sonar analysis without token preflight --- .github/workflows/ci.yml | 7 ------ .github/workflows/main-ci.yml | 4 +-- .github/workflows/pull-request-ci.yml | 8 +----- .github/workflows/sonar.yml | 36 ++------------------------- 4 files changed, 4 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc79b335..db8ed04c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,6 @@ on: description: Run dependency-review-action for non-draft pull requests. Enable only after GitHub Dependency graph is enabled for the repository. type: boolean default: false - secrets: - SONAR_TOKEN: - description: SonarQube Cloud token for protected same-repository analysis. - required: false - permissions: contents: read @@ -478,8 +473,6 @@ jobs: uses: ./.github/workflows/sonar.yml with: wait-quality-gate: true - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} dependency-review: name: Dependency review diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index d645bf99..51c77b1f 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -19,7 +19,5 @@ jobs: name: CI uses: ./.github/workflows/ci.yml with: - collect-coverage: ${{ vars.SONAR_CI_ENABLED == 'true' && github.ref == 'refs/heads/main' }} + collect-coverage: true dependency-review: false - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 61d2d168..843aeec4 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -17,11 +17,5 @@ jobs: if: github.event.pull_request.draft == false uses: ./.github/workflows/ci.yml with: - collect-coverage: >- - ${{ vars.SONAR_CI_ENABLED == 'true' - && github.event.pull_request.draft == false - && github.event.pull_request.head.repo.full_name == github.repository - && github.event.pull_request.user.login != 'dependabot[bot]' }} + collect-coverage: true dependency-review: ${{ vars.DEPENDENCY_REVIEW_ENABLED == 'true' }} - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index db215816..9842bc30 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -15,11 +15,6 @@ on: description: Fail the job when the quality gate fails. type: boolean default: true - secrets: - SONAR_TOKEN: - description: SonarQube Cloud token. - required: true - permissions: contents: read @@ -42,28 +37,6 @@ jobs: SONAR_ORGANIZATION: ${{ inputs.organization }} SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }} steps: - - name: Require token - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - $ErrorActionPreference = 'Stop' - if ([string]::IsNullOrWhiteSpace($env:SONAR_TOKEN)) { - throw 'SONAR_TOKEN is required when SONAR_CI_ENABLED is true.' - } - - # CI analysis and Automatic Analysis cannot run for the same SonarQube Cloud project. Keep this preflight before - # checkout so a PR cannot turn a configuration failure into arbitrary checkout code running in this tokened job. - - name: Require CI-based analysis - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - $ErrorActionPreference = 'Stop' - $uri = "https://sonarcloud.io/api/settings/values?component=$env:SONAR_PROJECT_KEY&keys=sonar.autoscan.enabled" - $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SONAR_TOKEN" } -TimeoutSec 30 -MaximumRetryCount 2 -RetryIntervalSec 3 - if (@($response.settings | Where-Object { $_.key -eq 'sonar.autoscan.enabled' -and $_.value -eq 'true' })) { - throw "Automatic Analysis is enabled on $env:SONAR_PROJECT_KEY. Disable it in SonarQube Cloud: Administration > Analysis Method before enabling SONAR_CI_ENABLED." - } - - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -95,8 +68,8 @@ jobs: pattern: coverage-* path: ${{ runner.temp }}/coverage - # Do not use the checked-out nuget.config to resolve tooling. This job later uses SONAR_TOKEN, so the scanner - # package and every restored dependency must come only from nuget.org, not a source a pull request can redirect. + # 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 id: nuget-config run: | @@ -133,10 +106,7 @@ jobs: throw "SonarScanner installation failed with exit code $LASTEXITCODE." } - # The begin step ignores the SONAR_TOKEN variable, so the token travels in SONARQUBE_SCANNER_PARAMS, which both scanner steps read. It stays off the command line. - name: Begin analysis - env: - SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' run: | $coverage = "$env:RUNNER_TEMP/coverage" $reports = @(Get-ChildItem -Path $coverage -Filter *.xml -Recurse -File -ErrorAction SilentlyContinue) @@ -205,8 +175,6 @@ jobs: } - name: End analysis - env: - SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' run: | & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end if ($LASTEXITCODE -ne 0) { From 4479274c8149638cb234b0c28a0120e82d6a7217 Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 20:46:15 +0200 Subject: [PATCH 7/9] Rebuild CI around build-once artifacts and a correct gate Build and test the solution once per configuration, pack the tested Release build, reuse the native bridge, ABI facts and coverage artifacts, restore the Sonar token with fork and Dependabot guards, run actionlint on every event, and evaluate the gate generically over all jobs. --- .github/workflows/ci.yml | 404 ++++++++-------------- .github/workflows/main-ci.yml | 9 +- .github/workflows/pull-request-ci.yml | 9 +- .github/workflows/sonar.yml | 64 +++- CheatEngine.SDK.slnx | 1 + Directory.Packages.props | 2 + eng/Tests.props | 1 + libs/CheatEngine.SDK.Abi/README.md | 2 +- tests/CheatEngine.SDK.Abi.Tests/README.md | 8 +- tests/native-abi-fixture/README.md | 5 +- 10 files changed, 208 insertions(+), 297 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db8ed04c..ebfac757 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,37 @@ name: CI +# Reusable pipeline for pull-request-ci.yml, main-ci.yml and release.yml. +# +# native ─► build-test (Debug, Release) ─► sonar ─┐ +# └► aot ─────────────────────────────────┼─► gate (the only required check: "CI / Gate") +# lint ──────────────────────────────────────────┘ +# +# Jobs never rebuild what an upstream job produced; they exchange artifacts: +# lua-protection-bridge, classic-abi-fixture-facts native → build-test, aot +# coverage build-test (Debug) → sonar +# nuget-package build-test (Release) → sonar, release publish, reviewers +# test-results- build-test → humans + on: workflow_call: inputs: - collect-coverage: - description: Emit Visual Studio XML coverage from Debug test jobs and upload it as coverage-. - type: boolean - default: false - upload-package: - description: Upload the packed package as the nuget-package artifact. - type: boolean - default: false - test-release: - description: Also run every test project against the Release build, which is what ships. - type: boolean - default: false - dependency-review: - description: Run dependency-review-action for non-draft pull requests. Enable only after GitHub Dependency graph is enabled for the repository. + sonar: + description: Run the SonarQube Cloud analysis. Merge-queue runs, fork pull requests and Dependabot always skip it. type: boolean default: false + package-version: + description: When set, the Release leg must produce exactly CheatEngine.SDK..nupkg. + type: string + default: '' + package-retention-days: + description: Days to keep the nuget-package artifact. + type: number + default: 7 + secrets: + SONAR_TOKEN: + description: SonarQube Cloud analysis token. Needed only when sonar is true. + required: false + permissions: contents: read @@ -27,45 +40,6 @@ defaults: shell: pwsh jobs: - discover: - name: Discover tests - runs-on: windows-latest - timeout-minutes: 5 - outputs: - matrix: ${{ steps.projects.outputs.matrix }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - sparse-checkout: tests - persist-credentials: false - - - name: Find test projects - id: projects - env: - TEST_RELEASE: ${{ inputs.test-release }} - # Mirrors eng/Tests.props: a test project is any tests/**/*.Tests project, so a new one needs no edit here. - run: | - $projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File | Sort-Object Name) - if ($projects.Count -eq 0) { throw 'No *.Tests.csproj project found under tests.' } - $configurations = if ($env:TEST_RELEASE -eq 'true') { 'Debug', 'Release' } else { 'Debug' } - $include = foreach ($project in $projects) { - foreach ($configuration in $configurations) { - $suffix = if ($configuration -eq 'Release') { '-release' } else { '' } - [ordered]@{ - name = $project.BaseName - project = [IO.Path]::GetRelativePath($PWD.Path, $project.FullName).Replace('\', '/') - configuration = $configuration - label = "$($project.BaseName)$(if ($suffix) { ' (Release)' })" - artifact = "$($project.BaseName)$suffix" - } - } - } - $matrix = ConvertTo-Json -Compress -InputObject ([ordered]@{ include = @($include) }) - "matrix=$matrix" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - Write-Host "Found $($projects.Count) test projects." - $projects | ForEach-Object { Write-Host " $($_.BaseName)" } - native: name: Build native bridge runs-on: windows-latest @@ -77,7 +51,7 @@ jobs: persist-credentials: false - name: Setup xmake - uses: xmake-io/github-action-setup-xmake@3a1a5dddfc7fa625d9a698738334bf55655a861a # v1 + uses: xmake-io/github-action-setup-xmake@3a1a5dddfc7fa625d9a698738334bf55655a861a # v1.2.5 with: xmake-version: '3.0.9' @@ -168,6 +142,7 @@ jobs: # This compiles the C++ transcription under MSVC x64 and validates its emitted facts. It proves the checked-in # header fixture and managed layout numbers agree; it does not contact or qualify a live Cheat Engine host. + # The Debug build-test leg compares these facts with the managed ABI measurements. - name: Build and validate classic ABI fixture run: | $ErrorActionPreference = 'Stop' @@ -176,30 +151,6 @@ jobs: throw "Classic ABI fixture build failed with exit code $LASTEXITCODE." } - # The fixture's fixed facts are first schema-validated in its build script. Build the managed ABI test app and - # pass that same file to its direct comparer so C++ x64 measurements are checked against managed sizeof, offset, - # and alignment measurements in one CI execution. The executable is used deliberately: native MTP currently - # discovers this xUnit v3 app reliably through its generated host, while the SDK command only sees its module. - - name: Setup .NET for classic ABI fact comparison - uses: ./.github/actions/setup-dotnet - with: - restore: tests/CheatEngine.SDK.Abi.Tests/CheatEngine.SDK.Abi.Tests.csproj - - - name: Compare native fixture facts with managed ABI measurements - env: - CE77_NATIVE_ABI_FACTS_PATH: ${{ github.workspace }}/artifacts/native-abi-fixture/ce77-native-abi-facts.txt - CE77_NATIVE_ABI_REQUIRED: 'true' - run: | - $ErrorActionPreference = 'Stop' - dotnet build tests/CheatEngine.SDK.Abi.Tests/CheatEngine.SDK.Abi.Tests.csproj -c Debug --no-restore --disable-build-servers - if ($LASTEXITCODE -ne 0) { - throw "Managed ABI fact comparer build failed with exit code $LASTEXITCODE." - } - & ./artifacts/bin/CheatEngine.SDK.Abi.Tests/debug/CheatEngine.SDK.Abi.Tests.exe --fail-skips on - if ($LASTEXITCODE -ne 0) { - throw "Managed ABI fact comparer exited with code $LASTEXITCODE." - } - - name: Upload classic ABI fixture facts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -208,16 +159,23 @@ jobs: if-no-files-found: error retention-days: 14 - build: - name: Build + build-test: + name: Build and test (${{ matrix.configuration }}) needs: native runs-on: windows-latest - timeout-minutes: 20 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + configuration: [ Debug, Release ] + env: + CONFIGURATION: ${{ matrix.configuration }} + RESULTS: artifacts/test-results/${{ matrix.configuration }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 # MinVer computes the version from tags and history + fetch-depth: 0 # MinVer and the packaging tests need tags and full history persist-credentials: false - name: Setup .NET @@ -231,159 +189,92 @@ jobs: name: lua-protection-bridge path: native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native - # Tests run against Debug, where some guards exist only. Release is what ships. - - name: Build Debug - run: | - dotnet build CheatEngine.SDK.slnx --no-restore -c Debug - if ($LASTEXITCODE -ne 0) { - throw "Debug solution build failed with exit code $LASTEXITCODE." - } - - # Do not rely only on the per-project matrix below: native MTP must also discover every test executable when - # invoked through the solution. The preceding build is deliberately part of this job, so --no-build cannot - # silently exercise stale or incomplete test-host output. - - name: Test Debug solution discovery - run: | - dotnet test --solution CheatEngine.SDK.slnx --no-build --no-restore -c Debug --fail-skips on - if ($LASTEXITCODE -ne 0) { - throw "Debug solution test discovery failed with exit code $LASTEXITCODE." - } + - name: Use classic ABI fixture facts + if: matrix.configuration == 'Debug' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: classic-abi-fixture-facts + path: artifacts/native-abi-fixture - - name: Build Release + - name: Build run: | - dotnet build CheatEngine.SDK.slnx --no-restore -c Release + dotnet build CheatEngine.SDK.slnx -c $env:CONFIGURATION --no-restore if ($LASTEXITCODE -ne 0) { - throw "Release solution build failed with exit code $LASTEXITCODE." + throw "$env:CONFIGURATION solution build failed with exit code $LASTEXITCODE." } - test: - name: Test ${{ matrix.label }} - needs: [ discover, native ] - runs-on: windows-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.discover.outputs.matrix) }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 # MinVer computes the version from tags and history - persist-credentials: false - - - name: Setup .NET - uses: ./.github/actions/setup-dotnet - - - name: Use CI-built native bridge - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: lua-protection-bridge - path: native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native - - # A skipped test fails the Debug job: NativeLua tests skip when the bundled Lua DLL does not bind, and a green check must not hide that. - # Release keeps one legitimate skip, a guard that only exists in Debug. + # One parallel run over every tests/**/*.Tests project of the solution, which also proves that the solution + # discovers every test module. A skip fails both configurations: NativeLua tests skip when the bundled Lua DLL + # does not bind, and a green check must not hide that. In Debug, the facts the native job built make the managed + # ABI comparison mandatory, and every module writes its own GUID-named coverage report. - name: Test - env: - PROJECT: ${{ matrix.project }} - CONFIGURATION: ${{ matrix.configuration }} - RESULTS: artifacts/test-results/${{ matrix.artifact }} - COLLECT_COVERAGE: ${{ inputs.collect-coverage }} run: | - $options = '--project', $env:PROJECT, '-c', $env:CONFIGURATION, '--report-trx', '--results-directory', $env:RESULTS - if ($env:CONFIGURATION -eq 'Debug') { $options += '--fail-skips', 'on' } - if ($env:CONFIGURATION -eq 'Debug' -and $env:COLLECT_COVERAGE -eq 'true') { $options += '--coverage', '--coverage-output-format', 'xml', '--coverage-output', 'coverage.xml' } + $options = @( + '--solution', 'CheatEngine.SDK.slnx', '-c', $env:CONFIGURATION, '--no-build', '--results-directory', $env:RESULTS, + '--fail-skips', 'on', '--report-trx', '--report-gh', '--report-gh-groups', 'off' + ) + if ($env:CONFIGURATION -eq 'Debug') { + $env:CE77_NATIVE_ABI_FACTS_PATH = Join-Path $env:GITHUB_WORKSPACE 'artifacts/native-abi-fixture/ce77-native-abi-facts.txt' + $env:CE77_NATIVE_ABI_REQUIRED = 'true' + $options += '--coverage', '--coverage-output-format', 'xml' + } dotnet test @options if ($LASTEXITCODE -ne 0) { - throw "Test project '$env:PROJECT' failed with exit code $LASTEXITCODE." + throw "$env:CONFIGURATION tests failed with exit code $LASTEXITCODE." } - - - name: Summarize results - if: ${{ !cancelled() }} - env: - NAME: ${{ matrix.label }} - RESULTS: artifacts/test-results/${{ matrix.artifact }} - run: | - $files = @(Get-ChildItem -Path $env:RESULTS -Filter *.trx -File -ErrorAction SilentlyContinue) - if ($files.Count -eq 0) { Write-Host '::warning::The test run produced no TRX report.'; return } - $lines = foreach ($file in $files) { - $results = @(([xml](Get-Content -LiteralPath $file.FullName -Raw)).TestRun.Results.UnitTestResult | Where-Object { $_ }) - $passed = @($results | Where-Object outcome -eq 'Passed').Count - $skipped = @($results | Where-Object outcome -eq 'NotExecuted').Count - $failed = @($results | Where-Object { $_.outcome -notin 'Passed', 'NotExecuted' }) - "### $env:NAME" - '' - '| Total | Passed | Failed | Skipped |' - '| ---: | ---: | ---: | ---: |' - "| $($results.Count) | $passed | $($failed.Count) | $skipped |" - if ($failed.Count -gt 0) { - '' - $failed | Select-Object -First 20 | ForEach-Object { "- ``$($_.testName)``" } - $rest = @($failed | Select-Object -Skip 20).Count - if ($rest -gt 0) { "- and $rest more" } + if ($env:CONFIGURATION -eq 'Debug') { + $modules = @(Get-ChildItem -LiteralPath $env:RESULTS -Filter *.trx -File).Count + $reports = @(Get-ChildItem -LiteralPath $env:RESULTS -Filter *.xml -File).Count + if ($reports -eq 0 -or $reports -ne $modules) { + throw "Expected one coverage report per test module ($modules), found $reports." } } - $lines | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - - - name: Upload test results - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: test-results-${{ matrix.artifact }} - path: artifacts/test-results/${{ matrix.artifact }}/*.trx - if-no-files-found: warn - retention-days: 14 - - - name: Upload coverage - if: ${{ inputs.collect-coverage && matrix.configuration == 'Debug' && !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: coverage-${{ matrix.name }} - path: artifacts/test-results/${{ matrix.artifact }}/coverage.xml - if-no-files-found: error - retention-days: 7 - - pack: - name: Pack - needs: native - runs-on: windows-latest - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 # MinVer computes the version from tags and history - persist-credentials: false - - - name: Setup .NET - uses: ./.github/actions/setup-dotnet - - - name: Use CI-built native bridge - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: lua-protection-bridge - path: native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native + # The shipped package comes from the build the tests just ran against. The pack is incremental: nothing recompiles. - name: Pack + if: matrix.configuration == 'Release' + env: + PACKAGE_VERSION: ${{ inputs.package-version }} run: | - dotnet pack src/CheatEngine.SDK -c Release -o artifacts/nuget + dotnet pack src/CheatEngine.SDK -c Release --no-restore -o artifacts/nuget if ($LASTEXITCODE -ne 0) { throw "SDK package creation failed with exit code $LASTEXITCODE." } - - - name: Verify package - run: | $packages = @(Get-ChildItem -Path artifacts/nuget -Filter *.nupkg -File) - if ($packages.Count -ne 1) { throw "Expected one package in artifacts/nuget, found $($packages.Count)." } - Write-Host "Packed $($packages[0].Name)." + if ($packages.Count -ne 1) { + throw "Expected one package in artifacts/nuget, found $($packages.Count)." + } + if ($env:PACKAGE_VERSION -and $packages[0].Name -ne "CheatEngine.SDK.$env:PACKAGE_VERSION.nupkg") { + throw "Packed $($packages[0].Name), but the release requires CheatEngine.SDK.$env:PACKAGE_VERSION.nupkg." + } + "Packed ``$($packages[0].Name)``." | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - name: Upload package - if: ${{ inputs.upload-package }} + if: matrix.configuration == 'Release' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: nuget-package path: artifacts/nuget/*.nupkg if-no-files-found: error - retention-days: 90 + retention-days: ${{ inputs.package-retention-days }} + + - name: Upload coverage + if: matrix.configuration == 'Debug' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-results-${{ matrix.configuration }} + path: ${{ env.RESULTS }}/*.trx + if-no-files-found: warn + retention-days: 7 aot: name: Native AOT publication probe @@ -466,34 +357,29 @@ jobs: & $harness --load --acknowledge-process-resident-load if ($LASTEXITCODE -ne 0) { throw "Native AOT loader harness load exited with code $LASTEXITCODE." } + # Secrets never reach fork or Dependabot runs, and a merge-queue branch is analysed again once it lands on main. + # Pull requests fail on the quality gate; main only reports it. sonar: name: Sonar - needs: [ native, test ] - if: ${{ inputs.collect-coverage }} + 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: true + wait-quality-gate: ${{ github.event_name == 'pull_request' }} + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - dependency-review: - name: Dependency review - if: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - steps: - - name: Review dependency changes - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - with: - fail-on-severity: high - lint-workflows: + lint: name: Lint workflows - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} - runs-on: windows-latest + runs-on: ubuntu-latest timeout-minutes: 5 env: ACTIONLINT_VERSION: 1.7.12 - ACTIONLINT_SHA256: 6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9 + ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 # linux_amd64, official checksums file steps: - name: Checkout workflow definitions uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -504,61 +390,47 @@ jobs: - name: Run actionlint run: | $ErrorActionPreference = 'Stop' - $archive = Join-Path $env:RUNNER_TEMP 'actionlint.zip' - $url = "https://github.com/rhysd/actionlint/releases/download/v$env:ACTIONLINT_VERSION/actionlint_$($env:ACTIONLINT_VERSION)_windows_amd64.zip" + $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." } - $destination = Join-Path $env:RUNNER_TEMP 'actionlint' - Expand-Archive -LiteralPath $archive -DestinationPath $destination - & (Join-Path $destination 'actionlint.exe') -color + 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." } gate: name: Gate - if: ${{ always() }} - needs: [ discover, native, build, test, pack, aot, sonar, dependency-review, lint-workflows ] - runs-on: windows-latest + # always(): a failed or cancelled job must turn the required check red instead of skipping it. + if: always() + needs: [ native, build-test, aot, sonar, lint ] + runs-on: ubuntu-latest timeout-minutes: 5 permissions: { } steps: - name: Check results env: - DISCOVER_RESULT: ${{ needs.discover.result }} - NATIVE_RESULT: ${{ needs.native.result }} - BUILD_RESULT: ${{ needs.build.result }} - TEST_RESULT: ${{ needs.test.result }} - PACK_RESULT: ${{ needs.pack.result }} - AOT_RESULT: ${{ needs.aot.result }} - SONAR_RESULT: ${{ needs.sonar.result }} - SONAR_REQUIRED: ${{ inputs.collect-coverage }} - DEPENDENCY_RESULT: ${{ needs.dependency-review.result }} - DEPENDENCY_REQUIRED: ${{ inputs.dependency-review && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} - LINT_RESULT: ${{ needs.lint-workflows.result }} + NEEDS: ${{ toJSON(needs) }} run: | - $results = [ordered]@{ - discover = $env:DISCOVER_RESULT - native = $env:NATIVE_RESULT - build = $env:BUILD_RESULT - test = $env:TEST_RESULT - pack = $env:PACK_RESULT - aot = $env:AOT_RESULT - sonar = $env:SONAR_RESULT - 'dependency-review' = $env:DEPENDENCY_RESULT - 'lint-workflows' = $env:LINT_RESULT - } - $rows = $results.GetEnumerator() | ForEach-Object { "| $($_.Key) | $($_.Value) |" } - '| Job | Result |', '| --- | --- |', $rows | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - $failed = @($results.GetEnumerator() | Where-Object { - $_.Key -in 'discover', 'native', 'build', 'test', 'pack', 'aot' -and $_.Value -ne 'success' -or - $_.Key -eq 'sonar' -and $env:SONAR_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or - $_.Key -eq 'dependency-review' -and $env:DEPENDENCY_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or - $_.Key -eq 'lint-workflows' -and $_.Value -ne 'success' - } | ForEach-Object Key) + # Every job must succeed. Only sonar may be skipped: its own condition turns it off for releases, the merge + # queue, forks and Dependabot. A sonar skipped because build-test failed is caught by build-test's result. + $mayBeSkipped = @('sonar') + $needs = $env:NEEDS | ConvertFrom-Json -AsHashtable + $failed = [Collections.Generic.List[string]]::new() + $rows = foreach ($job in @($needs.Keys | Sort-Object)) { + $result = $needs[$job].result + $allowed = if ($job -in $mayBeSkipped) { 'success', 'skipped' } else { 'success' } + if ($result -notin $allowed) { $failed.Add("$job ($result)") } + "| $job | $result |" + } + @('| Job | Result |', '| --- | --- |') + $rows | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 if ($failed.Count -gt 0) { Write-Host "::error::Not successful: $($failed -join ', ')." exit 1 diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 51c77b1f..baaadf1f 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -7,9 +7,7 @@ on: types: [ checks_requested ] workflow_dispatch: -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false +# No concurrency group: every main commit and merge group keeps its own complete run. permissions: contents: read @@ -19,5 +17,6 @@ jobs: name: CI uses: ./.github/workflows/ci.yml with: - collect-coverage: true - dependency-review: false + sonar: true # ci.yml skips it for the merge queue and only reports the quality gate outside pull requests + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 843aeec4..c3502c32 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -4,6 +4,7 @@ on: pull_request: types: [ opened, synchronize, reopened, ready_for_review ] +# A new push supersedes the previous run of the same pull request. concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -14,8 +15,10 @@ permissions: jobs: ci: name: CI - if: github.event.pull_request.draft == false + # Drafts do not run CI. "Ready for review" starts it; until then the required CI / Gate check stays pending. + if: ${{ !github.event.pull_request.draft }} uses: ./.github/workflows/ci.yml with: - collect-coverage: true - dependency-review: ${{ vars.DEPENDENCY_REVIEW_ENABLED == 'true' }} + sonar: true # ci.yml still skips Sonar for fork and Dependabot pull requests, which receive no secrets + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 9842bc30..8c7d4c2c 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -1,5 +1,8 @@ name: Sonar +# Reusable SonarQube Cloud analysis, called by ci.yml after build-test. It reuses the coverage and nuget-package +# artifacts of the same run; only the instrumented build is repeated, because the scanner must observe a compilation. + on: workflow_call: inputs: @@ -12,15 +15,18 @@ on: type: string default: cheatenginenet wait-quality-gate: - description: Fail the job when the quality gate fails. + description: Fail the job when the quality gate fails. Pull requests wait for it; main only reports it. type: boolean default: true + secrets: + SONAR_TOKEN: + description: SonarQube Cloud analysis token. The job fails fast when it is empty. + required: false + permissions: contents: read env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true SONAR_SCANNER_VERSION: 11.3.0 defaults: @@ -37,10 +43,21 @@ jobs: SONAR_ORGANIZATION: ${{ inputs.organization }} SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }} 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. + - name: Require token + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + if ([string]::IsNullOrEmpty($env:SONAR_TOKEN)) { + Write-Host '::error title=SONAR_TOKEN missing::Add the repository secret SONAR_TOKEN (a SonarQube Cloud analysis token for this project).' + exit 1 + } + - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 + fetch-depth: 0 # SCM blame for new-code detection persist-credentials: false # SonarScanner for .NET uses Java. Pin the same JDK 21 distribution as CheatEngine.Client so scanner behavior is @@ -51,22 +68,21 @@ jobs: distribution: zulu java-version: '21' - - name: Install pinned .NET SDK - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - global-json-file: global.json + # SDK and CLI settings only: the restore below must not use the checked-out nuget.config. + - name: Setup .NET + uses: ./.github/actions/setup-dotnet - - name: Use CI-built native bridge + - name: Download coverage uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: lua-protection-bridge - path: native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native + name: coverage + path: ${{ runner.temp }}/coverage - - name: Download coverage + - name: Download package uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - pattern: coverage-* - path: ${{ runner.temp }}/coverage + name: nuget-package + path: ${{ runner.temp }}/package # 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. @@ -106,15 +122,27 @@ jobs: throw "SonarScanner installation failed with exit code $LASTEXITCODE." } + # The begin step ignores the SONAR_TOKEN variable, so the token travels in SONARQUBE_SCANNER_PARAMS, which both + # scanner steps read. It stays off the command line and out of the build step that compiles pull-request code. - name: Begin analysis + env: + SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' run: | $coverage = "$env:RUNNER_TEMP/coverage" $reports = @(Get-ChildItem -Path $coverage -Filter *.xml -Recurse -File -ErrorAction SilentlyContinue) if ($reports.Count -eq 0) { - Write-Host '::error::No coverage report was downloaded. Call ci.yml with collect-coverage set to true.' + Write-Host '::error::The coverage artifact holds no report.' + exit 1 + } + # The new-code period follows sonar.projectVersion. The core version of the package built in this run (1.0.1 + # for 1.0.1-alpha.0.37) changes only at a release, so new code on main means changes since the last release. + $packages = @(Get-ChildItem -Path "$env:RUNNER_TEMP/package" -Filter 'CheatEngine.SDK.*.nupkg' -File) + if ($packages.Count -ne 1 -or $packages[0].Name -notmatch '^CheatEngine\.SDK\.(?\d+\.\d+\.\d+)') { + Write-Host '::error::The nuget-package artifact must hold exactly one CheatEngine.SDK package.' exit 1 } - Write-Host "Coverage reports: $($reports.Count)" + $version = $Matches['version'] + Write-Host "Coverage reports: $($reports.Count); project version: $version" # These findings conflict with deliberate repository contracts. Keep them in the scanner configuration so # production and test sources do not need Sonar-only attributes or code changes. $ignoredIssues = @( @@ -149,6 +177,7 @@ jobs: $arguments = @( "/k:$env:SONAR_PROJECT_KEY" "/o:$env:SONAR_ORGANIZATION" + "/v:$version" '/d:sonar.exclusions=artifacts/**,tests/CheatEngine.SDK.Benchmarks/**,tests/CheatEngine.SDK.LivePlugin/**' # CI publishes managed coverage only. Keep build-time tooling and test-only sources out of the product # coverage metric instead of presenting an incomplete report as if it covered those paths. @@ -167,6 +196,7 @@ jobs: throw "SonarScanner begin failed with exit code $LASTEXITCODE." } + # The scanner turns TreatWarningsAsErrors off for this build, which is why build-test, not this job, gates. - name: Build run: | dotnet build CheatEngine.SDK.slnx -c Debug --no-restore --no-incremental --disable-build-servers @@ -175,6 +205,8 @@ jobs: } - name: End analysis + env: + SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' run: | & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end if ($LASTEXITCODE -ne 0) { diff --git a/CheatEngine.SDK.slnx b/CheatEngine.SDK.slnx index 82b0bfe7..5948c8b7 100644 --- a/CheatEngine.SDK.slnx +++ b/CheatEngine.SDK.slnx @@ -23,6 +23,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 21135554..f07b0ced 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,8 @@ + + diff --git a/eng/Tests.props b/eng/Tests.props index a9d7ad9d..b962f32b 100644 --- a/eng/Tests.props +++ b/eng/Tests.props @@ -22,6 +22,7 @@ + diff --git a/libs/CheatEngine.SDK.Abi/README.md b/libs/CheatEngine.SDK.Abi/README.md index 8b337c92..168a61bd 100644 --- a/libs/CheatEngine.SDK.Abi/README.md +++ b/libs/CheatEngine.SDK.Abi/README.md @@ -67,7 +67,7 @@ and [pinned The source index records the installed-file hashes reviewed for the historic CE 7.7 baseline. The independently compiled fixture is deliberately more limited: it compiles a checked-in transcription of the pinned upstream C-header subset under MSVC x64, validates 104 facts, and compares its `sizeof`, `offsetof`, alignment, export, and topology facts -with a versioned expectation. The native CI job also passes that facts file into a compiled managed test, which measures +with a versioned expectation. The Debug CI test run also passes that facts file into a compiled managed test, which measures the matching managed record sizes, offsets, and alignments directly. It is therefore a `compiled-transcription-fixture` proof, not proof that a live CE host loads a slot, uses a given Pascal boolean width, or provides a non-null table entry. The records stay internal until a diff --git a/tests/CheatEngine.SDK.Abi.Tests/README.md b/tests/CheatEngine.SDK.Abi.Tests/README.md index fbdba0e1..5b9487e0 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/README.md +++ b/tests/CheatEngine.SDK.Abi.Tests/README.md @@ -26,15 +26,15 @@ numbers are literals next to the assertion, never derived from the code under te | Address-of arithmetic | `Layout.SizeOf()` and `Layout.OffsetOf` measure the size and every field offset. | | Raw bytes | The packed 36-byte init record is written into a guard-filled buffer at an aligned and an odd address. The 48-byte exports record is built as raw bytes, then read through the struct. | | Host simulation | `&Method` of a real `[UnmanagedCallersOnly]` `Stdcall` function is stored in every typed function-pointer slot, then called through the field. | -| Native-fact comparison | The native CI job provides the checked `ce77-native-abi-facts.txt` and sets its required gate; a compiled managed test measures every fixture-covered layout and compares its size, alignment, and offsets to that output. | +| Native-fact comparison | The native CI job builds the checked `ce77-native-abi-facts.txt`; the Debug build-test job passes it in and sets the required gate. A compiled managed test measures every fixture-covered layout and compares its size, alignment, and offsets to that output. | The test assembly applies `[assembly: DisableRuntimeMarshalling]`, so calls take the path a plugin takes. `BoolCallBoundaryTests` calls through pointers whose signature differs by one substitution (`int` for `Bool32`, `byte` for `Bool8`). Every branch of `AbiArchitecture` is tested through its internal overloads. -`NativeAbiFixtureManagedComparisonTests` has no local fixture dependency. The native CI job supplies -`CE77_NATIVE_ABI_FACTS_PATH` and sets `CE77_NATIVE_ABI_REQUIRED=true`, which makes a missing facts path fail the -comparison gate. Ordinary managed runs omit the required mode and can omit the facts path; that local opt-out is +`NativeAbiFixtureManagedComparisonTests` has no local fixture dependency. The Debug build-test CI job downloads the +facts the native job built, supplies `CE77_NATIVE_ABI_FACTS_PATH` and sets `CE77_NATIVE_ABI_REQUIRED=true` for its +solution test run, which makes a missing facts path fail the comparison gate. Ordinary managed runs omit the required mode and can omit the facts path; that local opt-out is intentional and is not a substitute for an exact-host test. - `AssemblyConformanceTests` compares an expected-size table with the public structs in both directions, then runs the diff --git a/tests/native-abi-fixture/README.md b/tests/native-abi-fixture/README.md index 3d839c6b..2be201e3 100644 --- a/tests/native-abi-fixture/README.md +++ b/tests/native-abi-fixture/README.md @@ -64,7 +64,8 @@ sentinel.plugin_version.outer_guard=passed ``` The script writes `ce77-native-abi-facts.txt` and validates every emitted key and value with -`Validate-Facts.ps1`. CI invokes this fixture in the native job, then passes its facts path to the compiled managed ABI -test with `CE77_NATIVE_ABI_REQUIRED=true`; that required mode fails the comparison gate if the path is absent. Ordinary +`Validate-Facts.ps1`. CI invokes this fixture in the native job and publishes the facts as the +`classic-abi-fixture-facts` artifact; the Debug build-test job passes that path to the compiled managed ABI test with +`CE77_NATIVE_ABI_REQUIRED=true`; that required mode fails the comparison gate if the path is absent. Ordinary ABI tests remain pure .NET tests in `tests/CheatEngine.SDK.Abi.Tests`: their local opt-out does not require a C++ compiler or the facts file. From 5c289de2c16573f32b349eb08f19ae866ca2fc3d Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 20:46:15 +0200 Subject: [PATCH 8/9] Publish the tested package from tag releases Release the nuget-package artifact built and tested on the tag, behind the nuget environment approval, with CHANGELOG release notes, a nuget.org duplicate guard and attestation after the push. --- .github/workflows/release.yml | 136 +++++++++++++++++++++++----------- RELEASING.md | 51 ++++++++++--- 2 files changed, 134 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd159261..4c0a2acd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,9 @@ name: Release run-name: Release ${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && ' (dry run)' || '' }} +# verify ─► ci (build and test the tag, pack the tested build) ─► publish (manual approval) ─► github-release +# The nuget-package artifact built and tested by ci is the exact file pushed to nuget.org and attached to the release. + on: push: tags: @@ -15,6 +18,10 @@ concurrency: permissions: contents: read +defaults: + run: + shell: pwsh + jobs: verify: name: Verify tag @@ -34,7 +41,6 @@ jobs: # Only a commit on the first-parent line of main, the merged head, may be released. - name: Verify tag id: tag - shell: pwsh run: | $ErrorActionPreference = 'Stop' @@ -65,51 +71,81 @@ jobs: exit 1 } + # A full re-run after a successful publish would rebuild a different file for an immutable version. Re-run + # only the failed jobs of the original run: they reuse its artifacts. + try { + $index = Invoke-RestMethod -Uri 'https://api.nuget.org/v3-flatcontainer/cheatengine.sdk/index.json' -MaximumRetryCount 3 -RetryIntervalSec 5 + } + catch { + Write-Host "::error::Could not read the published CheatEngine.SDK versions from nuget.org: $($_.Exception.Message)" + exit 1 + } + if (@($index.versions) -contains $version.ToLowerInvariant()) { + Write-Host "::error::CheatEngine.SDK $version is already on nuget.org. Re-run only the failed jobs of the original run, or tag a new version." + exit 1 + } + $prerelease = if ($version.Contains('-')) { 'true' } else { 'false' } "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 "prerelease=$prerelease" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - ci: - name: CI - needs: verify - uses: ./.github/workflows/ci.yml - with: - upload-package: true - test-release: true - - package: - name: Check package - needs: [ verify, ci ] - runs-on: windows-latest - timeout-minutes: 5 - steps: - - name: Download package - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: nuget-package - path: artifacts/nuget - - - name: Check contents - shell: pwsh + # The GitHub release notes are the CHANGELOG section of the version. A prerelease may ship before its entries + # leave [Unreleased]; a final release may not. + - name: Extract release notes + if: steps.tag.outputs.version != '' env: - VERSION: ${{ needs.verify.outputs.version }} + VERSION: ${{ steps.tag.outputs.version }} + PRERELEASE: ${{ steps.tag.outputs.prerelease }} run: | $ErrorActionPreference = 'Stop' - $names = @(Get-ChildItem -Path artifacts/nuget -File | ForEach-Object Name) - $expected = if ($env:VERSION) { "CheatEngine.SDK.${env:VERSION}.nupkg" } else { 'CheatEngine.SDK.*.nupkg' } - if ($names.Count -ne 1 -or $names[0] -notlike $expected) { - Write-Host "::error::The package artifact must contain exactly $expected, but it contains: $($names -join ', ')." + $changelog = Get-Content -LiteralPath CHANGELOG.md -Raw + $sections = @($env:VERSION) + if ($env:PRERELEASE -eq 'true') { $sections += 'Unreleased' } + $notes = '' + foreach ($section in $sections) { + $pattern = '(?ms)^## \[' + [regex]::Escape($section) + '\][^\r\n]*\r?\n(?.*?)(?=^## \[|^\[[^\]]+\]:|\z)' + $match = [regex]::Match($changelog, $pattern) + if ($match.Success -and $match.Groups['body'].Value.Trim()) { + $notes = $match.Groups['body'].Value.Trim() + break + } + } + if (-not $notes) { + Write-Host "::error file=CHANGELOG.md::CHANGELOG.md has no non-empty section for $($sections -join ' or '). Move the release entries under '## [$env:VERSION] - ' before tagging." exit 1 } + New-Item -ItemType Directory -Path artifacts -Force | Out-Null + @($notes, '', "NuGet package: https://www.nuget.org/packages/CheatEngine.SDK/$env:VERSION") -join "`n" | + Set-Content -LiteralPath artifacts/release-notes.md -Encoding utf8NoBOM + + - name: Upload release notes + if: steps.tag.outputs.version != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-notes + path: artifacts/release-notes.md + if-no-files-found: error + retention-days: 90 + + # Build and test on the tag commit: MinVer stamps the tag version at compile time, so a main build cannot be + # promoted. Sonar already analysed this commit on main. + ci: + name: CI + needs: verify + uses: ./.github/workflows/ci.yml + with: + package-version: ${{ needs.verify.outputs.version }} + package-retention-days: 90 + # NuGet trusted publishing is bound to this file and the nuget environment, so login and push stay in this job. publish: name: Publish to NuGet - needs: [ verify, package ] + needs: [ verify, ci ] if: github.event_name == 'push' && github.ref_type == 'tag' && github.repository == 'CheatEngineNet/CheatEngine.SDK' runs-on: windows-latest timeout-minutes: 15 environment: - name: nuget + name: nuget # required reviewers approve the publication; the environment only admits v*.*.* tags url: https://www.nuget.org/packages/CheatEngine.SDK/${{ needs.verify.outputs.version }} permissions: contents: read @@ -118,7 +154,7 @@ jobs: env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true - VERSION: ${{ needs.verify.outputs.version }} + PACKAGE: artifacts/nuget/CheatEngine.SDK.${{ needs.verify.outputs.version }}.nupkg steps: - name: Download package uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -126,11 +162,6 @@ jobs: name: nuget-package path: artifacts/nuget - - name: Attest provenance - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 - with: - subject-path: artifacts/nuget/CheatEngine.SDK.${{ env.VERSION }}.nupkg - # The user is the nuget.org profile name that registered the trusted publishing policy, not an email address. - name: NuGet login id: login @@ -140,10 +171,19 @@ jobs: # A re-run after a partial failure must not fail on a version that is already published. - name: Push package - shell: pwsh env: NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} - run: dotnet nuget push "artifacts/nuget/CheatEngine.SDK.${env:VERSION}.nupkg" --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + run: | + dotnet nuget push $env:PACKAGE --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + if ($LASTEXITCODE -ne 0) { + throw "NuGet push failed with exit code $LASTEXITCODE." + } + + # After the push, so a failed push never leaves an attestation for a package that was not published. + - name: Attest provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: ${{ env.PACKAGE }} github-release: name: Create GitHub release @@ -165,17 +205,27 @@ jobs: name: nuget-package path: artifacts/nuget + - name: Download release notes + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-notes + path: artifacts + - name: Create release - shell: pwsh run: | - $name = "CheatEngine.SDK.${env:VERSION}.nupkg" + $name = "CheatEngine.SDK.$env:VERSION.nupkg" + $package = "artifacts/nuget/$name" $attached = gh release view $env:TAG --json assets --jq '.assets[].name' 2>$null if ($LASTEXITCODE -ne 0) { - $options = @('--verify-tag', '--generate-notes', '--notes', "NuGet package: https://www.nuget.org/packages/CheatEngine.SDK/$env:VERSION") + $options = @('--verify-tag', '--title', $env:TAG, '--notes-file', 'artifacts/release-notes.md') if ($env:PRERELEASE -eq 'true') { $options += '--prerelease' } - gh release create $env:TAG "artifacts/nuget/$name" @options + gh release create $env:TAG $package @options } elseif ($attached -notcontains $name) { - gh release upload $env:TAG "artifacts/nuget/$name" + gh release upload $env:TAG $package } else { Write-Host "::notice::Release $env:TAG already carries $name." + exit 0 + } + if ($LASTEXITCODE -ne 0) { + throw "Creating the GitHub release failed with exit code $LASTEXITCODE." } diff --git a/RELEASING.md b/RELEASING.md index b189e699..978a97fe 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,8 +1,14 @@ # Releasing CheatEngine.SDK -NuGet releases are produced by `.github/workflows/release.yml` from version tags. The workflow verifies, builds, tests, -packs, attests, and publishes the package before creating the matching GitHub release. Do not upload a locally built -package manually: nuget.org versions are immutable, and the workflow artifact is the release artifact. +NuGet releases are produced by `.github/workflows/release.yml` from version tags: + +```text +verify ─► ci (build and test the tag, pack the tested build) ─► publish (manual approval) ─► github-release +``` + +The `nuget-package` artifact that the `ci` job builds and tests is the exact file pushed to nuget.org, attested, and +attached to the GitHub release. Do not upload a locally built package manually: nuget.org versions are immutable, and +the workflow artifact is the release artifact. ## One-time trusted publishing setup @@ -23,12 +29,16 @@ The GitHub `nuget` environment must contain an environment secret named `NUGET_U nuget.org username of the administrator who created the policy, currently `AriusII`, not the organization name and not an email address. Organization membership alone does not make another username valid for that policy; if a different administrator recreates it, update `NUGET_USER` to that policy creator. Restrict the environment to deployment tags -matching `v*.*.*`. The workflow exchanges GitHub's OIDC token for a one-use, short-lived NuGet API key through -`NuGet/login`; it must not store a long-lived NuGet API key. +matching `v*.*.*` and add the maintainers who approve publications as required reviewers. The workflow exchanges +GitHub's OIDC token for a one-use, short-lived NuGet API key through `NuGet/login`; it must not store a long-lived NuGet +API key. Because the policy names `release.yml` and `nuget`, the login and push steps must stay in the `publish` job of +that file. ## Prepare a release -1. Move the completed entries from `Unreleased` to a versioned section in `CHANGELOG.md` and use the release date. +1. Move the completed entries from `Unreleased` to a `## [X.Y.Z] - YYYY-MM-DD` section in `CHANGELOG.md` and add its + link reference. The body of that section becomes the GitHub release notes: a stable tag fails without it. A + prerelease tag uses its own `## [X.Y.Z-rc.N]` section when present, otherwise the `[Unreleased]` section. 2. Update version-specific examples and analyzer release tracking when the public baseline changes. 3. Set `MinVerMinimumMajorMinor` in `Directory.Build.props` to the release line. The exact version still comes from the `v..` tag. @@ -38,12 +48,17 @@ matching `v*.*.*`. The workflow exchanges GitHub's OIDC token for a one-use, sho dotnet restore CheatEngine.SDK.slnx dotnet build CheatEngine.SDK.slnx -c Debug --no-restore dotnet test --solution CheatEngine.SDK.slnx -c Debug --fail-skips on - dotnet test --solution CheatEngine.SDK.slnx -c Release + dotnet test --solution CheatEngine.SDK.slnx -c Release --fail-skips on dotnet pack src/CheatEngine.SDK -c Release -o artifacts/nuget -p:MinVerVersionOverride=1.0.0 --no-restore ``` Replace `1.0.0` only in the local pack command when rehearsing another release. Inspect the resulting `.nupkg` as a ZIP archive and confirm its ID, version, README, license, assemblies, analyzers, build assets, and native bridge. +5. Merge the release pull request (squash) once `CI / Gate` passes. + +To rehearse the pipeline without publishing, start `Release` manually from a branch (**Actions → Release → Run +workflow**, or `gh workflow run release.yml --ref `). The dry run executes `verify` and the full `ci` job, then +skips `publish` and `github-release`. ## Publish @@ -55,9 +70,25 @@ git tag -a v1.0.0 -m "Release 1.0.0" git push origin v1.0.0 ``` -The tag starts the `Release` workflow. Confirm that all jobs pass, then verify both the -[NuGet package](https://www.nuget.org/packages/CheatEngine.SDK) and the generated GitHub release. NuGet validation and -search indexing can take several minutes. +The tag starts the `Release` workflow: + +1. `verify` checks the SemVer tag, that it points to `main`, that the version is not already on nuget.org, and extracts + the release notes from `CHANGELOG.md`. +2. `ci` builds and tests the tag in Debug and Release and packs `CheatEngine.SDK..nupkg` from the tested + Release build. The Release leg fails if the file name does not match the tag. +3. `publish` waits for a required reviewer to approve the `nuget` deployment in the run page, then pushes the package + and attests its provenance. +4. `github-release` creates the GitHub release from the extracted notes, attaches the package, and marks prereleases. + +Verify both the [NuGet package](https://www.nuget.org/packages/CheatEngine.SDK) and the GitHub release afterwards. +NuGet validation and search indexing can take several minutes. After a successful release, raise `MinVerMinimumMajorMinor` to the next development line and commit that change on `main`. For example, after `v1.0.0`, use `1.1` so subsequent untagged builds become `1.1.0-alpha.0.N`. + +## Re-running a release + +Use **Re-run failed jobs** only. Completed jobs are not repeated and the re-run reuses the artifacts of the original +attempt, so the pushed package, its attestation, and the release asset stay the same file. A push of an existing +version is skipped as a duplicate, and an existing GitHub release only receives a missing asset. **Re-run all jobs** +after a successful publish stops in `verify`, because the version is already on nuget.org. From e40c99f52048352fc8adb2f2e37236278c33f772 Mon Sep 17 00:00:00 2001 From: AriusII Date: Tue, 22 Sep 2026 20:46:15 +0200 Subject: [PATCH 9/9] Make CodeRabbit assertive and document the new CI Use the assertive profile with advisory pre-merge checks, simplify the pull request template, and describe the CI, squash merges and releases in CONTRIBUTING. --- .coderabbit.yaml | 99 +++++++++++++++++++++++++++----- .github/PULL_REQUEST_TEMPLATE.md | 45 +++++++-------- CONTRIBUTING.md | 37 +++++++++--- 3 files changed, 133 insertions(+), 48 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index f238dc0e..9c37eff4 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -4,8 +4,9 @@ language: en-US early_access: false reviews: - # Keep automated review useful for real defects without turning the App into a second formatter or approval process. - profile: chill + # Assertive and advisory: CodeRabbit reviews thoroughly but never approves, requests changes or sets a required + # status. The only merge requirement is the CI / Gate check. + profile: assertive request_changes_workflow: false review_details: false review_progress: true @@ -13,7 +14,8 @@ reviews: fail_commit_status: false high_level_summary: true high_level_summary_instructions: >- - Summarize only material SDK contract, native ABI, generator, test, packaging, or CI/CD effects. Keep it concise. + Summarize only material SDK contract, native ABI, Lua stack, generator, analyzer, test, packaging, or CI/CD + effects. Keep it concise. collapse_walkthrough: true changed_files_summary: false sequence_diagrams: false @@ -31,16 +33,67 @@ reviews: abort_on_close: true slop_detection: enabled: false - # This repository uses the GitHub App for review, not a CodeRabbit coding agent or autonomous follow-up changes. pre_merge_checks: + # Every check is a warning: none of them can block a merge. title: - mode: 'off' + mode: warning + requirements: >- + Pull requests are squash-merged and the title becomes the commit subject: a short imperative sentence such as + "Fix CI validation findings", at most 72 characters, no trailing period. description: - mode: 'off' + mode: warning issue_assessment: - mode: 'off' + mode: 'off' # issues are disabled on this repository docstrings: - mode: 'off' + mode: 'off' # CS1591 already fails the build for undocumented public APIs + custom_checks: + - name: Native ABI evidence + mode: warning + instructions: >- + Applies only when the pull request changes files under libs/CheatEngine.SDK.Abi/, native/ or + tests/native-abi-fixture/, or changes a StructLayout, FieldOffset, function-pointer signature, + UnmanagedCallersOnly or LibraryImport declaration anywhere; otherwise pass. Pass when the description or the + changed documentation names the upstream Cheat Engine source (file and symbol, pinned to a commit or tested + release), the Cheat Engine version, the x64 architecture and the calling convention, and the change adds or + updates size, offset or export tests in tests/CheatEngine.SDK.Abi.Tests or tests/native-abi-fixture. Fail + when a layout, export or signature changes without that evidence, or when fixture evidence is presented as + live Cheat Engine host qualification. + - name: Lua stack balance tests + mode: warning + instructions: >- + Applies only when the pull request changes code that pushes, pops, calls or references values on a Lua stack + in libs/CheatEngine.SDK.Lua/, libs/CheatEngine.SDK.Lua.Interop/, + source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/ or native/cheatengine-sdk-lua-bridge/; + otherwise pass. Pass when tests assert that the stack top is restored on success and on every touched failure + path (conversion failure, callback exception, protected-call error) and that registry references and + callbacks are released. Fail when such code changes without those assertions, or when a raw Lua call that + can raise is added outside a protected boundary. + - name: Public API documentation and changelog + mode: warning + instructions: >- + Applies when public or protected API in libs/, src/, analyzers/ or source-generators/ is added, removed or + changes signature or behavior, or when an analyzer diagnostic identifier or message changes; otherwise pass. + Pass when every new public member has XML documentation, the owning project's sibling README.md is updated + where it documents the contract, CHANGELOG.md has a matching entry under [Unreleased] (removals and behavior + changes marked as breaking), and diagnostic changes also update analyzers/docs and analyzer release tracking. + Fail otherwise. + - name: Dependency direction + mode: warning + instructions: >- + Fail when any project, source file or package reference references CheatEngine.Client, CheatEngine.Mcp or + their namespaces or packages, when Client-level policy (fluent APIs, dependency-injection registration, + application workflows) is added to this SDK, or when a shipping project under src/ or libs/ gains a new + PackageReference without a reason stated in the description. Otherwise pass. + - name: Workflow hygiene + mode: warning + instructions: >- + Applies only to changes under .github/; otherwise pass. Fail when an action is referenced by tag or branch + instead of a full 40-character commit SHA with a version comment; pull_request_target is used; + actions/checkout omits persist-credentials: false; a permission is widened without a comment giving the + reason; a PowerShell step runs a native command (dotnet, xmake, git, gh, tar, actionlint) without checking + $LASTEXITCODE; SONAR_TOKEN or NUGET_USER can reach fork or Dependabot runs; a job added to ci.yml is missing + from the Gate's needs; or NuGet/login moves out of the release.yml publish job that uses environment nuget. + Otherwise pass. finishing_touches: docstrings: enabled: false @@ -57,6 +110,7 @@ reviews: auto_incremental_review: true drafts: false base_branches: [ main ] + ignore_usernames: [ 'dependabot[bot]' ] # Exclude only generated outputs and non-reviewable binary payloads. Source, specifications, tests, CI and docs stay in scope. path_filters: - '!artifacts/**' @@ -102,19 +156,30 @@ reviews: and binary compatibility. Fluent developer workflows and application policy belong in CheatEngine.Client. - path: 'tests/**' instructions: >- - Tests use xUnit v3 with Microsoft.Testing.Platform. Debug CI rejects skipped tests. Distinguish fixture, - package and NativeAOT probes from actual live Cheat Engine host qualification. + Tests use xUnit v3 with Microsoft.Testing.Platform. CI runs the whole solution once in Debug and once in + Release with --fail-skips on, so a skipped test fails both. Distinguish fixture, package and NativeAOT probes + from actual live Cheat Engine host qualification. + - path: 'CHANGELOG.md' + instructions: >- + Keep a Changelog 1.1.0. The release workflow publishes the body of the "## [X.Y.Z]" section (or [Unreleased] + for a prerelease) as the GitHub release notes, so keep version headings exact and link references at the end. - path: '.github/**' instructions: >- - Preserve the discover/native/build/test/pack/AOT/gate DAG and its bridge, coverage and NuGet artifact flows. - Require least-privilege permissions and pinned action SHAs. actionlint already runs in pull-request CI; do not - ask for a duplicate CodeRabbit actionlint run. Never use pull_request_target to check out or execute code from - forks. Sonar secrets must remain unavailable to forks. + pull-request-ci.yml, main-ci.yml and release.yml are thin callers of the reusable ci.yml (native, build-test + matrix Debug/Release, aot, sonar through sonar.yml, lint, gate) and must stay thin. Jobs exchange artifacts + instead of redoing work: lua-protection-bridge and classic-abi-fixture-facts from native; nuget-package, + coverage and test-results- from build-test; release-notes from the release verify job. Do not + reintroduce per-project test matrices, a second test run of the same configuration, or rebuilds of what an + upstream job produced. NativeBridgePeAuditTests asserts the native job text. The Gate evaluates toJSON(needs) + and only sonar may be skipped. NuGet/login stays in release.yml with environment nuget (trusted publishing + binding). Require SHA-pinned actions, least privilege, persist-credentials: false and $LASTEXITCODE checks. + actionlint already runs in CI; do not ask for a duplicate CodeRabbit actionlint run. Never use + pull_request_target. Sonar secrets must stay unavailable to forks and Dependabot. tools: # CodeRabbit is used as the GitHub App; pipeline failures are surfaced through its GitHub Checks integration. github-checks: enabled: true - # pull-request-ci.yml is the deterministic actionlint owner. + # The ci.yml lint job is the deterministic actionlint owner. actionlint: enabled: false @@ -122,6 +187,10 @@ chat: auto_reply: true knowledge_base: + code_guidelines: + enabled: true + filePatterns: + - CONTRIBUTING.md automatic_linking_mode: disabled linked_repositories: - repository: CheatEngineNet/CheatEngine.Client diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ca5b83e3..d7cffe44 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,34 +1,29 @@ -## Outcome and linked issue +## Summary -Closes + -## Scope and architectural ownership +## Scope and ownership -Describe resulting behavior, affected contracts, and exclusions. SDK owns CE integration; Client owns workflows and -policy. + -## Dependencies and containing artifacts +## Validation -Link upstream prerequisites without closing them. Identify the SDK package containing every consumed primitive. +| Check | Evidence (command, run link or artifact) | Result | +|------------------------|--------------------------------------------------------------|--------------| +| CI / Gate | | Pending | +| Local build and tests | | Not executed | +| Packed package | | Not executed | +| Live Cheat Engine host | | Not executed | -## Validation actually performed +## Compatibility and release impact -| Check | Command / profile | Actual result | Evidence | -|-----------------|-------------------|---------------|----------| -| Unit / fixture | | Not executed | | -| Packed consumer | | Not executed | | -| Live host | | Not executed | | -| AOT publication | | Not executed | | + -## Compatibility, lifetime and partial effects +## Checklist -Explain public API or behavior changes, ownership, target switches, cleanup, cancellation and migration. - -## Documentation and review checklist - -- [ ] Scope is focused; existing repository style and contribution rules are preserved. -- [ ] Relevant regression evidence is attached; pending gates remain explicit. -- [ ] Ownership, provenance, and raw-state exposure match the supported consumer boundary of the affected layer. -- [ ] Capability and artifact claims match actual results. -- [ ] Documentation and release impact are recorded. -- [ ] No automatic merge, release, protection change or unsupported capability activation is requested. +- [ ] The change is focused and follows CONTRIBUTING.md. +- [ ] Tests cover the change; no skipped test hides a failure. +- [ ] Consumer-visible changes are recorded under `[Unreleased]` in CHANGELOG.md. +- [ ] Affected READMEs and documentation are updated in this pull request. +- [ ] The claims above match the actual CI and local results; live-host limitations are stated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 311c0495..ae4836dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Run these commands from the repository root: dotnet restore CheatEngine.SDK.slnx dotnet build CheatEngine.SDK.slnx -c Debug --no-restore dotnet test --solution CheatEngine.SDK.slnx -c Debug --fail-skips on -dotnet test --solution CheatEngine.SDK.slnx -c Release +dotnet test --solution CheatEngine.SDK.slnx -c Release --fail-skips on ``` To create the package locally: @@ -30,9 +30,27 @@ To create the package locally: dotnet pack src/CheatEngine.SDK -c Release -o artifacts/nuget ``` -The CI workflow builds Debug and Release, and tests each `tests/**/*.Tests.csproj` project. Debug tests treat skipped -tests as failures; Release permits the repository's existing Debug-only guard skip. For manual host validation, use -the [live-plugin guide](tests/CheatEngine.SDK.LivePlugin/README.md). +For manual host validation, use the [live-plugin guide](tests/CheatEngine.SDK.LivePlugin/README.md). + +## Continuous integration + +Pull requests run `Pull request CI`, pushes to `main` run `Main CI`, and version tags run `Release`. All three call the +reusable [`ci.yml`](.github/workflows/ci.yml), which builds each thing once and passes it on as an artifact: + +1. `native` rebuilds the Lua bridge twice to prove it is reproducible, checks the checked-in DLL against its source, + and builds the classic ABI fixture facts. +2. `build-test` builds the solution once per configuration (Debug and Release) and runs every + `tests/**/*.Tests` project in a single `dotnet test --solution` run. A skipped test fails both configurations. + Debug also collects coverage and compares the ABI fixture facts with the managed layouts; Release packs the tested + build as the `nuget-package` artifact, which you can download from the run. +3. `aot` publishes and runs the Native AOT probes. +4. `sonar` analyzes the code with SonarQube Cloud from the Debug coverage. It runs for branches of this repository only; + fork and Dependabot pull requests skip it. The quality gate fails pull requests and is only reported on `main`. +5. `lint` runs actionlint on the workflows. + +`CI / Gate` is the only required check: it fails when any job fails, and only `sonar` may be skipped. Drafts do not +run CI until they are marked ready for review. CodeRabbit reviews every pull request, but its findings and pre-merge +checks are advisory. ## Style and analyzers @@ -55,9 +73,11 @@ rules and their fixes. 2. Make the smallest change that solves the problem. 3. Run the relevant build, test, and package commands. 4. Open a pull request against `main`; do not push directly to the protected branch. +5. Record consumer-visible changes under `[Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md). -PR descriptions should state the problem, resulting behavior, related issues, validation commands and results, and any -remaining live-host limitations. Include documentation changes that the work requires. +Fill in the pull request template: state the problem, resulting behavior, validation commands and results, and any +remaining live-host limitations. Include documentation changes that the work requires. Pull requests are +squash-merged once `CI / Gate` passes, so the pull request title becomes the commit subject on `main`. ## Commits @@ -68,5 +88,6 @@ are not required. Do not add `Co-authored-by` trailers. Versions are derived by MinVer from the nearest `v*` tag; the current minimum major/minor line is `1.0`, as configured in [`Directory.Build.props`](Directory.Build.props). Pushing a valid `v..` tag (an optional SemVer -prerelease is allowed) starts the release workflow. After verification, CI, and package checks, that workflow publishes -`CheatEngine.SDK` to NuGet and creates a GitHub release. +prerelease is allowed) starts the release workflow. It builds and tests the tag, waits for manual approval on the +`nuget` environment, publishes the tested package to NuGet, and creates a GitHub release whose notes are the +`CHANGELOG.md` section of that version. See [`RELEASING.md`](RELEASING.md).