diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
new file mode 100644
index 00000000..5c203078
--- /dev/null
+++ b/.gitea/workflows/ci.yml
@@ -0,0 +1,140 @@
+# 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]
+ 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
+
+ - name: Complete Release gate
+ shell: pwsh
+ run: ./tools/run-release-gate.ps1
+
+ - name: Upload gate evidence
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-gate-windows
+ path: artifacts/release-gate/
+ if-no-files-found: warn
+ retention-days: 14
+
+ 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
+ for p in \
+ tests/AcDream.Platform.Tests \
+ tests/AcDream.Core.Tests \
+ tests/AcDream.Core.Net.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!=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: Point the launcher manifest at the new release
+ shell: pwsh
+ env:
+ TAG: ${{ steps.ver.outputs.version }}
+ TOKEN: ${{ secrets.GITEA_TOKEN }}
+ run: |
+ $ErrorActionPreference = 'Stop'
+ # dist carries ONLY manifest.json (~500 bytes), force-replaced each
+ # publish, never pushed to GitHub. Payloads stay in release
+ # attachments, outside git history entirely.
+ $url = "${{ github.server_url }}/${{ github.repository }}.git" -replace '^https://', "https://x:$env:TOKEN@"
+ git config --global user.email 'ci@acdream.local'
+ git config --global user.name 'acdream CI'
+ New-Item -ItemType Directory -Force dist-branch/bin | Out-Null
+ Copy-Item bin/manifest.json dist-branch/bin/manifest.json
+ Push-Location dist-branch
+ git init -q
+ git checkout -q -b dist
+ git add -f bin/manifest.json
+ git commit -q -m "release: manifest for $env:TAG"
+ git push -q --force $url dist:dist
+ Pop-Location
+ Write-Host "manifest published for $env:TAG"
diff --git a/tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj b/tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj
index 23aedde8..78b2ec1b 100644
--- a/tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj
+++ b/tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj
@@ -23,4 +23,14 @@
+
+
+
+
+
diff --git a/tests/AcDream.Launcher.Tests/LauncherProjectBoundaryTests.cs b/tests/AcDream.Launcher.Tests/LauncherProjectBoundaryTests.cs
index 111145e4..73713571 100644
--- a/tests/AcDream.Launcher.Tests/LauncherProjectBoundaryTests.cs
+++ b/tests/AcDream.Launcher.Tests/LauncherProjectBoundaryTests.cs
@@ -140,10 +140,13 @@ public sealed class LauncherProjectBoundaryTests
"workflows",
"headless-portability.yml"));
- Assert.Contains("src/AcDream.Launcher/**", workflow, StringComparison.Ordinal);
- Assert.Contains("tests/AcDream.Launcher.Tests/**", workflow, StringComparison.Ordinal);
- Assert.Contains("src/AcDream.Bake/**", workflow, StringComparison.Ordinal);
- Assert.Contains("tests/AcDream.Bake.Tests/**", workflow, StringComparison.Ordinal);
+ // The four `**` path filters this used to assert belonged to the
+ // push/pull_request triggers that `8be14d39 ci: make GitHub workflows
+ // manual only` deliberately removed; the workflow is workflow_dispatch
+ // now. What this test is actually for — that the portability workflow
+ // BUILDS, TESTS, PUBLISHES and EXECUTES the launcher and bake CLI — is
+ // covered by the content assertions below, which are unaffected by how
+ // the workflow is triggered.
Assert.Contains("portable-launcher:", workflow, StringComparison.Ordinal);
Assert.Contains("tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj", workflow, StringComparison.Ordinal);
Assert.Contains("tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj", workflow, StringComparison.Ordinal);
diff --git a/tests/AcDream.Launcher.Tests/xunit.runner.json b/tests/AcDream.Launcher.Tests/xunit.runner.json
new file mode 100644
index 00000000..c3155894
--- /dev/null
+++ b/tests/AcDream.Launcher.Tests/xunit.runner.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false,
+ "maxParallelThreads": 1
+}
diff --git a/tools/publish-bin.ps1 b/tools/publish-bin.ps1
index d1df1889..918889b4 100644
--- a/tools/publish-bin.ps1
+++ b/tools/publish-bin.ps1
@@ -34,6 +34,7 @@
[CmdletBinding()]
param(
[string]$Version,
+ [string]$BaseUrl,
[switch]$IncludeLinux,
[string]$MinimumLauncherVersion = '0.0.1'
)
@@ -61,9 +62,16 @@ foreach ($candidate in @($Version, $MinimumLauncherVersion)) {
}
}
-# Raw-file base for the PUBLIC Gitea repo's dist branch. Must agree with
-# ReleaseManifestClient.ProductionManifestUri.
-$RawBase = 'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin'
+# Where the manifest says the payloads live. The CI pipeline passes the Gitea
+# RELEASE asset base for the tag it is publishing, so payloads live outside git
+# entirely; the default keeps the older dist-branch layout working for a manual
+# local publish. The manifest itself always stays at the stable dist raw URL
+# that ReleaseManifestClient.ProductionManifestUri points at.
+$RawBase = if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
+ 'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin'
+} else {
+ $BaseUrl.TrimEnd('/')
+}
$BinRoot = Join-Path $RepoRoot 'bin'
$Staging = Join-Path $BinRoot 'payload'