Ветка задачи может принести миграцию, а run.ps1/run.sh сразу запускали uvicorn: приложение поднималось на старой схеме и падало 500 на первом обращении к новой таблице. Так вышло с 0012_match_finish_draft — создание партии отвечало «no such table: match_finish_drafts». В test/prod такого нет, там схему накатывает entrypoint.sh контейнера. Отдельно разобран случай «база новее ветки»: после возврата с ветки задачи alembic не находит ревизию, которой в этой ветке ещё нет. Останавливать запуск тут не за что — лишние таблицы старому коду не мешают, поэтому печатаем предупреждение и идём дальше. Настоящая ошибка миграции по-прежнему останавливает запуск. stderr alembic в run.ps1 уводится в файл: в Windows PowerShell 5.1 «2>&1» на нативном exe заворачивает каждую строку в ErrorRecord, а решение принимается по коду возврата. run.ps1 остался строго ASCII. #36 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
163 lines
7.8 KiB
PowerShell
163 lines
7.8 KiB
PowerShell
#!/usr/bin/env pwsh
|
|
# Unified launcher for dev / test. APP_ENV in the root .env decides what to run.
|
|
# development -> uvicorn --reload (backend) + vite (frontend), native, two windows
|
|
# test -> docker compose prod-clone (port 8080)
|
|
# production -> NOT started by launcher (prod is separate: docker compose up -d on Pi)
|
|
#
|
|
# LOCAL_PUBLIC=vps (dev only) opens an SSH tunnel to the VPS, exposing dev at
|
|
# https://forbidden-stars.ru. test/prod expose themselves via an in-container tunnel.
|
|
#
|
|
# Run: .\run.ps1
|
|
# If blocked by policy: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
|
#
|
|
# NOTE: keep this file ASCII-only. Windows PowerShell 5.1 reads BOM-less .ps1 in the
|
|
# system ANSI codepage, so non-ASCII (Cyrillic / box chars) breaks the parser.
|
|
$ErrorActionPreference = "Stop"
|
|
$root = $PSScriptRoot
|
|
|
|
$envFile = Join-Path $root ".env"
|
|
if (-not (Test-Path $envFile)) {
|
|
Write-Host "No .env in repo root. Create it: Copy-Item .env.example .env" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# Read a key from .env (last assignment wins; comments ignored).
|
|
function Get-EnvVal([string]$name, [string]$default) {
|
|
$val = $default
|
|
foreach ($line in Get-Content $envFile) {
|
|
if ($line -match "^\s*$name\s*=\s*([^#\s]+)") { $val = $matches[1].Trim() }
|
|
}
|
|
return $val
|
|
}
|
|
|
|
$appEnv = (Get-EnvVal "APP_ENV" "development").ToLower()
|
|
$localPublic = (Get-EnvVal "LOCAL_PUBLIC" "local").ToLower()
|
|
$vpsHost = Get-EnvVal "VPS_TUNNEL_HOST" ""
|
|
$vpsUser = Get-EnvVal "VPS_TUNNEL_USER" "tunnel"
|
|
$vpsPort = Get-EnvVal "VPS_TUNNEL_PORT" "9001"
|
|
|
|
Write-Host "APP_ENV = $appEnv LOCAL_PUBLIC = $localPublic" -ForegroundColor Cyan
|
|
|
|
# Open SSH reverse tunnel VPS:$vpsPort -> localhost:$localPort (in a separate window).
|
|
function Start-VpsTunnel([int]$localPort) {
|
|
if (-not $vpsHost) {
|
|
Write-Host "LOCAL_PUBLIC=vps but VPS_TUNNEL_HOST is empty in .env - tunnel skipped." -ForegroundColor Red
|
|
return
|
|
}
|
|
Write-Host "SSH tunnel: forbidden-stars.ru (VPS:$vpsPort) -> localhost:$localPort ..." -ForegroundColor Green
|
|
$ssh = "ssh -N -R ${vpsPort}:localhost:${localPort} " +
|
|
"-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes " +
|
|
"${vpsUser}@${vpsHost}"
|
|
Start-Process powershell -ArgumentList @(
|
|
"-NoExit", "-Command",
|
|
"Write-Host 'SSH tunnel to forbidden-stars.ru. Close this window to stop.' -ForegroundColor Cyan; $ssh"
|
|
)
|
|
}
|
|
|
|
# Kill any leftover tunnel from a previous run, so LOCAL_PUBLIC is authoritative each launch
|
|
# (the tunnel lives in its own window and would otherwise keep the domain served).
|
|
function Stop-VpsTunnel {
|
|
Get-CimInstance Win32_Process -Filter "Name='ssh.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.CommandLine -match "-R\s+${vpsPort}:localhost" } |
|
|
ForEach-Object {
|
|
Write-Host "Closing leftover tunnel (pid $($_.ProcessId))..." -ForegroundColor DarkYellow
|
|
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# Start from a clean tunnel state; a fresh tunnel is opened only if LOCAL_PUBLIC=vps below.
|
|
Stop-VpsTunnel
|
|
|
|
switch ($appEnv) {
|
|
"development" {
|
|
$backend = Join-Path $root "backend"
|
|
$frontend = Join-Path $root "frontend"
|
|
$py = Join-Path $backend ".venv\Scripts\python.exe"
|
|
if (-not (Test-Path $py)) {
|
|
Write-Host "venv not found: $py" -ForegroundColor Red
|
|
Write-Host "First: cd backend; python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e `".[dev]`"" -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
if (-not (Test-Path (Join-Path $frontend "node_modules"))) {
|
|
Write-Host "node_modules not found. First: cd frontend; npm install" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
# Schema first: a branch may add a migration, and uvicorn would otherwise start
|
|
# on the old schema and fail with 500 on the first hit of a new table.
|
|
# stderr goes to a file: alembic logs there, and in PowerShell 5.1 "2>&1" on a
|
|
# native exe wraps every line into an ErrorRecord.
|
|
Write-Host "Applying migrations (alembic upgrade head)..." -ForegroundColor Green
|
|
$migLog = [System.IO.Path]::GetTempFileName()
|
|
Push-Location $backend
|
|
& $py -m alembic upgrade head 2> $migLog
|
|
$migCode = $LASTEXITCODE
|
|
Pop-Location
|
|
$migText = (Get-Content $migLog -Raw)
|
|
Remove-Item $migLog -Force -ErrorAction SilentlyContinue
|
|
if ($migCode -ne 0) {
|
|
Write-Host $migText
|
|
if ($migText -match "locate revision") {
|
|
# The DB carries a migration this branch does not have (switched back from
|
|
# a feature branch). Extra tables do not bother the older code - go on.
|
|
Write-Host "DB is newer than this branch - migrations skipped." -ForegroundColor Yellow
|
|
} else {
|
|
Write-Host "Migrations failed - not starting. Fix the error above and retry." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
Write-Host "Starting backend (uvicorn --reload) and frontend (vite) in separate windows..." -ForegroundColor Green
|
|
Start-Process powershell -ArgumentList @(
|
|
"-NoExit", "-Command",
|
|
"Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload"
|
|
)
|
|
Start-Process powershell -ArgumentList @(
|
|
"-NoExit", "-Command",
|
|
"Set-Location '$frontend'; npm run dev"
|
|
)
|
|
Write-Host ""
|
|
Write-Host " Backend: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
|
|
Write-Host " Frontend: http://127.0.0.1:5173" -ForegroundColor Green
|
|
if ($localPublic -eq "vps") {
|
|
Start-VpsTunnel 5173
|
|
Write-Host " Public: https://forbidden-stars.ru" -ForegroundColor Green
|
|
}
|
|
}
|
|
"test" {
|
|
$compose = Join-Path $root "docker-compose.test.yml"
|
|
docker info --format '{{.ServerVersion}}' *> $null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host "Docker daemon is not reachable. Start Docker Desktop, wait until it is running, then re-run .\run.ps1" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
$keyFile = Join-Path $root "deploy\tunnel\id_tunnel"
|
|
if (-not (Test-Path $keyFile -PathType Leaf)) {
|
|
if (Test-Path $keyFile -PathType Container) {
|
|
Write-Host "deploy\tunnel\id_tunnel is a DIRECTORY - Docker auto-created it because the key file was missing." -ForegroundColor Red
|
|
Write-Host "Remove it first: Remove-Item -Recurse -Force deploy\tunnel\id_tunnel" -ForegroundColor Yellow
|
|
} else {
|
|
Write-Host "No SSH key at deploy\tunnel\id_tunnel (the tunnel container needs it)." -ForegroundColor Red
|
|
}
|
|
Write-Host "Add your VPS-authorized key: Copy-Item `$env:USERPROFILE\.ssh\id_ed25519 deploy\tunnel\id_tunnel" -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
Write-Host "Building and starting prod-clone in Docker (app + in-container tunnel)..." -ForegroundColor Green
|
|
docker compose -f $compose up --build -d
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
Write-Host ""
|
|
Write-Host " Public: https://forbidden-stars.ru (Swagger: /api/docs)" -ForegroundColor Green
|
|
Write-Host " (no host port - reachable only via the domain; tunnel runs inside compose)"
|
|
Write-Host " Logs: docker compose -f docker-compose.test.yml logs -f"
|
|
Write-Host " Stop: docker compose -f docker-compose.test.yml down -v"
|
|
}
|
|
"production" {
|
|
Write-Host "production is not started by the launcher - prod is separate." -ForegroundColor Yellow
|
|
Write-Host "Deploy on Pi (from main branch): docker compose up -d --build"
|
|
exit 1
|
|
}
|
|
default {
|
|
Write-Host "Unknown APP_ENV='$appEnv'. Allowed: development | test | production." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|