# Gitea Actions CI gate for the self-hosted runners. # # Deliberately does NOT use actions/setup-dotnet: data.forgejo.org (the mirror # Gitea resolves actions from) does not host that action at all, and the # self-hosted runners carry the pinned SDK band from global.json already. # actions/checkout IS mirrored, so it is used normally. # # The suite runs through tools/run-release-gate.ps1 rather than a bare # `dotnet test`: that script owns the xUnit trait-lane filter which excludes # the InstalledDat / Live / Manual / OS-specific lanes. A bare `dotnet test` # fails ~36 tests by design because those lanes assert their own preconditions. name: CI on: push: branches: [main] # Docs-only pushes change nothing a test can fail on, and each gate run is # ~7 minutes of clean build + 14k tests + a 121 MB release. Skip them; a # code push (or manual dispatch) still runs everything from scratch — # deliberately uncached, so the gate keeps proving a from-nothing build. paths-ignore: - 'docs/**' - 'claude-memory/**' - 'memory/**' - '**.md' workflow_dispatch: jobs: windows-gate: runs-on: windows-latest timeout-minutes: 45 steps: - uses: actions/checkout@v6 - name: Verify the pinned SDK band resolves shell: pwsh run: | dotnet --version dotnet --list-sdks # NOT tools/run-release-gate.ps1 here. That script redirects every child # process to its own log file, so the step emits nothing for minutes at a # time; Forgejo treats a task that stops reporting as a zombie and fails # it while the work is still running (observed: job marked failed with 20 # dotnet processes still alive and a complete 8.7 MB TRX on disk). Running # the projects directly keeps output streaming. The script stays the # canonical LOCAL gate; the trait filter below is copied from its default. - name: Build shell: pwsh run: dotnet build AcDream.slnx -c Release --nologo - name: Test (lane-filtered, streaming) shell: pwsh run: | $ErrorActionPreference = 'Stop' $filter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' $failed = @() foreach ($proj in Get-ChildItem tests -Directory | Sort-Object Name) { $csproj = Join-Path $proj.FullName "$($proj.Name).csproj" if (-not (Test-Path $csproj)) { continue } Write-Host "::group::$($proj.Name)" dotnet test $csproj -c Release --no-build --nologo --filter $filter if ($LASTEXITCODE -ne 0) { $failed += $proj.Name } Write-Host "::endgroup::" } if ($failed.Count) { throw "Failed test projects: $($failed -join ', ')" } linux-portable: runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@v6 - name: Portable closure (Linux lanes run here, not on Windows) run: | set -e dotnet --version # Core.Net runs SINGLE-THREADED here, on its own, and the split is # measured rather than defensive: on this 6-core container the # assembly FAILS in 40 s with default parallelism and PASSES in 10 s # with one thread. Its sessions do real socket work on background # threads, so contention both breaks and slows them. Windows has 18 # cores, passes in ~7 s parallel, and REGRESSED when serialized, so # this stays scoped to Linux. echo '::group::AcDream.Core.Net.Tests (single-threaded)' dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj \ -c Release --nologo \ --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' \ -- xUnit.MaxParallelThreads=1 echo '::endgroup::' for p in \ tests/AcDream.Platform.Tests \ tests/AcDream.Core.Tests \ tests/AcDream.Content.Tests \ tests/AcDream.Runtime.Tests \ tests/AcDream.Headless.Tests \ tests/AcDream.Launcher.Core.Tests \ tests/AcDream.UI.Abstractions.Tests ; do echo "::group::$p" dotnet test "$p" -c Release --nologo \ --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' echo "::endgroup::" done release: # Same workflow rather than a workflow_run trigger: workflow_run is a # GitHub feature whose Forgejo support is unreliable, while `needs` is # guaranteed. A red gate therefore cannot publish. needs: [windows-gate, linux-portable] runs-on: windows-latest timeout-minutes: 60 steps: - uses: actions/checkout@v6 - name: Compute release version id: ver shell: pwsh run: | $v = '0.1.0-build.{0}' -f ([DateTime]::UtcNow.ToString('yyyyMMddHHmm')) "version=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 Write-Host "release version: $v" - name: Build payloads with release-attachment URLs shell: pwsh env: TAG: ${{ steps.ver.outputs.version }} run: | ./tools/publish-bin.ps1 -Version $env:TAG -BaseUrl "${{ github.server_url }}/${{ github.repository }}/releases/download/$env:TAG" - name: Create the release and upload payloads shell: pwsh env: TAG: ${{ steps.ver.outputs.version }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | $ErrorActionPreference = 'Stop' $api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" $headers = @{ Authorization = "token $env:TOKEN" } $body = @{ tag_name = $env:TAG name = "acdream alpha $env:TAG" body = "Automated alpha build from ${{ github.sha }}." draft = $false prerelease = $true target_commitish = 'main' } | ConvertTo-Json $release = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers -ContentType 'application/json' -Body $body Write-Host "created release id=$($release.id)" foreach ($f in Get-ChildItem bin -File) { Write-Host ("uploading {0} ({1:N1} MB)" -f $f.Name, ($f.Length/1MB)) Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases/$($release.id)/assets?name=$($f.Name)" -Form @{ attachment = Get-Item $f.FullName } | Out-Null } - name: Republish the `latest` pointer release shell: pwsh env: TAG: ${{ steps.ver.outputs.version }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | $ErrorActionPreference = 'Stop' $api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" $headers = @{ Authorization = "token $env:TOKEN" } # Forgejo has no /releases/latest/download/ route, so the launcher # needs a pointer at a URL that never changes. A one-asset release on # the fixed `latest` tag is that pointer. Keeping it in a release # rather than in git means no payload branch, no bot commits on main, # and no push that would retrigger this workflow. $existing = Invoke-RestMethod -Method Get -Headers $headers ` -Uri "$api/releases/tags/latest" -SkipHttpErrorCheck if ($existing.id) { Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($existing.id)" | Out-Null # The tag outlives its release and would block recreation. Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/latest" -SkipHttpErrorCheck | Out-Null Write-Host "removed the previous latest pointer" } $body = @{ tag_name = 'latest' name = "Update feed -> $env:TAG" body = "**Download ``launcher-win-x64.zip``**, unzip it, and run ``acdream-launcher.exe``. It installs the game and keeps itself and the client up to date.`n`nThis is build ``$env:TAG``." draft = $false prerelease = $false target_commitish = 'main' } | ConvertTo-Json $pointer = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers ` -ContentType 'application/json' -Body $body # Upload the payloads here too, not just the manifest. `latest` is the # top of the Releases page and the first thing a person sees; a # pointer-only release gives them nothing to click and makes them hunt # for a build tagged with a timestamp. The launcher only needs # manifest.json, but a friend needs launcher-win-x64.zip. foreach ($f in Get-ChildItem bin -File) { Invoke-RestMethod -Method Post -Headers $headers ` -Uri "$api/releases/$($pointer.id)/assets?name=$($f.Name)" ` -Form @{ attachment = Get-Item $f.FullName } | Out-Null } Write-Host "latest now carries $env:TAG and its downloads" - name: Prune old releases shell: pwsh env: KEEP: '5' TOKEN: ${{ secrets.GITEA_TOKEN }} run: | $ErrorActionPreference = 'Stop' $api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" $headers = @{ Authorization = "token $env:TOKEN" } $keep = [int]$env:KEEP # Each build is ~121 MB of attachments, so without this the server # grows by that much on EVERY push to main. Keep the newest $keep # versioned releases: enough to grab a previous build or bisect a # regression, bounded at well under a gigabyte. $releases = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases?limit=100" # Never touch the `latest` pointer — it is the launcher's feed, not a build. $builds = @($releases | Where-Object { $_.tag_name -ne 'latest' } | Sort-Object -Property created_at -Descending) Write-Host "$($builds.Count) versioned release(s); keeping $keep" foreach ($old in ($builds | Select-Object -Skip $keep)) { Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($old.id)" | Out-Null # The tag survives its release and would otherwise accumulate. Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/$($old.tag_name)" -SkipHttpErrorCheck | Out-Null Write-Host " pruned $($old.tag_name)" }