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
`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
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):
```powershell
# Publishes both win-x64 and linux-x64 by default.
./dotnet/publish-server.ps1
# Publish just one target when needed.
./dotnet/publish-server.ps1 -Runtime linux-x64
./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
@@ -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.
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
graphs remain reproducible. Windows x64 and Linux x64 self-contained outputs are checked.
The publish script produces Windows x64 and Linux x64 self-contained outputs in one command
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
data creation, sends SIGTERM and requires a clean exit:
+14 -4
View File
@@ -1,7 +1,17 @@
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$Runtime = 'win-x64',
[string]$Output = "$PSScriptRoot/artifacts/server/$Runtime"
[string[]]$Runtime = @('win-x64', 'linux-x64'),
[string]$Output
)
$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": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
"requested": "[10.0.12, )",
"resolved": "10.0.12",
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
},
"Google.Protobuf": {
"type": "Transitive",
@@ -10,9 +10,9 @@
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
"requested": "[10.0.12, )",
"resolved": "10.0.12",
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
},
"Google.Protobuf": {
"type": "Transitive",
@@ -16,9 +16,9 @@
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
"requested": "[10.0.12, )",
"resolved": "10.0.12",
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
}
},
"net10.0/linux-x64": {}
@@ -16,9 +16,9 @@
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
"requested": "[10.0.12, )",
"resolved": "10.0.12",
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
}
},
"net10.0/win-x64": {}
@@ -13,9 +13,9 @@
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
"requested": "[10.0.12, )",
"resolved": "10.0.12",
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
},
"SourceGear.sqlite3": {
"type": "Direct",
@@ -13,9 +13,9 @@
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
"requested": "[10.0.12, )",
"resolved": "10.0.12",
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
},
"SourceGear.sqlite3": {
"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.");
}
}