Refactor run-aspire scripts for improved cross-platform support. Replace run-aspire.cmd with run-aspire.ps1 for Windows and add run-aspire.sh for Ubuntu. Update documentation to reflect these changes and ensure synchronization between build rules across scripts. Enhance Tailscale integration and clarify usage instructions.
ci / server (push) Failing after 3m38s
ci / client (push) Successful in 18s

This commit is contained in:
Leonid Pershin
2026-08-20 10:48:19 +03:00
parent f45347b335
commit d3396b5b8f
8 changed files with 304 additions and 144 deletions
+10 -8
View File
@@ -53,11 +53,13 @@ npm --prefix src/HSchool.Client run build
dotnet run --project src/HSchool.AppHost
```
`run-aspire.cmd` is the Windows entry point: it skips MSBuild when AppHost and Server dlls are
newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on PATH
it also runs `tailscale serve --bg 5173` when `tailscale` is on PATH and connected; otherwise
it warns and continues locally only. Pass `--no-tailscale` to skip Serve. Keep the script in sync
with `tools/apphost-uptodate.ps1` if the AppHost path ever moves.
`run-aspire.ps1` is the Windows entry point (`run-aspire.cmd` just forwards to it);
`run-aspire.sh` is the same on Ubuntu. They skip MSBuild when AppHost and Server dlls are
newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on
PATH and connected they run `tailscale serve --bg 5173` and print the HTTPS client URL;
otherwise they warn and continue locally only. Pass `--no-tailscale` to skip Serve. Keep
`tools/apphost-uptodate.ps1`, `LaunchBuildStamp`, and `needs_build` in `run-aspire.sh` in
sync if the AppHost path ever moves.
`dotnet run --project src/HSchool.AppHost` still compiles every time — that is for a dirty tree,
not a fast relaunch. It starts the server *and* the Vite dev server and opens the Aspire
@@ -201,6 +203,6 @@ only resolved a status line. Solution-wide is CI.
keep going out. Opening sends one extra snapshot. A test that waits for *people* on pause
will hang; a test that waits for a lesson label will not.
- **`dotnet run` on the AppHost always evaluates MSBuild**, even when nothing changed.
`run-aspire.cmd` skips that with `--no-build` when AppHost and Server dlls are newer than
C# / csproj / props. `--rebuild` forces a build. Bare `dotnet run --project src/HSchool.AppHost`
still compiles.
`run-aspire.ps1` / `run-aspire.sh` skip that with `--no-build` when AppHost and Server dlls
are newer than C# / csproj / props. `--rebuild` forces a build. Bare
`dotnet run --project src/HSchool.AppHost` still compiles.
+10 -9
View File
@@ -30,10 +30,10 @@ school screen with a running game clock. The school itself is still empty.
dotnet run --project src/HSchool.AppHost
```
On Windows `run-aspire.cmd` does the same and can be double-clicked; it checks that the .NET SDK
and Node are on PATH first and passes any arguments through
(`run-aspire.cmd --launch-profile http`). A second launch with no C# changes skips MSBuild;
`run-aspire.cmd --rebuild` forces a build.
On Windows `run-aspire.ps1` does the same (`run-aspire.cmd` forwards to it and can be
double-clicked). On Ubuntu `./run-aspire.sh`. Both check that the .NET SDK and Node are on
PATH first and pass any arguments through (`--launch-profile http`). A second launch with no
C# changes skips MSBuild; `--rebuild` forces a build.
The Aspire dashboard opens with two resources: `server` (ASP.NET Core) and `client` (Vite dev
server on port **5173**).
@@ -43,11 +43,12 @@ server on port **5173**).
For a dev session over your tailnet, expose only the Vite client — not the game server (5180),
Aspire dashboard (15180), or SwarmUI.
`run-aspire.cmd` runs `tailscale serve --bg 5173` before the AppHost when `tailscale` is on
PATH and connected. If Tailscale is missing, offline, or `serve` fails, the script prints a
warning and continues with local access only (`http://localhost:5173`). Pass `--no-tailscale`
to skip Serve entirely. You can also run the command yourself once per boot if you start with
`dotnet run --project src/HSchool.AppHost` instead.
`run-aspire.ps1` / `run-aspire.sh` run `tailscale serve --bg 5173` before the AppHost when
`tailscale` is on PATH and connected. If Tailscale is missing, offline, or `serve` fails, the script prints a
warning and continues with local access only (`http://localhost:5173`). When Serve is up, it
prints the HTTPS client URL next to the local one; the Aspire dashboard stays on localhost.
Pass `--no-tailscale` to skip Serve entirely. You can also run the command yourself once per
boot if you start with `dotnet run --project src/HSchool.AppHost` instead.
Friends on the same tailnet open `https://<your-machine>.<tailnet>.ts.net`. The page, REST API
and game WebSocket all go through Vites proxy on one origin; Kestrel stays on localhost.
+1 -1
View File
@@ -110,7 +110,7 @@
Клиентский TypeScript и JSONC модов не входы: Vite и каталог читают диск (`ContentRoot`), а не
`bin`. Тестовые проекты AppHost не ссылается — их правки запуск не пересобирают.
Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `.cmd`) и в
Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `run-aspire.ps1`) и в
`LaunchBuildStamp` (то, что проверяют тесты). Менять надо оба.
### Что не входит
+4 -123
View File
@@ -1,127 +1,8 @@
@echo off
setlocal EnableDelayedExpansion
rem Starts the whole app: game server, Vite client and the Aspire dashboard.
rem Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props.
rem run-aspire.cmd --launch-profile http
rem run-aspire.cmd --rebuild
rem run-aspire.cmd --no-tailscale skip `tailscale serve --bg 5173`
rem Trampoline for Explorer double-click and muscle memory. The launcher is run-aspire.ps1.
cd /d "%~dp0"
where dotnet >nul 2>&1
if errorlevel 1 (
echo [run-aspire] dotnet SDK not found in PATH.
echo Install .NET 10: https://dotnet.microsoft.com/download
call :maybe_pause
exit /b 1
)
where node >nul 2>&1
if errorlevel 1 (
echo [run-aspire] Node.js not found in PATH - the client resource will fail to start.
echo Install Node 22.12 or newer: https://nodejs.org
echo.
)
set "CONFIG=Debug"
set "REBUILD=0"
set "TAILSCALE_SERVE=1"
set "ARGS="
:parse
if "%~1"=="" goto parsed
if /I "%~1"=="--rebuild" (
set "REBUILD=1"
shift
goto parse
)
if /I "%~1"=="--no-tailscale" (
set "TAILSCALE_SERVE=0"
shift
goto parse
)
if /I "%~1"=="-c" if not "%~2"=="" set "CONFIG=%~2"
if /I "%~1"=="--configuration" if not "%~2"=="" set "CONFIG=%~2"
set "ARGS=!ARGS! %1"
shift
goto parse
:parsed
if "%REBUILD%"=="1" (
echo [run-aspire] --rebuild: building.
goto build
)
where powershell >nul 2>&1
if errorlevel 1 (
echo [run-aspire] PowerShell not found, building.
goto build
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\apphost-uptodate.ps1" -RepoRoot "%CD%" -Configuration "%CONFIG%"
if errorlevel 1 (
echo [run-aspire] sources changed or binaries missing, building.
goto build
)
echo [run-aspire] binaries are up to date, skipping build.
goto run
:build
echo.
dotnet build "src\HSchool.AppHost\HSchool.AppHost.csproj" -c "%CONFIG%"
if errorlevel 1 (
echo.
echo [run-aspire] build failed.
call :maybe_pause
exit /b 1
)
echo.
:run
if "%TAILSCALE_SERVE%"=="1" (
where tailscale >nul 2>&1
if errorlevel 1 (
echo [run-aspire] WARNING: tailscale not found in PATH.
echo Friends cannot reach the client over the tailnet.
echo Continuing with local access only ^(http://localhost:5173^).
) else (
tailscale status >nul 2>&1
if errorlevel 1 (
echo [run-aspire] WARNING: tailscale is not connected or not running.
echo Friends cannot reach the client over the tailnet.
echo Continuing with local access only ^(http://localhost:5173^).
) else (
echo [run-aspire] tailscale serve --bg 5173
tailscale serve --bg 5173
if errorlevel 1 (
echo [run-aspire] WARNING: tailscale serve failed.
echo Friends cannot reach the client over the tailnet.
echo Continuing with local access only ^(http://localhost:5173^).
)
)
)
echo.
)
echo [run-aspire] Starting the Aspire AppHost. Press Ctrl+C to shut everything down.
echo.
dotnet run --project "src\HSchool.AppHost\HSchool.AppHost.csproj" --no-build --no-restore !ARGS!
set "EXITCODE=!ERRORLEVEL!"
if not "!EXITCODE!"=="0" (
echo.
echo [run-aspire] AppHost exited with code !EXITCODE!.
)
call :maybe_pause
exit /b !EXITCODE!
rem Keeps the window open when the file was double-clicked from Explorer.
:maybe_pause
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0run-aspire.ps1" %*
set "EXITCODE=%ERRORLEVEL%"
echo %cmdcmdline% | find /i "%~nx0" >nul
if not errorlevel 1 pause
exit /b 0
exit /b %EXITCODE%
+132
View File
@@ -0,0 +1,132 @@
# Starts the whole app: game server, Vite client and the Aspire dashboard.
# Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props.
# .\run-aspire.ps1
# .\run-aspire.ps1 --launch-profile http
# .\run-aspire.ps1 --rebuild
# .\run-aspire.ps1 --no-tailscale
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
Set-Location -LiteralPath $PSScriptRoot
function Write-Log([string] $Message) {
Write-Host "[run-aspire] $Message"
}
function Write-TailscaleSkipped([string] $Why) {
Write-Log "WARNING: $Why"
Write-Host ' Friends cannot reach the client over the tailnet.'
Write-Host ' Continuing with local access only (http://localhost:5173).'
}
$configuration = 'Debug'
$rebuild = $false
$tailscaleServe = $true
$dotnetArgs = @()
$expectConfiguration = $false
foreach ($arg in $args) {
if ($expectConfiguration) {
$configuration = $arg
$expectConfiguration = $false
continue
}
switch ($arg) {
'--rebuild' { $rebuild = $true }
'--no-tailscale' { $tailscaleServe = $false }
{ $_ -in '-c', '--configuration' } { $expectConfiguration = $true }
default { $dotnetArgs += $arg }
}
}
if ($expectConfiguration) {
Write-Log 'missing value for -c / --configuration.'
exit 1
}
if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) {
Write-Log 'dotnet SDK not found in PATH.'
Write-Host ' Install .NET 10: https://dotnet.microsoft.com/download'
exit 1
}
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-Log 'Node.js not found in PATH - the client resource will fail to start.'
Write-Host ' Install Node 22.12 or newer: https://nodejs.org'
Write-Host ''
}
if ($rebuild) {
Write-Log '--rebuild: building.'
$needBuild = $true
} else {
$stamp = Join-Path $PSScriptRoot 'tools\apphost-uptodate.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stamp -RepoRoot $PSScriptRoot -Configuration $configuration
if ($LASTEXITCODE -eq 0) {
Write-Log 'binaries are up to date, skipping build.'
$needBuild = $false
} else {
Write-Log 'sources changed or binaries missing, building.'
$needBuild = $true
}
}
if ($needBuild) {
Write-Host ''
& dotnet build (Join-Path $PSScriptRoot 'src\HSchool.AppHost\HSchool.AppHost.csproj') -c $configuration
if ($LASTEXITCODE -ne 0) {
Write-Host ''
Write-Log 'build failed.'
exit $LASTEXITCODE
}
Write-Host ''
}
$clientTailscaleUrl = $null
if ($tailscaleServe) {
if (-not (Get-Command tailscale -ErrorAction SilentlyContinue)) {
Write-TailscaleSkipped 'tailscale not found in PATH.'
} else {
& tailscale status 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-TailscaleSkipped 'tailscale is not connected or not running.'
} else {
Write-Log 'tailscale serve --bg 5173'
& tailscale serve --bg 5173
if ($LASTEXITCODE -ne 0) {
Write-TailscaleSkipped 'tailscale serve failed.'
} else {
$line = & tailscale serve status 2>$null | Select-Object -First 1
$token = if ($line) { ($line -split '\s+', 2)[0] } else { $null }
if ($token -like 'https://*') {
$clientTailscaleUrl = $token
}
}
}
}
Write-Host ''
}
Write-Log 'Starting the Aspire AppHost. Press Ctrl+C to shut everything down.'
Write-Log 'Client: http://localhost:5173'
if ($clientTailscaleUrl) {
Write-Log "Client (Tailscale): $clientTailscaleUrl"
}
Write-Host ''
$run = @(
'--project', (Join-Path $PSScriptRoot 'src\HSchool.AppHost\HSchool.AppHost.csproj')
'--no-build'
'--no-restore'
) + $dotnetArgs
& dotnet run @run
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host ''
Write-Log "AppHost exited with code $exitCode."
}
exit $exitCode
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Starts the whole app: game server, Vite client and the Aspire dashboard.
# Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props.
# ./run-aspire.sh
# ./run-aspire.sh --launch-profile http
# ./run-aspire.sh --rebuild
# ./run-aspire.sh --no-tailscale
set -euo pipefail
cd "$(dirname "$0")"
log() {
echo "[run-aspire] $*"
}
skip_tailscale() {
log "WARNING: $*"
echo " Friends cannot reach the client over the tailnet."
echo " Continuing with local access only (http://localhost:5173)."
}
# Same rules as tools/apphost-uptodate.ps1 and LaunchBuildStamp.cs.
needs_build() {
local tfm=net10.0
local apphost="src/HSchool.AppHost/bin/${configuration}/${tfm}/HSchool.AppHost.dll"
local server="src/HSchool.Server/bin/${configuration}/${tfm}/HSchool.Server.dll"
[[ -f "$apphost" && -f "$server" ]] || return 0
local oldest=$apphost
[[ "$server" -ot "$apphost" ]] && oldest=$server
local name
for name in global.json Directory.Build.props Directory.Packages.props; do
[[ -f "$name" && "$name" -nt "$oldest" ]] && return 0
done
[[ -d src ]] || return 0
local found
found=$(find src \( -name bin -o -name obj -o -name node_modules \) -prune -o \
-type f \( -name '*.cs' -o -name '*.csproj' -o -name '*.props' \) -newer "$oldest" -print -quit)
[[ -n "$found" ]]
}
configuration=Debug
rebuild=0
tailscale_serve=1
dotnet_args=()
expect_configuration=0
while [[ $# -gt 0 ]]; do
if [[ $expect_configuration -eq 1 ]]; then
configuration=$1
expect_configuration=0
shift
continue
fi
case $1 in
--rebuild) rebuild=1 ;;
--no-tailscale) tailscale_serve=0 ;;
-c|--configuration) expect_configuration=1 ;;
*) dotnet_args+=("$1") ;;
esac
shift
done
if [[ $expect_configuration -eq 1 ]]; then
log "missing value for -c / --configuration."
exit 1
fi
if ! command -v dotnet >/dev/null; then
log "dotnet SDK not found in PATH."
echo " Install .NET 10: https://dotnet.microsoft.com/download"
exit 1
fi
if ! command -v node >/dev/null; then
log "Node.js not found in PATH - the client resource will fail to start."
echo " Install Node 22.12 or newer: https://nodejs.org"
echo
fi
need_build=0
if [[ $rebuild -eq 1 ]]; then
log "--rebuild: building."
need_build=1
elif needs_build; then
log "sources changed or binaries missing, building."
need_build=1
else
log "binaries are up to date, skipping build."
fi
if [[ $need_build -eq 1 ]]; then
echo
if ! dotnet build src/HSchool.AppHost/HSchool.AppHost.csproj -c "$configuration"; then
echo
log "build failed."
exit 1
fi
echo
fi
client_tailscale_url=
if [[ $tailscale_serve -eq 1 ]]; then
if ! command -v tailscale >/dev/null; then
skip_tailscale "tailscale not found in PATH."
elif ! tailscale status >/dev/null 2>&1; then
skip_tailscale "tailscale is not connected or not running."
else
log "tailscale serve --bg 5173"
if ! tailscale serve --bg 5173; then
skip_tailscale "tailscale serve failed."
else
line=$(tailscale serve status 2>/dev/null | head -n 1 || true)
token=${line%% *}
case $token in
https://*) client_tailscale_url=$token ;;
esac
fi
fi
echo
fi
log "Starting the Aspire AppHost. Press Ctrl+C to shut everything down."
log "Client: http://localhost:5173"
if [[ -n "$client_tailscale_url" ]]; then
log "Client (Tailscale): $client_tailscale_url"
fi
echo
set +e
dotnet run --project src/HSchool.AppHost/HSchool.AppHost.csproj --no-build --no-restore "${dotnet_args[@]}"
exit_code=$?
set -e
if [[ $exit_code -ne 0 ]]; then
echo
log "AppHost exited with code ${exit_code}."
fi
exit "$exit_code"
@@ -1,7 +1,8 @@
namespace HSchool.AppHost.Tests;
/// <summary>
/// Same rules as <c>tools/apphost-uptodate.ps1</c>. Keep the two in sync.
/// Same rules as <c>tools/apphost-uptodate.ps1</c> and <c>needs_build</c> in
/// <c>run-aspire.sh</c>. Keep the three in sync.
/// </summary>
internal static class LaunchBuildStamp
{
+2 -2
View File
@@ -1,5 +1,5 @@
# Same rules as tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs. Keep the two in sync.
# Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build.
# Same rules as tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs and needs_build in run-aspire.sh.
# Keep the three in sync. Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build.
param(
[Parameter(Mandatory = $true)]
[string] $RepoRoot,