acdream/.gitea/workflows/ci.yml
Erik a3b0455f59
All checks were successful
CI / linux-portable (push) Successful in 3m14s
CI / windows-gate (push) Successful in 4m52s
CI / release (push) Successful in 1m56s
ci: put the downloads in the 'latest' release, not just the pointer
'latest' held only manifest.json, which serves the launcher but not a person:
it sits at the top of the Releases page with nothing to click, so a friend has
to know to scroll past it to a build tagged with a timestamp. That is the exact
friction this feed exists to remove.

It now carries the client and launcher zips as well, and its body says plainly
which file to download and what to do with it. Storage cost is flat, not per
build: 'latest' is deleted and recreated each publish, so it is one extra copy
at any time rather than one per release.
2026-08-19 16:25:48 +02:00

227 lines
11 KiB
YAML

# 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)"
}