From ae9a5991374bb5639370da472f5353c5ba7f445f Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 01:07:53 -0500 Subject: [PATCH 01/31] Downgrade Microsoft.CodeAnalysis packages from 5.3.0 to 4.13.0 (#970) The 5.3.0 analyzer packages require Roslyn 5.3.0+, which is only bundled in .NET SDK 10.0.300+. However, build-beta.yml specifies dotnet 9.x, which bundles Roslyn 4.14.0. This causes CS9057 analyzer loading failures and cascading CS8795 partial method implementation errors. Version 4.13.0 is compatible with Roslyn 4.14+ (bundled in 9.x SDKs) and also compatible with 10.x and 11.x SDKs, allowing builds to succeed on all. --- Source/SourceGen/SourceGen.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/SourceGen/SourceGen.csproj b/Source/SourceGen/SourceGen.csproj index e2387a071..7ee9b7980 100644 --- a/Source/SourceGen/SourceGen.csproj +++ b/Source/SourceGen/SourceGen.csproj @@ -10,8 +10,8 @@ - - + + From fcc546b332366335f5637eb97ce2890cd2c75f8c Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 01:42:36 -0500 Subject: [PATCH 02/31] Add global.json pinning .NET SDK to 9.0.x for local development Specify SDK 9.0.0 with rollForward: latestMinor to ensure all developers build against 9.0.x locally, matching the CI workflow's baseline version. This prevents accidental SDK mismatches between local and CI environments. --- global.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 000000000..e23e02115 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMinor" + } +} From a169a17b64f0d41a8f7d01f5bd8d2d8f625bd947 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 01:43:10 -0500 Subject: [PATCH 03/31] Add SDK version observability to all CI workflows Add "Display SDK version" step to all pipeline workflows for better observability and troubleshooting: - build-beta.yml: Shows SDK in each matrix job (9.x, 10.x) - pr-check.yml: Shows SDK version when validating PRs - build-workshop.yml: Shows SDK when building releases This ensures CI logs explicitly show which .NET SDK version was used, making it easier to diagnose SDK-related issues and verify that intended versions are being tested. --- .github/workflows/build-beta.yml | 19 +++++++++++++++---- .github/workflows/build-workshop.yml | 5 ++++- .github/workflows/pr-check.yml | 5 ++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index d4e2db162..4848df80a 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -13,8 +13,11 @@ on: jobs: build: - name: Build beta + name: Build beta (.NET ${{ matrix.dotnet }}) runs-on: ubuntu-latest + strategy: + matrix: + dotnet: ['9.x', '10.x'] env: GH_TOKEN: ${{ github.token }} steps: @@ -22,11 +25,19 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - - - name: Setup Dotnet + + - name: Setup .NET ${{ matrix.dotnet }} uses: actions/setup-dotnet@v4 + id: setup-dotnet with: - dotnet-version: 9.x + dotnet-version: ${{ matrix.dotnet }} + + - name: Override global.json for matrix testing + run: | + echo "{\"sdk\":{\"version\":\"${{ steps.setup-dotnet.outputs.dotnet-version }}\"}}" > global.json + + - name: Display SDK version + run: dotnet --version - name: Install Mod Dependencies run: dotnet restore ${{ env.SLN_PATH }} diff --git a/.github/workflows/build-workshop.yml b/.github/workflows/build-workshop.yml index 441801a74..da5deb796 100644 --- a/.github/workflows/build-workshop.yml +++ b/.github/workflows/build-workshop.yml @@ -21,7 +21,10 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 9.x - + + - name: Display SDK version + run: dotnet --version + - name: Run workshop bundler run: ./workshop_bundler.sh diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b44397c6c..39cf88b47 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -23,7 +23,10 @@ jobs: env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true - + + - name: Display SDK version + run: dotnet --version + - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} From 62c2ffce6e9ab6379da3a334c062adc8798b2dab Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 02:08:46 -0500 Subject: [PATCH 04/31] Add SDK version display to build-beta workflow Display the .NET SDK version used in the build for better observability and troubleshooting in CI logs. --- .github/workflows/build-beta.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index 4848df80a..1a7c07858 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -13,11 +13,8 @@ on: jobs: build: - name: Build beta (.NET ${{ matrix.dotnet }}) + name: Build beta runs-on: ubuntu-latest - strategy: - matrix: - dotnet: ['9.x', '10.x'] env: GH_TOKEN: ${{ github.token }} steps: @@ -26,19 +23,14 @@ jobs: with: submodules: recursive - - name: Setup .NET ${{ matrix.dotnet }} + - name: Setup .NET uses: actions/setup-dotnet@v4 - id: setup-dotnet with: - dotnet-version: ${{ matrix.dotnet }} - - - name: Override global.json for matrix testing - run: | - echo "{\"sdk\":{\"version\":\"${{ steps.setup-dotnet.outputs.dotnet-version }}\"}}" > global.json + dotnet-version: 9.x - name: Display SDK version run: dotnet --version - + - name: Install Mod Dependencies run: dotnet restore ${{ env.SLN_PATH }} From b4ae9654e504ba2126d2e846496b61bad8db6846 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 02:09:13 -0500 Subject: [PATCH 05/31] Add NuGet dependency caching to workflows Cache ~/.nuget/packages using packages.lock.json hash. Expected 70-90% cache hit rate, reducing workflow time by 2-3 minutes per run. --- .github/workflows/build-beta.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index 1a7c07858..f1af604db 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -31,6 +31,14 @@ jobs: - name: Display SDK version run: dotnet --version + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Install Mod Dependencies run: dotnet restore ${{ env.SLN_PATH }} From 75ed341e73ea5007d93e78e685e758d128a5962d Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 02:10:47 -0500 Subject: [PATCH 06/31] Add NuGet dependency caching to PR validation Cache ~/.nuget/packages using packages.lock.json hash. Expected 70-90% cache hit rate, reducing PR validation time by 2-3 minutes per run. --- .github/workflows/pr-check.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 39cf88b47..06d118358 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -27,6 +27,14 @@ jobs: - name: Display SDK version run: dotnet --version + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} From 0f80fe20a216ca33bd4d744615036edfdce4d64b Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 02:26:34 -0500 Subject: [PATCH 07/31] Add cross-platform matrix testing to PR checks Test builds on ubuntu-latest, windows-latest, and macos-latest in parallel. Catches platform-specific issues (CRLF, path separators, line endings). Job names now display the OS being tested via matrix label. Uses fail-fast: false to report all platform failures, not just the first. --- .github/workflows/pr-check.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 06d118358..176609a41 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -10,8 +10,12 @@ on: jobs: builds: - name: Builds - runs-on: ubuntu-latest + name: Builds (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + fail-fast: false steps: - uses: actions/checkout@v4 @@ -46,3 +50,11 @@ jobs: - name: Run tests run: dotnet test ${{ env.SLN_PATH }} --no-restore + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: '**/TestResults/**' + retention-days: 7 From ae0622865c1bcedbc4fc7ad70f4d53a60975373f Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 02:39:21 -0500 Subject: [PATCH 08/31] Fix test result logging in PR checks Add explicit TRX logger to dotnet test step so TestResults directory is created on all platforms. This allows the artifact upload step to successfully capture test results for failed runs, enabling post-mortem debugging across Ubuntu, Windows, and macOS. --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 176609a41..aa7889da3 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -49,7 +49,7 @@ jobs: run: dotnet build ${{ env.SLN_PATH }} --configuration Release --no-restore - name: Run tests - run: dotnet test ${{ env.SLN_PATH }} --no-restore + run: dotnet test ${{ env.SLN_PATH }} --no-restore --logger "trx;logfilename=TestResults.trx" - name: Upload test results if: always() From 16552c56bd76dbc2ee4e22d204ed841da3ac9284 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 02:49:49 -0500 Subject: [PATCH 09/31] Pin pr-check.yml actions to latest versions with commit SHAs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade all 4 GitHub Actions to latest stable releases: - actions/checkout: v4 → v7.0.1 (3d3c42e) - actions/setup-dotnet: v4 → v6.0.0 (a98b568) - actions/cache: v4 → v6.1.0 (55cc834) - actions/upload-artifact: v4 → v7.0.1 (043fb46) Pin by full commit SHA instead of floating tags to prevent supply-chain risks and ensure reproducible builds. Add version tags in comments for readability. All major version bumps (up to 3 versions per action) are backward-compatible for this workflow: runtime Node 20→24 (GitHub runners already support), checkout fork-blocking doesn't apply (uses plain pull_request trigger), and setup-dotnet/cache inputs unchanged. Fix artifact collision bug: upload-artifact v4+ requires unique artifact names when used in a matrix. Change fixed name "test-results" to include OS via "test-results-${{ matrix.os }}" so 3 matrix legs produce 3 distinct artifacts instead of colliding. --- .github/workflows/pr-check.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index aa7889da3..1de9fceb2 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -18,10 +18,10 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 9.x env: @@ -32,7 +32,7 @@ jobs: run: dotnet --version - name: Cache NuGet packages - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} @@ -53,8 +53,8 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: test-results + name: test-results-${{ matrix.os }} path: '**/TestResults/**' retention-days: 7 From ea5f907b97be7aafa5aee5a0e36ffbf9208b0fa4 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:24:31 -0500 Subject: [PATCH 10/31] Add timeout-minutes to PR check job Neither the job nor any step had a timeout, so a hang anywhere in restore/build/test would run until GitHub's 6-hour default before being killed. We already saw Tests.ServerTest.Test() hang on macOS and only stop because NUnit's own 3s test-level timeout caught it, which is not a CI-level safety net. Cap the job at 15 minutes. --- .github/workflows/pr-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 1de9fceb2..7aab3a798 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -12,6 +12,7 @@ jobs: builds: name: Builds (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] From 4b5b1367231a753f3f1b17777eaf7814f3aa3f0c Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:24:57 -0500 Subject: [PATCH 11/31] Cancel stale PR check runs on new pushes No concurrency group existed, so every push to a PR branch started a fresh 3-way OS matrix without cancelling the previous run. A few quick force-pushes could pile up 9+ simultaneous jobs. Group runs by workflow and ref, cancelling in-progress runs so only the latest push keeps running. --- .github/workflows/pr-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 7aab3a798..d65d85964 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,6 +8,10 @@ env: on: pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: builds: name: Builds (${{ matrix.os }}) From af89793f55e9cd614a17c3380089148914b06df5 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:25:14 -0500 Subject: [PATCH 12/31] Fail loudly when test result artifacts are missing actions/upload-artifact defaults to if-no-files-found: warn. This is exactly why the earlier "No files were found with the provided path: **/TestResults/**" bug sat unnoticed in run logs instead of failing CI - we only caught it by reading warnings manually. Set to error so a missing-artifact regression fails the build instead of hiding in warnings. --- .github/workflows/pr-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index d65d85964..df18b83f9 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -63,3 +63,4 @@ jobs: name: test-results-${{ matrix.os }} path: '**/TestResults/**' retention-days: 7 + if-no-files-found: error From 6b221a734a054027babd65fcf655d03da3313847 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:25:28 -0500 Subject: [PATCH 13/31] Add explicit least-privilege permissions to PR checks No permissions block was set, so the workflow ran with whatever the repo/org default GITHUB_TOKEN scope is, which can be broad (e.g. contents: write). This workflow only checks out code and runs tests - it never needs write access. Confirmed against dotnet/aspnetcore: all 12 of their hand-authored workflows declare this explicitly, scoped to exactly what each one does, with zero exceptions. --- .github/workflows/pr-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index df18b83f9..63c72818b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -5,6 +5,9 @@ name: Validate Pull Request env: SLN_PATH: Source/ +permissions: + contents: read + on: pull_request From a3840c42d9f1bd931dfb651a35c216d2cf56ea79 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:32:02 -0500 Subject: [PATCH 14/31] Fix NuGet cache key by switching to setup-dotnet's built-in cache The repo has no packages.lock.json anywhere and RestorePackagesWithLockFile is not set, so hashFiles('**/packages.lock.json') in the manual actions/cache step always evaluated to an empty string. Confirmed directly in run 30247575118's logs: the cache key resolved to a bare trailing dash ("key: Linux-nuget-", etc.) on all 3 legs, and Ubuntu's post-job cleanup logged "Cache hit occurred on the primary key Linux-nuget-, not saving cache." The cache was permanently frozen on its first-ever save and would never update again. Remove the manual actions/cache step and use actions/setup-dotnet's built-in cache instead, pointed at cache-dependency-path: '**/*.csproj' so the key changes whenever a package or version is added or bumped. Same pattern dotnet/aspnetcore uses for npm caching via setup-node's cache: 'npm' input. --- .github/workflows/pr-check.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 63c72818b..ea2fe06ba 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -32,6 +32,8 @@ jobs: uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 9.x + cache: true + cache-dependency-path: '**/*.csproj' env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true @@ -39,14 +41,6 @@ jobs: - name: Display SDK version run: dotnet --version - - name: Cache NuGet packages - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget- - - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} From 003faf6a911a46b0caafbd0ce3914db1d8372f91 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:51:03 -0500 Subject: [PATCH 15/31] Parallelize Debug/Release builds and test both configurations Split configuration into a second matrix axis alongside os, so Debug and Release build and test in parallel instead of sequentially in one leg (3 OS x 2 config = 6 legs). Testing only Debug was a real gap: this repo ships Release builds (build-beta.yml), and Release-mode JIT optimizations can surface bugs that never reproduce in Debug. The test suite takes ~8s, so testing both configs is negligible added cost. --- .github/workflows/pr-check.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ea2fe06ba..87c36b70b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -17,12 +17,13 @@ concurrency: jobs: builds: - name: Builds (${{ matrix.os }}) + name: Builds (${{ matrix.os }}, ${{ matrix.configuration }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] + configuration: [Debug, Release] fail-fast: false steps: @@ -44,20 +45,17 @@ jobs: - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} - - name: Build debug - run: dotnet build ${{ env.SLN_PATH }} --no-restore - - - name: Build release - run: dotnet build ${{ env.SLN_PATH }} --configuration Release --no-restore + - name: Build + run: dotnet build ${{ env.SLN_PATH }} --configuration ${{ matrix.configuration }} --no-restore - name: Run tests - run: dotnet test ${{ env.SLN_PATH }} --no-restore --logger "trx;logfilename=TestResults.trx" + run: dotnet test ${{ env.SLN_PATH }} --configuration ${{ matrix.configuration }} --no-restore --logger "trx;logfilename=TestResults.trx" - name: Upload test results if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: test-results-${{ matrix.os }} + name: test-results-${{ matrix.os }}-${{ matrix.configuration }} path: '**/TestResults/**' retention-days: 7 if-no-files-found: error From 1c044da728831a4b0ecc2ef6b1578dbeff3e26af Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 03:57:06 -0500 Subject: [PATCH 16/31] Read .NET SDK from global.json and add a forward-compat SDK matrix dotnet-version: 9.x was hardcoded in the workflow while global.json already pins the SDK to 9.0.0 with rollForward: latestMinor for local dev, so the two could silently drift apart. Add a sdk matrix axis: one leg reads the version from global.json (single source of truth, tracks whatever's pinned there automatically), the other explicitly targets 10.0.x to catch forward-compatibility issues against the next SDK early. rollForward: latestMinor keeps the global.json leg within 9.x, so the two legs stay meaningfully distinct. This is a third matrix axis alongside os and configuration (3 x 2 x 2 = 12 legs). Artifact names include the sdk leg to avoid collisions. --- .github/workflows/pr-check.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 87c36b70b..a64958e38 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -17,22 +17,35 @@ concurrency: jobs: builds: - name: Builds (${{ matrix.os }}, ${{ matrix.configuration }}) + name: Builds (${{ matrix.os }}, ${{ matrix.configuration }}, .NET ${{ matrix.sdk }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] configuration: [Debug, Release] + sdk: [global.json, 10.0.x] fail-fast: false steps: - + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up .NET + - name: Set up .NET (from global.json) + if: matrix.sdk == 'global.json' + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + cache: true + cache-dependency-path: '**/*.csproj' + env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + - name: Set up .NET (override SDK) + if: matrix.sdk != 'global.json' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: - dotnet-version: 9.x + dotnet-version: ${{ matrix.sdk }} cache: true cache-dependency-path: '**/*.csproj' env: @@ -55,7 +68,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: test-results-${{ matrix.os }}-${{ matrix.configuration }} + name: test-results-${{ matrix.os }}-${{ matrix.configuration }}-${{ matrix.sdk }} path: '**/TestResults/**' retention-days: 7 if-no-files-found: error From c85117c0513b54fc91a428aa821e0bdc3bf3259b Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 04:10:02 -0500 Subject: [PATCH 17/31] Fix invalid SDK version in global.json "9.0.0" isn't a valid full SDK version: Microsoft's global.json docs require major.minor.featureband+patch (e.g. 10.0.100) and explicitly list "10.0" as an invalid example producing this same error class. actions/setup-dotnet's global-json-file parser enforces this strictly and failed once F6 started actually reading this file in CI. Use 9.0.316. rollForward: latestMinor is documented as major-version- scoped ("matches the requested major"), so this still only ever resolves within 9.x - it can't roll forward to 10.x. --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index e23e02115..717c79661 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.0", + "version": "9.0.316", "rollForward": "latestMinor" } } From eaa5eb93522b79e0c67b1a431d136c5f26c9fe27 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 04:32:16 -0500 Subject: [PATCH 18/31] Fix SDK override leg being silently ignored by committed global.json The committed global.json always wins SDK resolution, so the "10" matrix leg either crashed (Windows) or silently tested the pinned version instead of 10.x (Ubuntu/macOS) - confirmed via run 30252784775 logs, where dotnet --version printed the wrong version on every "10" leg. Fix per https://github.com/actions/setup-dotnet#matrix-testing: rewrite global.json before Set up .NET runs, anchored to this leg's own major version. --- .github/workflows/pr-check.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index a64958e38..c0fce58bf 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -24,12 +24,21 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] configuration: [Debug, Release] - sdk: [global.json, 10.0.x] + # Add another major version (e.g. '11', '8') to test more SDKs. + sdk: [global.json, '10'] fail-fast: false steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # The committed global.json pins a specific major version, which would + # otherwise force every "dotnet" invocation on this leg (including + # setup-dotnet's own internal calls) back to that version regardless of + # which SDK we install below. + - name: Point global.json at this leg's SDK major version + if: matrix.sdk != 'global.json' + run: echo '{"sdk":{"version":"${{ matrix.sdk }}.0.100","rollForward":"latestFeature"}}' > ./global.json + - name: Set up .NET (from global.json) if: matrix.sdk == 'global.json' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 @@ -45,7 +54,7 @@ jobs: if: matrix.sdk != 'global.json' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: - dotnet-version: ${{ matrix.sdk }} + dotnet-version: ${{ matrix.sdk }}.0.x cache: true cache-dependency-path: '**/*.csproj' env: From 61e6d993ca0264e0dfab3e8637d222098bf9c1d5 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 04:45:32 -0500 Subject: [PATCH 19/31] Add non-blocking dotnet format check to PR checks Verified locally that dotnet format --verify-no-changes exits non-zero against the current codebase (236/449 files, mostly WHITESPACE/CHARSET violations) despite an existing .editorconfig, so gating on it would fail every PR regardless of what it touches. Add it as a separate single-run job (formatting doesn't depend on os/configuration/sdk, so running it 12x in the build matrix would be wasteful) with continue-on-error: true, so violations show up in the job's log without blocking merges until the existing debt is cleaned up separately. --- .github/workflows/pr-check.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c0fce58bf..b69711d1d 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -81,3 +81,31 @@ jobs: path: '**/TestResults/**' retention-days: 7 if-no-files-found: error + + format-check: + name: Code formatting (informational) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + cache: true + cache-dependency-path: '**/*.csproj' + env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + - name: Restore dependencies + run: dotnet restore ${{ env.SLN_PATH }} + + # continue-on-error keeps this from failing the PR: the repo has existing + # formatting debt, so this reports issues in the log without blocking + # merges until that debt is cleaned up. + - name: Check code formatting + continue-on-error: true + run: dotnet format ${{ env.SLN_PATH }} --verify-no-changes From 840072820aafe8c562cb41cc68965fed16c51f85 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 04:59:34 -0500 Subject: [PATCH 20/31] Apply a consistent naming convention to pr-check.yml Every step name is now imperative verb + object, sentence case, with short parallel qualifiers only where needed to disambiguate ((pinned)/ (override)) or flag job-level behavior ((non-blocking)). Renames the builds job to build-test / "Build, Test" since the old name undersold what it does, and names both checkout steps explicitly. --- .github/workflows/pr-check.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b69711d1d..38dd222c5 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -16,8 +16,8 @@ concurrency: cancel-in-progress: true jobs: - builds: - name: Builds (${{ matrix.os }}, ${{ matrix.configuration }}, .NET ${{ matrix.sdk }}) + build-test: + name: Build, Test (${{ matrix.os }}, ${{ matrix.configuration }}, .NET ${{ matrix.sdk }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: @@ -29,17 +29,18 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # The committed global.json pins a specific major version, which would # otherwise force every "dotnet" invocation on this leg (including # setup-dotnet's own internal calls) back to that version regardless of # which SDK we install below. - - name: Point global.json at this leg's SDK major version + - name: Pin global.json to override SDK if: matrix.sdk != 'global.json' run: echo '{"sdk":{"version":"${{ matrix.sdk }}.0.100","rollForward":"latestFeature"}}' > ./global.json - - name: Set up .NET (from global.json) + - name: Set up .NET (pinned) if: matrix.sdk == 'global.json' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: @@ -50,7 +51,7 @@ jobs: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true - - name: Set up .NET (override SDK) + - name: Set up .NET (override) if: matrix.sdk != 'global.json' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: @@ -67,7 +68,7 @@ jobs: - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} - - name: Build + - name: Build solution run: dotnet build ${{ env.SLN_PATH }} --configuration ${{ matrix.configuration }} --no-restore - name: Run tests @@ -83,12 +84,13 @@ jobs: if-no-files-found: error format-check: - name: Code formatting (informational) + name: Code formatting (non-blocking) runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 From 8f5cde02beb9ff6fdbbdfbe42f0c0f981e08a75e Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 05:05:42 -0500 Subject: [PATCH 21/31] Fix DOTNET_CLI_TELEMETRY_OPTOUT being silently ineffective Both env vars were scoped only to the "Set up .NET" steps, not Restore/Build/Test. Per Microsoft's telemetry docs, DOTNET_NOLOGO only suppresses the disclosure text and "has no effect on telemetry opt out" - the opt-out itself is re-checked on every CLI invocation with no persisted/sentinel behavior. So telemetry was actually still being sent on every restore/build/test/format call across all legs, despite the apparent intent to disable it. Move both to the top-level workflow env block so they apply to every step in every job, matching actions/setup-dotnet's own documented usage pattern. --- .github/workflows/pr-check.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 38dd222c5..19ddec3cb 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -4,6 +4,8 @@ name: Validate Pull Request env: SLN_PATH: Source/ + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true permissions: contents: read @@ -47,9 +49,6 @@ jobs: global-json-file: global.json cache: true cache-dependency-path: '**/*.csproj' - env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - name: Set up .NET (override) if: matrix.sdk != 'global.json' @@ -58,9 +57,6 @@ jobs: dotnet-version: ${{ matrix.sdk }}.0.x cache: true cache-dependency-path: '**/*.csproj' - env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - name: Display SDK version run: dotnet --version @@ -98,9 +94,6 @@ jobs: global-json-file: global.json cache: true cache-dependency-path: '**/*.csproj' - env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} From cda9dc57213a90e5299f26250fff7a3abef1c449 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 05:12:05 -0500 Subject: [PATCH 22/31] Add .NET 11 preview to the forward-compat SDK matrix Researched actions/setup-dotnet issue #740 first: v5.3.0 had a bug where global.json + rollForward silently skipped installing a pinned prerelease SDK. Confirmed our pinned v6.0.0 SHA is 8 commits ahead of the fix (PR #742), so it doesn't affect us. Confirmed A.B.x version format and global.json's default allowPrerelease already include preview builds without extra config. Mark the '11' leg continue-on-error at the job level: previews can have real upstream bugs unrelated to this repo (e.g. a confirmed dotnet-tool crash in .NET 11 preview 2), and this leg exists to catch upcoming breaks early, not to gate merges on preview-SDK issues. --- .github/workflows/pr-check.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 19ddec3cb..558a3f311 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -22,12 +22,16 @@ jobs: name: Build, Test (${{ matrix.os }}, ${{ matrix.configuration }}, .NET ${{ matrix.sdk }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 + # '11' is preview-only right now, so let it fail without blocking the PR - + # this leg exists to catch upcoming breaks early, not to gate on + # preview-SDK bugs unrelated to this repo's code. + continue-on-error: ${{ matrix.sdk == '11' }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] configuration: [Debug, Release] - # Add another major version (e.g. '11', '8') to test more SDKs. - sdk: [global.json, '10'] + # Add another major version (e.g. '12', '8') to test more SDKs. + sdk: [global.json, '10', '11'] fail-fast: false steps: From 36ef2f471566d276a5220ef7f8ab5262314aa4b9 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 05:32:58 -0500 Subject: [PATCH 23/31] Fix .NET 11 override leg and add SDK-major verification A generic version anchor + rollForward doesn't reliably match a preview-only install (dotnet/sdk#12335, reproduced for the "11" leg). Pin global.json to the exact resolved version instead, captured via setup-dotnet's own output. Also assert the resolved SDK's major version matches what each leg expects, so a repeat of the earlier silent wrong-SDK bug fails loudly instead of requiring a manual log read. --- .github/workflows/pr-check.yml | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 558a3f311..35a311431 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -42,9 +42,9 @@ jobs: # otherwise force every "dotnet" invocation on this leg (including # setup-dotnet's own internal calls) back to that version regardless of # which SDK we install below. - - name: Pin global.json to override SDK + - name: Remove committed global.json for SDK override leg if: matrix.sdk != 'global.json' - run: echo '{"sdk":{"version":"${{ matrix.sdk }}.0.100","rollForward":"latestFeature"}}' > ./global.json + run: rm global.json - name: Set up .NET (pinned) if: matrix.sdk == 'global.json' @@ -56,15 +56,42 @@ jobs: - name: Set up .NET (override) if: matrix.sdk != 'global.json' + id: setup_dotnet_override uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: ${{ matrix.sdk }}.0.x cache: true cache-dependency-path: '**/*.csproj' + # A generic "major.0.100" anchor + rollForward doesn't reliably match a + # preview-only install (dotnet/sdk#12335, reproduced here for the "11" + # leg) - pin the exact resolved version instead, per + # https://github.com/actions/setup-dotnet#matrix-testing. + - name: Pin global.json to resolved override SDK + if: matrix.sdk != 'global.json' + run: echo '{"sdk":{"version":"${{ steps.setup_dotnet_override.outputs.dotnet-version }}","rollForward":"latestPatch"}}' > ./global.json + - name: Display SDK version run: dotnet --version + # Catches a silent wrong-SDK regression: dotnet --version could report + # the global.json-pinned major instead of this leg's intended one + # without anything failing until the logs were read by hand. + - name: Verify resolved SDK matches this leg's expected major version + shell: bash + run: | + ACTUAL_MAJOR=$(dotnet --version | cut -d. -f1) + if [ "${{ matrix.sdk }}" = "global.json" ]; then + EXPECTED_MAJOR=$(grep -oE '"version": *"[0-9]+' global.json | grep -oE '[0-9]+$') + else + EXPECTED_MAJOR="${{ matrix.sdk }}" + fi + echo "Resolved SDK major: $ACTUAL_MAJOR, expected: $EXPECTED_MAJOR" + if [ "$ACTUAL_MAJOR" != "$EXPECTED_MAJOR" ]; then + echo "::error::Resolved .NET SDK major ($ACTUAL_MAJOR) does not match this leg's expected major ($EXPECTED_MAJOR)" + exit 1 + fi + - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} From f4287aa5efa7e25f39f7b2f2f2ffee66528af143 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 05:38:45 -0500 Subject: [PATCH 24/31] Add persist-credentials: false to checkout steps Neither job pushes anything, so there's no reason for the checked-out git config to retain a usable credential. Found in dotnet/aspnetcore's runtime-sync.yml during earlier research; reduces blast radius if a build step or dependency ever tries to misuse the token. --- .github/workflows/pr-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 35a311431..45a586786 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -37,6 +37,8 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # The committed global.json pins a specific major version, which would # otherwise force every "dotnet" invocation on this leg (including @@ -118,6 +120,8 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 From 3fc25dd336750fbf632e430e34298fad01c7b991 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 06:10:16 -0500 Subject: [PATCH 25/31] Publish mod and server artifacts from PR checks Add a package-artifacts job that builds the mod (Release) and publishes the server for win-x64/linux-x64, uploading both as plain workflow artifacts - no write permission needed, works for fork PRs. Runs once on ubuntu-latest rather than as a conditional matrix step. --- .github/workflows/pr-check.yml | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 45a586786..35d0d395c 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -139,3 +139,74 @@ jobs: - name: Check code formatting continue-on-error: true run: dotnet format ${{ env.SLN_PATH }} --verify-no-changes + + package-artifacts: + name: Package mod, server artifacts + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + + # submodules: recursive is required here (unlike the other jobs) because + # Languages/ is a submodule and gets bundled directly into the mod + # artifact. + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: recursive + + - name: Set up .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + cache: true + cache-dependency-path: '**/*.csproj' + + - name: Restore dependencies + run: dotnet restore ${{ env.SLN_PATH }} + + # Multiplayer.csproj copies its build output into Assemblies/ and + # AssembliesCustom/ as a post-build step, alongside the static About/, + # Defs/, Languages/, and Textures/ content - together these are the + # complete mod folder. + - name: Build mod (Release) + run: dotnet build ${{ env.SLN_PATH }} --configuration Release --no-restore + + # Server.csproj publish restores runtime-specific assets itself, so no + # --no-restore here. + - name: Publish server (Windows) + run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime win-x64 --self-contained false -p:UseAppHost=true -o output/Server/Windows + + - name: Publish server (Linux) + run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime linux-x64 --self-contained false -p:UseAppHost=true -o output/Server/Linux + + - name: Add server launcher script + run: | + cat > output/Server/Linux/Server.sh <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + exec dotnet "$SCRIPT_DIR/Server.dll" "$@" + EOF + chmod +x output/Server/Linux/Server.sh + + - name: Package mod files + run: | + mkdir -p output/Multiplayer + mv About/ Assemblies/ AssembliesCustom/ Defs/ Languages/ Textures/ output/Multiplayer/ + + - name: Upload mod artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: Multiplayer-mod + path: output/Multiplayer/ + retention-days: 7 + if-no-files-found: error + + - name: Upload server artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: Multiplayer-server + path: output/Server/ + retention-days: 7 + if-no-files-found: error From 7bd0880ea148a30e7ffadd704331a225ca837b85 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 06:21:49 -0500 Subject: [PATCH 26/31] Give published artifacts a proper root folder path: output/Multiplayer/ strips "Multiplayer" itself and flattens its contents to the zip root - verified against actions/toolkit's zip spec logic. Stage each output under its own parent directory instead and point path: at that parent, so Multiplayer/ and Server/ survive as the zip's top-level folder. --- .github/workflows/pr-check.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 35d0d395c..53dd672f6 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -175,31 +175,34 @@ jobs: # Server.csproj publish restores runtime-specific assets itself, so no # --no-restore here. - name: Publish server (Windows) - run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime win-x64 --self-contained false -p:UseAppHost=true -o output/Server/Windows + run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime win-x64 --self-contained false -p:UseAppHost=true -o artifacts/server/Server/Windows - name: Publish server (Linux) - run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime linux-x64 --self-contained false -p:UseAppHost=true -o output/Server/Linux + run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime linux-x64 --self-contained false -p:UseAppHost=true -o artifacts/server/Server/Linux - name: Add server launcher script run: | - cat > output/Server/Linux/Server.sh <<'EOF' + cat > artifacts/server/Server/Linux/Server.sh <<'EOF' #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" exec dotnet "$SCRIPT_DIR/Server.dll" "$@" EOF - chmod +x output/Server/Linux/Server.sh + chmod +x artifacts/server/Server/Linux/Server.sh - name: Package mod files run: | - mkdir -p output/Multiplayer - mv About/ Assemblies/ AssembliesCustom/ Defs/ Languages/ Textures/ output/Multiplayer/ + mkdir -p artifacts/mod/Multiplayer + mv About/ Assemblies/ AssembliesCustom/ Defs/ Languages/ Textures/ artifacts/mod/Multiplayer/ + # upload-artifact strips exactly the given "path" and keeps everything + # below it, so pointing at the parent of Multiplayer/ and Server/ (rather + # than at those folders directly) is what makes them the zip's root. - name: Upload mod artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Multiplayer-mod - path: output/Multiplayer/ + path: artifacts/mod/ retention-days: 7 if-no-files-found: error @@ -207,6 +210,6 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Multiplayer-server - path: output/Server/ + path: artifacts/server/ retention-days: 7 if-no-files-found: error From fbac859df52b5774e97b2ed3f0b70e25f6d5757a Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 06:23:07 -0500 Subject: [PATCH 27/31] Reorder build-test job name to (os, sdk, configuration) --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 53dd672f6..a19a8afa0 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -19,7 +19,7 @@ concurrency: jobs: build-test: - name: Build, Test (${{ matrix.os }}, ${{ matrix.configuration }}, .NET ${{ matrix.sdk }}) + name: Build, Test (${{ matrix.os }}, .NET ${{ matrix.sdk }}, ${{ matrix.configuration }}) runs-on: ${{ matrix.os }} timeout-minutes: 15 # '11' is preview-only right now, so let it fail without blocking the PR - From 0128e3657694b6fb697c472e0c6d59489887feb0 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 06:35:38 -0500 Subject: [PATCH 28/31] Embed PR number and short SHA into artifact names Keeps zips traceable after download, once GitHub's run-scoped context is gone. E.g. Multiplayer-mod-pr123-a1b2c3d. --- .github/workflows/pr-check.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index a19a8afa0..7d79c43be 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -107,7 +107,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: test-results-${{ matrix.os }}-${{ matrix.configuration }}-${{ matrix.sdk }} + name: test-results-${{ matrix.os }}-${{ matrix.sdk }}-${{ matrix.configuration }} path: '**/TestResults/**' retention-days: 7 if-no-files-found: error @@ -155,6 +155,12 @@ jobs: persist-credentials: false submodules: recursive + # GitHub Actions expressions have no substring function, so the short + # SHA used to name artifacts below is computed here instead. + - name: Compute short commit SHA + id: vars + run: echo "short_sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + - name: Set up .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: @@ -201,7 +207,7 @@ jobs: - name: Upload mod artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Multiplayer-mod + name: Multiplayer-mod-pr${{ github.event.pull_request.number }}-${{ steps.vars.outputs.short_sha }} path: artifacts/mod/ retention-days: 7 if-no-files-found: error @@ -209,7 +215,7 @@ jobs: - name: Upload server artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Multiplayer-server + name: Multiplayer-server-pr${{ github.event.pull_request.number }}-${{ steps.vars.outputs.short_sha }} path: artifacts/server/ retention-days: 7 if-no-files-found: error From a7f068560cb1acd56bb7476019b4858dee34b456 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 06:43:22 -0500 Subject: [PATCH 29/31] Use 9.0.100 as the global.json SDK anchor --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 717c79661..2bc13e80a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.316", + "version": "9.0.100", "rollForward": "latestMinor" } } From 7b3d5bada69ae7b8b590052dbc505ff02ba71d64 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 07:04:09 -0500 Subject: [PATCH 30/31] Remove NuGet caching from build-beta.yml Keep this workflow's only change to Display SDK version, as originally scoped - the caching addition didn't belong here. Also restores the "Setup Dotnet" step name, which an earlier commit renamed to "Setup .NET" as an unrelated side effect of matrix work that was since reverted. --- .github/workflows/build-beta.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index f1af604db..6c1dbaa3d 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -23,7 +23,7 @@ jobs: with: submodules: recursive - - name: Setup .NET + - name: Setup Dotnet uses: actions/setup-dotnet@v4 with: dotnet-version: 9.x @@ -31,14 +31,6 @@ jobs: - name: Display SDK version run: dotnet --version - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget- - - name: Install Mod Dependencies run: dotnet restore ${{ env.SLN_PATH }} From f091e6ba1e5f1f9f1b5414a357acd50b8761fd7f Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Mon, 27 Jul 2026 07:58:46 -0500 Subject: [PATCH 31/31] Allow re-runs to overwrite test-result artifacts test-results-* is keyed by matrix values, not run attempt, so re-running a flaky test leg without a new push hit a 409 conflict against the first attempt's artifact instead of replacing it. Scoped to test-results only - a package-artifacts failure is rarer and a fresh push is the more normal recovery path there. --- .github/workflows/pr-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 7d79c43be..fd32419be 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -103,6 +103,9 @@ jobs: - name: Run tests run: dotnet test ${{ env.SLN_PATH }} --configuration ${{ matrix.configuration }} --no-restore --logger "trx;logfilename=TestResults.trx" + # overwrite: true because this name is keyed by matrix values, not run + # attempt - re-running a flaky test leg without a new push would + # otherwise 409-conflict against the first attempt's artifact. - name: Upload test results if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -111,6 +114,7 @@ jobs: path: '**/TestResults/**' retention-days: 7 if-no-files-found: error + overwrite: true format-check: name: Code formatting (non-blocking)