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 dotnet run --project src/HSchool.AppHost
``` ```
`run-aspire.cmd` is the Windows entry point: it skips MSBuild when AppHost and Server dlls are `run-aspire.ps1` is the Windows entry point (`run-aspire.cmd` just forwards to it);
newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on PATH `run-aspire.sh` is the same on Ubuntu. They skip MSBuild when AppHost and Server dlls are
it also runs `tailscale serve --bg 5173` when `tailscale` is on PATH and connected; otherwise newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on
it warns and continues locally only. Pass `--no-tailscale` to skip Serve. Keep the script in sync PATH and connected they run `tailscale serve --bg 5173` and print the HTTPS client URL;
with `tools/apphost-uptodate.ps1` if the AppHost path ever moves. 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, `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 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 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. will hang; a test that waits for a lesson label will not.
- **`dotnet run` on the AppHost always evaluates MSBuild**, even when nothing changed. - **`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 `run-aspire.ps1` / `run-aspire.sh` skip that with `--no-build` when AppHost and Server dlls
C# / csproj / props. `--rebuild` forces a build. Bare `dotnet run --project src/HSchool.AppHost` are newer than C# / csproj / props. `--rebuild` forces a build. Bare
still compiles. `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 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 On Windows `run-aspire.ps1` does the same (`run-aspire.cmd` forwards to it and can be
and Node are on PATH first and passes any arguments through double-clicked). On Ubuntu `./run-aspire.sh`. Both check that the .NET SDK and Node are on
(`run-aspire.cmd --launch-profile http`). A second launch with no C# changes skips MSBuild; PATH first and pass any arguments through (`--launch-profile http`). A second launch with no
`run-aspire.cmd --rebuild` forces a build. C# changes skips MSBuild; `--rebuild` forces a build.
The Aspire dashboard opens with two resources: `server` (ASP.NET Core) and `client` (Vite dev The Aspire dashboard opens with two resources: `server` (ASP.NET Core) and `client` (Vite dev
server on port **5173**). 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), For a dev session over your tailnet, expose only the Vite client — not the game server (5180),
Aspire dashboard (15180), or SwarmUI. Aspire dashboard (15180), or SwarmUI.
`run-aspire.cmd` runs `tailscale serve --bg 5173` before the AppHost when `tailscale` is on `run-aspire.ps1` / `run-aspire.sh` run `tailscale serve --bg 5173` before the AppHost when
PATH and connected. If Tailscale is missing, offline, or `serve` fails, the script prints a `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` warning and continues with local access only (`http://localhost:5173`). When Serve is up, it
to skip Serve entirely. You can also run the command yourself once per boot if you start with prints the HTTPS client URL next to the local one; the Aspire dashboard stays on localhost.
`dotnet run --project src/HSchool.AppHost` instead. 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 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. 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`), а не Клиентский TypeScript и JSONC модов не входы: Vite и каталог читают диск (`ContentRoot`), а не
`bin`. Тестовые проекты AppHost не ссылается — их правки запуск не пересобирают. `bin`. Тестовые проекты AppHost не ссылается — их правки запуск не пересобирают.
Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `.cmd`) и в Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `run-aspire.ps1`) и в
`LaunchBuildStamp` (то, что проверяют тесты). Менять надо оба. `LaunchBuildStamp` (то, что проверяют тесты). Менять надо оба.
### Что не входит ### Что не входит
+4 -123
View File
@@ -1,127 +1,8 @@
@echo off @echo off
setlocal EnableDelayedExpansion rem Trampoline for Explorer double-click and muscle memory. The launcher is run-aspire.ps1.
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`
cd /d "%~dp0" cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0run-aspire.ps1" %*
where dotnet >nul 2>&1 set "EXITCODE=%ERRORLEVEL%"
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
echo %cmdcmdline% | find /i "%~nx0" >nul echo %cmdcmdline% | find /i "%~nx0" >nul
if not errorlevel 1 pause 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; namespace HSchool.AppHost.Tests;
/// <summary> /// <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> /// </summary>
internal static class LaunchBuildStamp 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. # Same rules as tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs and needs_build in run-aspire.sh.
# Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build. # Keep the three in sync. Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build.
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string] $RepoRoot, [string] $RepoRoot,