Functions/Publish-FlutterWeb.ps1
|
<#
.SYNOPSIS Compila y despliega una aplicación Flutter Web a un servidor Linux remoto vía SSH. .DESCRIPTION El cmdlet `Publish-FlutterWeb` gestiona el ciclo completo de despliegue de apps Flutter Web: - Lee `name` y `version` de `pubspec.yaml` (single source of truth). - Lee la configuración de despliegue de `deploy.yaml` (server, port). - Compila con `Invoke-FlutterBuild -Web` y empaqueta los artefactos. - Sube al servidor con releases versionados en /var/www/<name>/releases/ y symlink `current`. - Configura nginx con un site dedicado en sites-available/ si no existe. Se debe ejecutar desde la raíz del proyecto Flutter donde existen: - pubspec.yaml (name, version) - deploy.yaml (servidor, puerto — generar con -Init) .PARAMETER Init Genera el archivo deploy.yaml en el directorio actual. Requiere que exista pubspec.yaml. .PARAMETER Publish Ejecuta el despliegue completo al servidor remoto. Lee deploy.yaml para el servidor destino y el puerto nginx. .PARAMETER DeployReport Muestra las acciones que realizará -Publish sin ejecutarlas. Consulta el servidor para mostrar: versión actual, si la release existe, estado de nginx. .EXAMPLE Publish-FlutterWeb -Init Genera deploy.yaml en el directorio actual del proyecto Flutter. .EXAMPLE Publish-FlutterWeb -DeployReport Muestra un reporte de lo que hará -Publish sin realizar cambios. .EXAMPLE Publish-FlutterWeb -Publish Compila, empaqueta, sube y despliega la app Flutter Web al servidor configurado en deploy.yaml. .NOTES Versión: 2.0.0 Autor: @ccisnedev Requiere: - Flutter SDK instalado y en PATH - Configuración del host en ~/.ssh/config (Host, HostName, User, Port, IdentityFile) - nginx en el servidor remoto con sites-available/ y sites-enabled/ - Módulo powershell-yaml para parseo de deploy.yaml #> function Publish-FlutterWeb { [CmdletBinding(DefaultParameterSetName = 'Publish')] param( [Parameter(Mandatory, ParameterSetName = 'Init', HelpMessage = "Genera archivo de configuración (deploy.yaml)")] [switch]$Init, [Parameter(Mandatory, ParameterSetName = 'Publish', HelpMessage = "Ejecuta el despliegue completo al servidor remoto")] [switch]$Publish, [Parameter(Mandatory, ParameterSetName = 'DeployReport', HelpMessage = "Muestra las acciones que realizará -Publish sin ejecutarlas")] [switch]$DeployReport ) begin { $ErrorActionPreference = 'Stop' Ensure-YamlModule } process { # Banner Write-Host "" Write-Host "╔══════════════════════════════════════════════════╗" -ForegroundColor Cyan Write-Host "║ Publish-FlutterWeb — macss-devops ║" -ForegroundColor Cyan Write-Host "╚══════════════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" switch ($PSCmdlet.ParameterSetName) { # ═══════════════════════════════════════════════════ # INIT — Generar deploy.yaml # ═══════════════════════════════════════════════════ 'Init' { $cwd = (Get-Location).Path # Validar pubspec.yaml (es un proyecto Flutter) $pubspecPath = Join-Path $cwd "pubspec.yaml" if (-not (Test-Path $pubspecPath)) { throw "No se encontró pubspec.yaml en $cwd. Ejecute este cmdlet dentro de un proyecto Flutter." } # Validar que deploy.yaml no exista $deployYamlPath = Join-Path $cwd "deploy.yaml" if (Test-Path $deployYamlPath) { throw "Ya existe deploy.yaml en $cwd. Elimínelo primero si desea regenerar la configuración." } # Leer pubspec.yaml para mostrar información $pubspec = Get-Content $pubspecPath -Raw | ConvertFrom-Yaml $appName = $pubspec.name $appVersion = ($pubspec.version -split '\+')[0] Write-Host " Proyecto: $appName" -ForegroundColor Cyan Write-Host " Versión: $appVersion" -ForegroundColor Cyan Write-Host "" # Copiar template deploy.yaml $templatePath = Join-Path $PSScriptRoot "..\Resources\Publish-FlutterWeb\templates\deploy.yaml" if (-not (Test-Path $templatePath)) { throw "Template no encontrado: $templatePath" } Copy-Item -Path $templatePath -Destination $deployYamlPath Write-Host " Creado: deploy.yaml" -ForegroundColor Green # Instrucciones Write-Host "" Write-Host " Configuración creada. Próximos pasos:" -ForegroundColor Green Write-Host " 1. Edite deploy.yaml → cambie 'server' por su alias SSH" -ForegroundColor DarkGray Write-Host " 2. Edite deploy.yaml → cambie 'port' por el puerto nginx deseado" -ForegroundColor DarkGray Write-Host " 3. Ejecute: Publish-FlutterWeb -Publish" -ForegroundColor DarkGray Write-Host "" } # ═══════════════════════════════════════════════════ # PUBLISH — Despliegue completo # ═══════════════════════════════════════════════════ 'Publish' { $cwd = (Get-Location).Path # ─── 0. Validaciones ───────────────────────── $pubspecPath = Join-Path $cwd "pubspec.yaml" $deployYamlPath = Join-Path $cwd "deploy.yaml" if (-not (Test-Path $pubspecPath)) { throw "No se encontró pubspec.yaml en $cwd. Ejecute este cmdlet dentro de un proyecto Flutter." } if (-not (Test-Path $deployYamlPath)) { throw "No se encontró deploy.yaml. Ejecute 'Publish-FlutterWeb -Init' primero." } # ─── 1. Cargar helpers ─────────────────────── . "$PSScriptRoot/../Private/PublishHelpers.ps1" . "$PSScriptRoot/../Private/Read-SSHConfig.ps1" # ─── 2. Leer configuración ─────────────────── # pubspec.yaml (name, version) $pubspec = Get-Content $pubspecPath -Raw | ConvertFrom-Yaml $appName = $pubspec.name $appVersion = ($pubspec.version -split '\+')[0] # sin build metadata $release = "v$appVersion" # deploy.yaml (server, port) $deployConfig = Get-Content $deployYamlPath -Raw | ConvertFrom-Yaml $server = $deployConfig.server $port = $deployConfig.port # ─── 3. Validaciones de config ─────────────── if (-not $server) { throw "No se encontró 'server:' en deploy.yaml." } if ($server -eq 'your-ssh-alias') { throw "deploy.yaml contiene el valor de ejemplo 'your-ssh-alias'. Cambie 'server' por el alias SSH real de su servidor." } if (-not $port) { throw "No se encontró 'port:' en deploy.yaml." } Write-Host " Proyecto: $appName" -ForegroundColor Cyan Write-Host " Versión: $release" -ForegroundColor Cyan Write-Host " Servidor: $server" -ForegroundColor Cyan Write-Host " Puerto: $port" -ForegroundColor Cyan Write-Host "" # ─── 4. SSH Config ─────────────────────────── $sshConfig = Read-SSHConfig -HostAlias $server $user = $sshConfig.User $ip = $sshConfig.HostName $sshPort = $sshConfig.Port $privateKeyPath = $sshConfig.IdentityFile # ─── Constantes remotas ────────────────────── $remoteWebRoot = "/var/www" $zipFileName = "${appName}_web_${release}.zip" $remoteZipPath = "/tmp/$zipFileName" # ─── 5. Build Flutter Web ──────────────────── $webBuildFolder = "release/app_${appName}_v${appVersion}_web" if (Test-Path $webBuildFolder) { Write-Host " Limpiando build anterior: $webBuildFolder" -ForegroundColor Yellow Remove-Item -Recurse -Force $webBuildFolder } Write-Host " Compilando Flutter Web..." -ForegroundColor Cyan Invoke-FlutterBuild -Web $webBuildPath = Join-Path $cwd $webBuildFolder if (-not (Test-Path (Join-Path $webBuildPath "index.html"))) { throw "Build Flutter Web no generó index.html. Verifique que 'flutter build web' funciona correctamente." } Write-Host " Build completado: $webBuildFolder" -ForegroundColor Green # ─── 6. Comprimir artefactos ───────────────── Write-Host " Comprimiendo artefactos..." -ForegroundColor Cyan $localZipPath = Join-Path ([System.IO.Path]::GetTempPath()) $zipFileName if (Test-Path $localZipPath) { Remove-Item $localZipPath -Force } Compress-Archive -Path "$webBuildPath\*" -DestinationPath $localZipPath -CompressionLevel Optimal -Force $zipSize = [math]::Round((Get-Item $localZipPath).Length / 1MB, 1) Write-Host " Zip: $zipFileName ($($zipSize) MB)" -ForegroundColor Green try { # ─── 7. SCP: subir zip ─────────────────── Write-Host " Subiendo archivos a $ip..." -ForegroundColor Cyan $scpArgs = @('-i', $privateKeyPath, '-P', $sshPort, $localZipPath, "$($user)@$($ip):$remoteZipPath") & scp @scpArgs 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { throw "Error al subir zip (scp exit: $LASTEXITCODE)" } Write-Host " Zip subido" -ForegroundColor Green # ─── 8. Instalar release ───────────────── Write-Host " Instalando release $release..." -ForegroundColor Cyan $installScript = Get-BashScript -ScriptName "Install-FlutterWeb.sh" -Placeholders @{ '__NAME__' = $appName '__VERSION__' = $appVersion '__WEB_ROOT__' = $remoteWebRoot } $exitCode = Invoke-RemoteScript -ScriptContent $installScript ` -User $user -IP $ip -Port $sshPort ` -KeyPath $privateKeyPath ` -ScriptPrefix "psdevops_install_flutterweb_" if ($exitCode -ne 0) { throw "Instalación falló con código $exitCode. Revise la salida anterior." } # ─── 9. Configurar nginx ───────────────── Write-Host " Verificando configuración nginx..." -ForegroundColor Cyan $nginxScript = Get-BashScript -ScriptName "Configure-NginxSite.sh" -Placeholders @{ '__NAME__' = $appName '__PORT__' = $port.ToString() } $exitCode = Invoke-RemoteScript -ScriptContent $nginxScript ` -User $user -IP $ip -Port $sshPort ` -KeyPath $privateKeyPath ` -ScriptPrefix "psdevops_nginx_flutterweb_" if ($exitCode -ne 0) { throw "Configuración de nginx falló con código $exitCode" } # ─── 10. Verificación ──────────────────── Write-Host " Verificando: http://127.0.0.1:$port/" -ForegroundColor Cyan $verifyScript = @" #!/bin/bash set -e sleep 1 HTTP_CODE=`$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:$port/ 2>/dev/null || echo '000') if [ "`$HTTP_CODE" = "200" ]; then echo "OK: HTTP $port responde 200" exit 0 else echo "WARNING: HTTP $port respondió `$HTTP_CODE (puede necesitar tiempo para iniciar)" >&2 exit 0 fi "@ Invoke-RemoteScript -ScriptContent $verifyScript ` -User $user -IP $ip -Port $sshPort ` -KeyPath $privateKeyPath ` -ScriptPrefix "psdevops_verify_flutterweb_" | Out-Null # ─── Éxito ─────────────────────────────── Write-Host "" Write-Host "══════════════════════════════════════════════════" -ForegroundColor Green Write-Host " Deploy completado: $appName $release" -ForegroundColor Green Write-Host "══════════════════════════════════════════════════" -ForegroundColor Green Write-Host " Servidor: $ip" -ForegroundColor White Write-Host " Release: $remoteWebRoot/$appName/releases/$release" -ForegroundColor White Write-Host " Nginx: puerto $port" -ForegroundColor White Write-Host "══════════════════════════════════════════════════" -ForegroundColor Green Write-Host "" } finally { # Limpiar archivos temporales locales Remove-Item -LiteralPath $localZipPath -ErrorAction SilentlyContinue } } # ═══════════════════════════════════════════════════ # DEPLOY REPORT — Reporte pre-deploy (dry-run) # ═══════════════════════════════════════════════════ 'DeployReport' { $cwd = (Get-Location).Path # ─── 0. Validaciones ───────────────────────── $pubspecPath = Join-Path $cwd "pubspec.yaml" $deployYamlPath = Join-Path $cwd "deploy.yaml" if (-not (Test-Path $pubspecPath)) { throw "No se encontró pubspec.yaml en $cwd. Ejecute este cmdlet dentro de un proyecto Flutter." } if (-not (Test-Path $deployYamlPath)) { throw "No se encontró deploy.yaml. Ejecute 'Publish-FlutterWeb -Init' primero." } # ─── 1. Cargar helpers ─────────────────────── . "$PSScriptRoot/../Private/PublishHelpers.ps1" . "$PSScriptRoot/../Private/Read-SSHConfig.ps1" # ─── 2. Leer configuración ─────────────────── $pubspec = Get-Content $pubspecPath -Raw | ConvertFrom-Yaml $appName = $pubspec.name $appVersion = ($pubspec.version -split '\+')[0] $release = "v$appVersion" $deployConfig = Get-Content $deployYamlPath -Raw | ConvertFrom-Yaml $server = $deployConfig.server $port = $deployConfig.port # ─── 3. Validaciones de config ─────────────── if (-not $server) { throw "No se encontró 'server:' en deploy.yaml." } if ($server -eq 'your-ssh-alias') { throw "deploy.yaml contiene el valor de ejemplo 'your-ssh-alias'. Cambie 'server' por el alias SSH real de su servidor." } if (-not $port) { throw "No se encontró 'port:' en deploy.yaml." } # ─── 4. SSH Config ─────────────────────────── $sshConfig = Read-SSHConfig -HostAlias $server $user = $sshConfig.User $ip = $sshConfig.HostName $sshPort = $sshConfig.Port $privateKeyPath = $sshConfig.IdentityFile $remoteWebRoot = "/var/www" Write-Host " Modo: SOLO REPORTE (no se realizarán cambios)" -ForegroundColor Yellow Write-Host "" Write-Host " ─── Configuración local ───" -ForegroundColor Cyan Write-Host " Proyecto: $appName" -ForegroundColor White Write-Host " Versión: $release" -ForegroundColor White Write-Host " Servidor: $server ($ip)" -ForegroundColor White Write-Host " Puerto: $port" -ForegroundColor White Write-Host "" # ─── 5. Consultar estado del servidor ──────── Write-Host " ─── Estado del servidor ───" -ForegroundColor Cyan $reportScript = @" #!/bin/bash # Versión actual (symlink current) if [ -L "$remoteWebRoot/$appName/current" ]; then CURRENT=`$(readlink "$remoteWebRoot/$appName/current" | xargs basename) echo "CURRENT:`$CURRENT" else echo "CURRENT:none" fi # Release destino ya existe? if [ -d "$remoteWebRoot/$appName/releases/$release" ]; then echo "RELEASE:exists" else echo "RELEASE:new" fi # Nginx config if [ -f "/etc/nginx/sites-available/$appName" ]; then echo "NGINX:exists" else # Verificar puerto libre if ss -tlnH sport = :$port | grep -q .; then echo "NGINX:port-in-use" else echo "NGINX:will-create" fi fi "@ # Ejecutar script remoto y capturar salida (sin Invoke-RemoteScript, # necesitamos parsear stdout) $tmpLocal = New-UnixTempFile -Content $reportScript -Prefix "psdevops_report_flutterweb_" try { $remoteName = [IO.Path]::GetFileName($tmpLocal) $remotePath = "/tmp/$remoteName" & scp -i $privateKeyPath -P $sshPort $tmpLocal "$($user)@$($ip):$remotePath" 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { throw "Error al conectar con el servidor (scp exit: $LASTEXITCODE)" } $remoteCmd = "bash $remotePath ; rc=`$?; rm -f $remotePath; exit `$rc" $output = & ssh -i $privateKeyPath -p $sshPort "$($user)@$($ip)" $remoteCmd 2>&1 } finally { Remove-Item -LiteralPath $tmpLocal -ErrorAction SilentlyContinue } # Parsear salida $currentVersion = 'desconocido' $releaseStatus = 'desconocido' $nginxStatus = 'desconocido' foreach ($line in $output) { if ($line -match '^CURRENT:(.+)$') { $currentVersion = $Matches[1] } if ($line -match '^RELEASE:(.+)$') { $releaseStatus = $Matches[1] } if ($line -match '^NGINX:(.+)$') { $nginxStatus = $Matches[1] } } # Mostrar estado if ($currentVersion -eq 'none') { Write-Host " Current: (primer deploy)" -ForegroundColor Yellow } else { Write-Host " Current: $currentVersion" -ForegroundColor White } if ($releaseStatus -eq 'exists') { Write-Host " Release: $release ya existe (se sobreescribirá)" -ForegroundColor Yellow } else { Write-Host " Release: $release (nueva)" -ForegroundColor Green } if ($nginxStatus -eq 'exists') { Write-Host " Nginx: config existe (no se modifica)" -ForegroundColor White } elseif ($nginxStatus -eq 'port-in-use') { Write-Host " Nginx: PUERTO $port EN USO — el deploy fallará" -ForegroundColor Red } else { Write-Host " Nginx: se creará config en puerto $port" -ForegroundColor Green } # ─── 6. Acciones que realizará -Publish ────── Write-Host "" Write-Host " ─── Acciones que realizará -Publish ───" -ForegroundColor Cyan Write-Host " 1. Compilar Flutter Web (Invoke-FlutterBuild -Web)" -ForegroundColor White Write-Host " 2. Comprimir artefactos en zip" -ForegroundColor White Write-Host " 3. Subir zip a ${ip}:/tmp/" -ForegroundColor White Write-Host " 4. Instalar en ${remoteWebRoot}/${appName}/releases/${release}/" -ForegroundColor White Write-Host " 5. Actualizar symlink current → $release" -ForegroundColor White if ($nginxStatus -ne 'exists') { Write-Host " 6. Crear configuración nginx en puerto $port" -ForegroundColor White } Write-Host "" } } } } |