feat: establish GoAuto MVP baseline (#19)
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath,
|
||||
[string]$DatabaseHost,
|
||||
[Nullable[int]]$DatabasePort,
|
||||
[string]$DatabaseUser,
|
||||
[string]$DatabaseName,
|
||||
[switch]$SkipMigration,
|
||||
[switch]$ValidateConfigOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
||||
$serverDirectory = Join-Path $workspaceRoot "server"
|
||||
$migrationLog = Join-Path $serverDirectory "temp\startup-migration.log"
|
||||
$plainPassword = $null
|
||||
|
||||
function ConvertFrom-YamlScalar {
|
||||
param([string]$Value)
|
||||
|
||||
$value = $Value.Trim()
|
||||
if ($value.Length -ge 2) {
|
||||
if ($value.StartsWith('"') -and $value.EndsWith('"')) {
|
||||
return $value.Substring(1, $value.Length - 2).Replace('\"', '"').Replace('\\', '\')
|
||||
}
|
||||
if ($value.StartsWith("'") -and $value.EndsWith("'")) {
|
||||
return $value.Substring(1, $value.Length - 2).Replace("''", "'")
|
||||
}
|
||||
}
|
||||
return $value
|
||||
}
|
||||
|
||||
function Read-DatabaseConfig {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "Local configuration file was not found: $Path. Copy config.example.yaml to config.yaml and fill in database.password."
|
||||
}
|
||||
|
||||
$values = @{}
|
||||
$insideDatabase = $false
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
if ($line -match '^\s*(#.*)?$') {
|
||||
continue
|
||||
}
|
||||
if ($line -match '^database\s*:\s*$') {
|
||||
$insideDatabase = $true
|
||||
continue
|
||||
}
|
||||
if ($insideDatabase -and $line -match '^\S') {
|
||||
break
|
||||
}
|
||||
if ($insideDatabase -and $line -match '^\s+(host|port|user|password|name)\s*:\s*(.*?)\s*$') {
|
||||
$values[$Matches[1]] = ConvertFrom-YamlScalar $Matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($key in 'host', 'port', 'user', 'password', 'name') {
|
||||
if (-not $values.ContainsKey($key) -or [string]::IsNullOrWhiteSpace([string]$values[$key])) {
|
||||
throw "Missing database.$key in local configuration file: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
$parsedPort = 0
|
||||
if (-not [int]::TryParse($values.port, [ref]$parsedPort) -or $parsedPort -lt 1 -or $parsedPort -gt 65535) {
|
||||
throw "database.port must be an integer between 1 and 65535 in: $Path"
|
||||
}
|
||||
if ($values.user -notmatch '^[A-Za-z0-9_.-]+$') {
|
||||
throw "database.user contains unsupported characters in: $Path"
|
||||
}
|
||||
if ($values.name -notmatch '^[A-Za-z0-9_]+$') {
|
||||
throw "database.name contains unsupported characters in: $Path"
|
||||
}
|
||||
|
||||
return @{
|
||||
Host = [string]$values.host
|
||||
Port = $parsedPort
|
||||
User = [string]$values.user
|
||||
Password = [string]$values.password
|
||||
Name = [string]$values.name
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-MySqlClient {
|
||||
$services = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -match '^MySQL' -and $_.State -eq 'Running' }
|
||||
foreach ($service in $services) {
|
||||
if ($service.PathName -match '^"?(.*?\\mysqld\.exe)"?(?:\s|$)') {
|
||||
$serviceClient = Join-Path (Split-Path -Parent $Matches[1]) 'mysql.exe'
|
||||
if (Test-Path -LiteralPath $serviceClient) {
|
||||
return $serviceClient
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$defaultPath = "C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe"
|
||||
if (Test-Path -LiteralPath $defaultPath) {
|
||||
return $defaultPath
|
||||
}
|
||||
|
||||
$command = Get-Command mysql.exe -ErrorAction SilentlyContinue
|
||||
if ($command) {
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
throw "mysql.exe was not found. Add the MySQL 8.4 bin directory to PATH."
|
||||
}
|
||||
|
||||
try {
|
||||
if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
|
||||
$ConfigPath = Join-Path $workspaceRoot "config.yaml"
|
||||
}
|
||||
elseif (-not [IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path $workspaceRoot $ConfigPath
|
||||
}
|
||||
$ConfigPath = [IO.Path]::GetFullPath($ConfigPath)
|
||||
$databaseConfig = Read-DatabaseConfig $ConfigPath
|
||||
|
||||
if (-not $PSBoundParameters.ContainsKey('DatabaseHost')) {
|
||||
$DatabaseHost = $databaseConfig.Host
|
||||
}
|
||||
if (-not $PSBoundParameters.ContainsKey('DatabasePort')) {
|
||||
$DatabasePort = $databaseConfig.Port
|
||||
}
|
||||
if (-not $PSBoundParameters.ContainsKey('DatabaseUser')) {
|
||||
$DatabaseUser = $databaseConfig.User
|
||||
}
|
||||
if (-not $PSBoundParameters.ContainsKey('DatabaseName')) {
|
||||
$DatabaseName = $databaseConfig.Name
|
||||
}
|
||||
$plainPassword = $databaseConfig.Password
|
||||
|
||||
Write-Host "GoAuto local server" -ForegroundColor Cyan
|
||||
Write-Host "Config: $ConfigPath"
|
||||
Write-Host "MySQL: ${DatabaseUser}@${DatabaseHost}:${DatabasePort}/${DatabaseName}"
|
||||
if ($ValidateConfigOnly) {
|
||||
Write-Host "Local database configuration is valid." -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
|
||||
$mysqlClient = Resolve-MySqlClient
|
||||
$env:MYSQL_PWD = $plainPassword
|
||||
try {
|
||||
Write-Host "[1/3] Checking database ${DatabaseName}..." -ForegroundColor Cyan
|
||||
& $mysqlClient `
|
||||
--protocol=TCP `
|
||||
--host=$DatabaseHost `
|
||||
--port=$DatabasePort `
|
||||
--user=$DatabaseUser `
|
||||
--default-character-set=utf8mb4 `
|
||||
--execute="CREATE DATABASE IF NOT EXISTS ``${DatabaseName}`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "MySQL connection or database creation failed."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item Env:MYSQL_PWD -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$env:GOAUTO_DB_DRIVER = "mysql"
|
||||
$env:GOAUTO_DB_DSN = "${DatabaseUser}:${plainPassword}@tcp(${DatabaseHost}:${DatabasePort})/${DatabaseName}?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s"
|
||||
|
||||
Push-Location $serverDirectory
|
||||
try {
|
||||
if (-not $SkipMigration) {
|
||||
Write-Host "[2/3] Running database migrations..." -ForegroundColor Cyan
|
||||
New-Item -ItemType Directory -Path (Split-Path -Parent $migrationLog) -Force | Out-Null
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
try {
|
||||
$ErrorActionPreference = "Continue"
|
||||
& go run . migrate -c config/settings.yml 2>&1 |
|
||||
Tee-Object -FilePath $migrationLog
|
||||
$migrationExitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($migrationExitCode -ne 0) {
|
||||
$migrationTail = (Get-Content -LiteralPath $migrationLog -Tail 40 -ErrorAction SilentlyContinue) -join [Environment]::NewLine
|
||||
throw "Database migration failed. Log: ${migrationLog}`n${migrationTail}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[2/3] Database migration skipped." -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
Write-Host "[3/3] Starting server..." -ForegroundColor Green
|
||||
Write-Host "Local URL: http://127.0.0.1:8000"
|
||||
Write-Host "Android URL: use this computer's LAN IP on port 8000"
|
||||
Write-Host "Press Ctrl+C to stop." -ForegroundColor DarkYellow
|
||||
& go run . server -c config/settings.yml
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Server exited with code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item Env:MYSQL_PWD -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:GOAUTO_DB_DSN -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:GOAUTO_DB_DRIVER -ErrorAction SilentlyContinue
|
||||
$plainPassword = $null
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
param(
|
||||
[ValidateSet("all", "server", "web", "android")]
|
||||
[string]$Component = "all"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
function Invoke-Step([string]$Name, [scriptblock]$Action) {
|
||||
Write-Host "==> $Name"
|
||||
& $Action
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Name failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
if ($Component -in @("all", "server")) {
|
||||
Push-Location (Join-Path $projectRoot "server")
|
||||
try {
|
||||
$env:GOTOOLCHAIN = "auto"
|
||||
Invoke-Step "server: go test ./..." { go test ./... }
|
||||
Invoke-Step "server: go build" { go build -o (Join-Path $env:TEMP "goauto-server-check.exe") . }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
if ($Component -in @("all", "web")) {
|
||||
Push-Location (Join-Path $projectRoot "web")
|
||||
try {
|
||||
Invoke-Step "web: pnpm install" { pnpm install --frozen-lockfile }
|
||||
Invoke-Step "web: lint" { pnpm run lint }
|
||||
Invoke-Step "web: build" { pnpm run build:prod }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
if ($Component -in @("all", "android")) {
|
||||
Push-Location (Join-Path $projectRoot "android")
|
||||
try {
|
||||
Invoke-Step "android: unit tests" { .\gradlew.bat test }
|
||||
Invoke-Step "android: debug APK" { .\gradlew.bat assembleDebug }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user