diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index d4e2db162..6c1dbaa3d 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -22,12 +22,15 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - + - name: Setup Dotnet uses: actions/setup-dotnet@v4 with: dotnet-version: 9.x - + + - 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..fd32419be 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -4,34 +4,222 @@ name: Validate Pull Request env: SLN_PATH: Source/ + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +permissions: + contents: read on: pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - builds: - name: Builds + build-test: + 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 - + # 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. '12', '8') to test more SDKs. + sdk: [global.json, '10', '11'] + fail-fast: false + steps: + + - 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 + # setup-dotnet's own internal calls) back to that version regardless of + # which SDK we install below. + - name: Remove committed global.json for SDK override leg + if: matrix.sdk != 'global.json' + run: rm global.json + + - name: Set up .NET (pinned) + 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' + + - 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 }} + + - name: Build solution + run: dotnet build ${{ env.SLN_PATH }} --configuration ${{ matrix.configuration }} --no-restore + + - 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 + with: + name: test-results-${{ matrix.os }}-${{ matrix.sdk }}-${{ matrix.configuration }} + path: '**/TestResults/**' + retention-days: 7 + if-no-files-found: error + overwrite: true + + format-check: + name: Code formatting (non-blocking) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + + - 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 + with: + global-json-file: global.json + cache: true + cache-dependency-path: '**/*.csproj' + + - 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 + + package-artifacts: + name: Package mod, server artifacts runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - - uses: actions/checkout@v4 + + # 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 + + # 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@v4 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: - dotnet-version: 9.x - env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - + global-json-file: global.json + cache: true + cache-dependency-path: '**/*.csproj' + - name: Restore dependencies run: dotnet restore ${{ env.SLN_PATH }} - - name: Build debug - run: dotnet build ${{ env.SLN_PATH }} --no-restore - - - name: Build release + # 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 - - name: Run tests - run: dotnet test ${{ env.SLN_PATH }} --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 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 artifacts/server/Server/Linux + + - name: Add server launcher script + run: | + 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 artifacts/server/Server/Linux/Server.sh + + - name: Package mod files + run: | + 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-pr${{ github.event.pull_request.number }}-${{ steps.vars.outputs.short_sha }} + path: artifacts/mod/ + retention-days: 7 + if-no-files-found: error + + - name: Upload server artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + 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 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 @@ - - + + diff --git a/global.json b/global.json new file mode 100644 index 000000000..2bc13e80a --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "9.0.100", + "rollForward": "latestMinor" + } +}