Publish Windows and Linux servers together
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-20 01:15:52 +02:00
parent b79cbeff44
commit 57a63cc1cd
10 changed files with 89 additions and 25 deletions
+2 -1
View File
@@ -75,7 +75,8 @@ bundle; its environment variables enable Developer ID signing and notarization.
## Server publishing ## Server publishing
`dotnet/publish-server.ps1` produces locked self-contained Windows and Linux artifacts. `dotnet/publish-server.ps1` produces locked self-contained Windows and Linux artifacts in one
command. Pass `-Runtime win-x64` or `-Runtime linux-x64` to publish only one target.
`Dockerfile`, Compose configuration, and systemd packaging use the managed server. Validate a `Dockerfile`, Compose configuration, and systemd packaging use the managed server. Validate a
published binary with its TLS `--health-check`, preferably including `--expect-fingerprint`. published binary with its TLS `--health-check`, preferably including `--expect-fingerprint`.
+5 -2
View File
@@ -6,7 +6,9 @@ The .NET server preserves protocol v2 and the native schema/credentials. Publish
self-contained executable (no installed .NET runtime required): self-contained executable (no installed .NET runtime required):
```powershell ```powershell
# Publishes both win-x64 and linux-x64 by default.
./dotnet/publish-server.ps1 ./dotnet/publish-server.ps1
# Publish just one target when needed.
./dotnet/publish-server.ps1 -Runtime linux-x64 ./dotnet/publish-server.ps1 -Runtime linux-x64
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe --help ./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe --help
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe account add Operator --admin --data-dir ./voicecat-data ./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe account add Operator --admin --data-dir ./voicecat-data
@@ -52,8 +54,9 @@ backoff grows from one to thirty seconds. Success clears backoff but does not re
tokens. State is bounded to 4096 keys; idle entries retire after ten minutes when full. tokens. State is bounded to 4096 keys; idle entries retire after ten minutes when full.
Throttle and credential failures share the generic auth error. Limits are process-local. Throttle and credential failures share the generic auth error. Limits are process-local.
The publish script uses separate per-RID lock files so deployment and development restore The publish script produces Windows x64 and Linux x64 self-contained outputs in one command
graphs remain reproducible. Windows x64 and Linux x64 self-contained outputs are checked. by default. It uses separate per-RID lock files so deployment and development restore graphs
remain reproducible. Pass `-Runtime` to publish only one target.
The Linux smoke starts the published ELF, validates TLS health and the pin, checks mode 0700 The Linux smoke starts the published ELF, validates TLS health and the pin, checks mode 0700
data creation, sends SIGTERM and requires a clean exit: data creation, sends SIGTERM and requires a clean exit:
+14 -4
View File
@@ -1,7 +1,17 @@
[CmdletBinding(SupportsShouldProcess)]
param( param(
[string]$Runtime = 'win-x64', [string[]]$Runtime = @('win-x64', 'linux-x64'),
[string]$Output = "$PSScriptRoot/artifacts/server/$Runtime" [string]$Output
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
dotnet publish "$PSScriptRoot/src/VoiceCat.Server/VoiceCat.Server.csproj" -c Release -r $Runtime --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false -p:RestoreLockedMode=true "-p:NuGetLockFilePath=packages.publish.$Runtime.lock.json" -o $Output
if ($LASTEXITCODE -ne 0) { throw 'Managed server publish failed.' } if ($Runtime.Count -eq 0) { throw 'At least one runtime must be specified.' }
if ($Output -and $Runtime.Count -ne 1) { throw '-Output can only be used when publishing one runtime.' }
foreach ($targetRuntime in $Runtime) {
$targetOutput = if ($Output) { $Output } else { "$PSScriptRoot/artifacts/server/$targetRuntime" }
if (-not $PSCmdlet.ShouldProcess($targetOutput, "Publish VoiceCat.Server for $targetRuntime")) { continue }
dotnet publish "$PSScriptRoot/src/VoiceCat.Server/VoiceCat.Server.csproj" -c Release -r $targetRuntime --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false -p:RestoreLockedMode=true "-p:NuGetLockFilePath=packages.publish.$targetRuntime.lock.json" -o $targetOutput
if ($LASTEXITCODE -ne 0) { throw "Managed server publish failed for $targetRuntime." }
}
@@ -10,9 +10,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.7, )", "requested": "[10.0.12, )",
"resolved": "10.0.7", "resolved": "10.0.12",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
}, },
"Google.Protobuf": { "Google.Protobuf": {
"type": "Transitive", "type": "Transitive",
@@ -10,9 +10,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.7, )", "requested": "[10.0.12, )",
"resolved": "10.0.7", "resolved": "10.0.12",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
}, },
"Google.Protobuf": { "Google.Protobuf": {
"type": "Transitive", "type": "Transitive",
@@ -16,9 +16,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.7, )", "requested": "[10.0.12, )",
"resolved": "10.0.7", "resolved": "10.0.12",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
} }
}, },
"net10.0/linux-x64": {} "net10.0/linux-x64": {}
@@ -16,9 +16,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.7, )", "requested": "[10.0.12, )",
"resolved": "10.0.7", "resolved": "10.0.12",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
} }
}, },
"net10.0/win-x64": {} "net10.0/win-x64": {}
@@ -13,9 +13,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.7, )", "requested": "[10.0.12, )",
"resolved": "10.0.7", "resolved": "10.0.12",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
}, },
"SourceGear.sqlite3": { "SourceGear.sqlite3": {
"type": "Direct", "type": "Direct",
@@ -13,9 +13,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.7, )", "requested": "[10.0.12, )",
"resolved": "10.0.7", "resolved": "10.0.12",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
}, },
"SourceGear.sqlite3": { "SourceGear.sqlite3": {
"type": "Direct", "type": "Direct",
@@ -0,0 +1,50 @@
using System.Diagnostics;
namespace VoiceCat.Tests;
public class PublishServerScriptTests
{
[Fact]
public async Task DefaultPublishTargetsWindowsAndLinuxWhileRuntimeCanSelectOne()
{
string script = Path.Combine(FindRoot(), "dotnet", "publish-server.ps1");
string allTargets = await RunWhatIf(script);
Assert.Contains("win-x64", allTargets);
Assert.Contains("linux-x64", allTargets);
string linuxOnly = await RunWhatIf(script, "-Runtime", "linux-x64");
Assert.Contains("linux-x64", linuxOnly);
Assert.DoesNotContain("win-x64", linuxOnly);
}
private static async Task<string> RunWhatIf(string script, params string[] arguments)
{
string powerShell = OperatingSystem.IsWindows() ? "powershell.exe" : "pwsh";
var start = new ProcessStartInfo(powerShell)
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (string argument in new[] { "-NoProfile", "-NonInteractive", "-File", script, "-WhatIf" }.Concat(arguments))
start.ArgumentList.Add(argument);
using Process process = Process.Start(start) ?? throw new InvalidOperationException("Could not start PowerShell.");
Task<string> stdout = process.StandardOutput.ReadToEndAsync();
Task<string> stderr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
string output = await stdout + await stderr;
Assert.True(process.ExitCode == 0, output);
return output;
}
private static string FindRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "CMakePresets.json")))
directory = directory.Parent;
return directory?.FullName ?? throw new DirectoryNotFoundException("Repository root not found.");
}
}