Initial commit
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Local deployment configuration contains server credentials.
|
||||||
|
targets.json
|
||||||
|
|
||||||
|
# Legacy local scripts contain embedded credentials.
|
||||||
|
0001-backup_gt_nova.ps1
|
||||||
|
0002-deploy_gt_nova_v3.ps1
|
||||||
|
|
||||||
|
# Local Windows shortcuts.
|
||||||
|
*.lnk
|
||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ZipPath,
|
||||||
|
|
||||||
|
[string]$ReportsZipPath = "",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$TargetId,
|
||||||
|
|
||||||
|
[string]$TargetsPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Write-Step {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Success {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[SUCCESS] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ToolPath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$FileName,
|
||||||
|
|
||||||
|
[string[]]$FallbackPaths = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$localPath = Join-Path $PSScriptRoot $FileName
|
||||||
|
if (Test-Path $localPath) { return $localPath }
|
||||||
|
|
||||||
|
foreach ($path in $FallbackPaths) {
|
||||||
|
if (Test-Path $path) { return $path }
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = Get-Command $FileName -ErrorAction SilentlyContinue
|
||||||
|
if ($command) { return $command.Source }
|
||||||
|
|
||||||
|
throw "No se encontro $FileName. Copialo junto a la app o instalalo con PuTTY."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ZipEntryNames {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
$zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
|
||||||
|
try {
|
||||||
|
return @(
|
||||||
|
$zip.Entries |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_.FullName) } |
|
||||||
|
ForEach-Object { $_.FullName.Replace("\", "/") } |
|
||||||
|
Where-Object {
|
||||||
|
$_ -and
|
||||||
|
$_ -notmatch "^__MACOSX/" -and
|
||||||
|
$_ -notmatch "(^|/)\.DS_Store$"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$zip.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ShellArg {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
$singleQuote = [string][char]39
|
||||||
|
return $singleQuote + $Value.Replace($singleQuote, "$singleQuote\$singleQuote$singleQuote") + $singleQuote
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plink {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Command
|
||||||
|
)
|
||||||
|
|
||||||
|
& $script:Plink -ssh "$($script:Target.user)@$($script:Target.host)" -P $script:Target.port -pw $script:Target.password -batch $Command 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($TargetsPath)) {
|
||||||
|
$TargetsPath = Join-Path $PSScriptRoot "targets.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $TargetsPath)) {
|
||||||
|
throw "No se encontro el archivo de destinos: $TargetsPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $ZipPath)) {
|
||||||
|
throw "No se encontro el ZIP principal: $ZipPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetsConfig = Get-Content -Raw -Path $TargetsPath | ConvertFrom-Json
|
||||||
|
$targetMatches = @($targetsConfig.targets | Where-Object { $_.id -eq $TargetId })
|
||||||
|
if ($targetMatches.Count -eq 0) {
|
||||||
|
throw "No existe el destino seleccionado: $TargetId"
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:Target = $targetMatches[0]
|
||||||
|
$script:Plink = Get-ToolPath -FileName "plink.exe" -FallbackPaths @(
|
||||||
|
"C:\Program Files\PuTTY\plink.exe",
|
||||||
|
"C:\Program Files (x86)\PuTTY\plink.exe"
|
||||||
|
)
|
||||||
|
$pscp = Get-ToolPath -FileName "pscp.exe" -FallbackPaths @(
|
||||||
|
"C:\Program Files\PuTTY\pscp.exe",
|
||||||
|
"C:\Program Files (x86)\PuTTY\pscp.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
$zipFileName = Split-Path -Leaf $ZipPath
|
||||||
|
$reportesFolderName = "reportes_subidos_jasper"
|
||||||
|
$hasReportsZipPath = -not [string]::IsNullOrWhiteSpace($ReportsZipPath)
|
||||||
|
|
||||||
|
if ($hasReportsZipPath -and -not (Test-Path $ReportsZipPath)) {
|
||||||
|
throw "No se encontro el ZIP de reportes: $ReportsZipPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Version indicada: $Version"
|
||||||
|
Write-Step "Destino: $($Target.name) ($($Target.host):$($Target.port))"
|
||||||
|
Write-Step "Validando estructura del ZIP principal..."
|
||||||
|
|
||||||
|
$zipEntryNames = Get-ZipEntryNames -Path $ZipPath
|
||||||
|
if ($zipEntryNames.Count -eq 0) {
|
||||||
|
throw "El ZIP principal esta vacio: $ZipPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$topLevelNames = @(
|
||||||
|
$zipEntryNames |
|
||||||
|
ForEach-Object { ($_ -split "/", 2)[0] } |
|
||||||
|
Where-Object { $_ } |
|
||||||
|
Sort-Object -Unique
|
||||||
|
)
|
||||||
|
|
||||||
|
$topLevelFiles = @(
|
||||||
|
$zipEntryNames |
|
||||||
|
Where-Object { ($_ -notmatch "/") -and (-not $_.EndsWith("/")) }
|
||||||
|
)
|
||||||
|
|
||||||
|
$zipRootPrefix = $null
|
||||||
|
if (($topLevelNames.Count -eq 1) -and ($topLevelFiles.Count -eq 0)) {
|
||||||
|
$zipRootPrefix = $topLevelNames[0]
|
||||||
|
Write-Step "El ZIP contiene una carpeta raiz ($zipRootPrefix). Se desplegara su contenido."
|
||||||
|
}
|
||||||
|
|
||||||
|
$deployEntryNames = $zipEntryNames
|
||||||
|
if ($zipRootPrefix) {
|
||||||
|
$rootPrefixPattern = "^" + [regex]::Escape($zipRootPrefix.TrimEnd("/")) + "/"
|
||||||
|
$deployEntryNames = @(
|
||||||
|
$zipEntryNames |
|
||||||
|
Where-Object { $_ -match $rootPrefixPattern } |
|
||||||
|
ForEach-Object { $_ -replace $rootPrefixPattern, "" } |
|
||||||
|
Where-Object { $_ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$deployTopLevelFolders = @(
|
||||||
|
$deployEntryNames |
|
||||||
|
Where-Object { $_ -match "/" } |
|
||||||
|
ForEach-Object { ($_ -split "/", 2)[0] } |
|
||||||
|
Where-Object { $_ } |
|
||||||
|
Sort-Object -Unique
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($deployTopLevelFolders.Count -le 1) {
|
||||||
|
throw "El contenido a desplegar debe tener mas de una carpeta en el nivel padre."
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasReportesFolder = @(
|
||||||
|
$deployEntryNames |
|
||||||
|
Where-Object {
|
||||||
|
$_ -eq "$reportesFolderName/" -or
|
||||||
|
$_ -like "$reportesFolderName/*"
|
||||||
|
}
|
||||||
|
).Count -gt 0
|
||||||
|
|
||||||
|
$needsReportesZip = -not $hasReportesFolder
|
||||||
|
$reportesZipExtractMode = "direct"
|
||||||
|
$reportesZipName = ""
|
||||||
|
|
||||||
|
if ($needsReportesZip) {
|
||||||
|
if (-not $hasReportsZipPath) {
|
||||||
|
Write-Step "El ZIP no incluye $reportesFolderName y no se selecciono ZIP auxiliar. Se continua sin reportes."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$reportesZipName = Split-Path -Leaf $ReportsZipPath
|
||||||
|
$reportesEntryNames = Get-ZipEntryNames -Path $ReportsZipPath
|
||||||
|
if ($reportesEntryNames.Count -eq 0) {
|
||||||
|
throw "El ZIP de reportes esta vacio: $ReportsZipPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportesZipHasFolder = @(
|
||||||
|
$reportesEntryNames |
|
||||||
|
Where-Object {
|
||||||
|
$_ -eq "$reportesFolderName/" -or
|
||||||
|
$_ -like "$reportesFolderName/*"
|
||||||
|
}
|
||||||
|
).Count -gt 0
|
||||||
|
|
||||||
|
if (-not $reportesZipHasFolder) {
|
||||||
|
$reportesZipExtractMode = "into-folder"
|
||||||
|
Write-Step "El ZIP de reportes no contiene carpeta raiz. Se extraera dentro de $reportesFolderName."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteUploadPath = $Target.remoteUploadPath
|
||||||
|
$remoteAppPath = $Target.remoteAppPath
|
||||||
|
$backupDir = $Target.backupDir
|
||||||
|
$jasperstarterPath = "$remoteAppPath/_lib/libraries/grp/composer/vendor/geekcom/phpjasper/bin/jasperstarter/bin/jasperstarter"
|
||||||
|
$remoteTempPath = "$remoteUploadPath/gt_nova_deploy_tmp_$Version"
|
||||||
|
$remoteZipPath = "$remoteUploadPath/$zipFileName"
|
||||||
|
$remoteReportesZipPath = if ($reportesZipName) { "$remoteUploadPath/$reportesZipName" } else { "" }
|
||||||
|
$remoteExtractSource = if ($zipRootPrefix) { "$remoteTempPath/$zipRootPrefix" } else { $remoteTempPath }
|
||||||
|
|
||||||
|
$remoteAppArg = ConvertTo-ShellArg $remoteAppPath
|
||||||
|
$remoteTempArg = ConvertTo-ShellArg $remoteTempPath
|
||||||
|
$remoteZipArg = ConvertTo-ShellArg $remoteZipPath
|
||||||
|
$remoteExtractSourceArg = ConvertTo-ShellArg $remoteExtractSource
|
||||||
|
$remoteReportesFolderArg = ConvertTo-ShellArg "$remoteAppPath/$reportesFolderName"
|
||||||
|
$remoteReportesZipArg = if ($remoteReportesZipPath) { ConvertTo-ShellArg $remoteReportesZipPath } else { "" }
|
||||||
|
$jasperstarterArg = ConvertTo-ShellArg $jasperstarterPath
|
||||||
|
|
||||||
|
Write-Step "Probando conexion SSH..."
|
||||||
|
$connectionCheck = Invoke-Plink -Command "echo SSH_OK"
|
||||||
|
if ($connectionCheck -notmatch "SSH_OK") {
|
||||||
|
throw "No se pudo conectar por SSH. Salida: $connectionCheck"
|
||||||
|
}
|
||||||
|
Write-Success "Conexion SSH OK."
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||||
|
$backupFileName = "backup_gt_nova_${Version}_$timestamp.tar.gz"
|
||||||
|
$backupCommand = "mkdir -p $(ConvertTo-ShellArg $backupDir) && tar -czf $(ConvertTo-ShellArg "$backupDir/$backupFileName") -C /var/www/html $(Split-Path -Leaf $remoteAppPath) && echo BACKUP_OK"
|
||||||
|
|
||||||
|
Write-Step "Creando backup remoto..."
|
||||||
|
$backupOutput = Invoke-Plink -Command $backupCommand
|
||||||
|
if ($backupOutput -notmatch "BACKUP_OK") {
|
||||||
|
throw "Error al crear backup remoto. Salida: $backupOutput"
|
||||||
|
}
|
||||||
|
Write-Success "Backup creado: $backupDir/$backupFileName"
|
||||||
|
|
||||||
|
Write-Step "Subiendo ZIP principal..."
|
||||||
|
& $pscp -P $Target.port -pw $Target.password $ZipPath "$($Target.user)@$($Target.host):$remoteUploadPath/" 2>&1 | ForEach-Object { Write-Host $_ }
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Error al subir el ZIP principal."
|
||||||
|
}
|
||||||
|
Write-Success "ZIP principal subido."
|
||||||
|
|
||||||
|
$shouldUploadReportsZip = $needsReportesZip -and $hasReportsZipPath
|
||||||
|
if ($shouldUploadReportsZip) {
|
||||||
|
Write-Step "Subiendo ZIP auxiliar de reportes..."
|
||||||
|
& $pscp -P $Target.port -pw $Target.password $ReportsZipPath "$($Target.user)@$($Target.host):$remoteUploadPath/" 2>&1 | ForEach-Object { Write-Host $_ }
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Error al subir el ZIP de reportes."
|
||||||
|
}
|
||||||
|
Write-Success "ZIP de reportes subido."
|
||||||
|
}
|
||||||
|
|
||||||
|
$cmdReportes = ":"
|
||||||
|
if ($shouldUploadReportsZip -and $reportesZipExtractMode -eq "direct") {
|
||||||
|
$cmdReportes = "sudo unzip -oq $remoteReportesZipArg -d $remoteAppArg"
|
||||||
|
}
|
||||||
|
elseif ($shouldUploadReportsZip -and $reportesZipExtractMode -eq "into-folder") {
|
||||||
|
$cmdReportes = @"
|
||||||
|
sudo mkdir -p $remoteReportesFolderArg
|
||||||
|
sudo unzip -oq $remoteReportesZipArg -d $remoteReportesFolderArg
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Descomprimiendo y reemplazando aplicacion..."
|
||||||
|
$deployCommand = @"
|
||||||
|
set -e
|
||||||
|
sudo rm -rf $remoteTempArg
|
||||||
|
sudo mkdir -p $remoteTempArg $remoteAppArg
|
||||||
|
sudo unzip -q $remoteZipArg -d $remoteTempArg
|
||||||
|
sudo find $remoteAppArg -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||||
|
sudo cp -a $remoteExtractSourceArg/. $remoteAppArg/
|
||||||
|
$cmdReportes
|
||||||
|
sudo rm -rf $remoteTempArg
|
||||||
|
echo UNZIP_OK
|
||||||
|
"@
|
||||||
|
|
||||||
|
$deployOutput = Invoke-Plink -Command $deployCommand
|
||||||
|
if ($deployOutput -notmatch "UNZIP_OK") {
|
||||||
|
throw "Error al descomprimir o reemplazar aplicacion. Salida: $deployOutput"
|
||||||
|
}
|
||||||
|
Write-Success "Aplicacion reemplazada."
|
||||||
|
|
||||||
|
$serviceRestartLines = @()
|
||||||
|
foreach ($service in @($Target.services)) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($service)) {
|
||||||
|
$serviceRestartLines += "sudo systemctl restart $(ConvertTo-ShellArg $service)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanupReportes = if ($shouldUploadReportsZip) { "sudo rm -f $remoteReportesZipArg" } else { ":" }
|
||||||
|
$fixJasperstarterPermissions = ":"
|
||||||
|
if ($Target.host -eq "4.246.183.98") {
|
||||||
|
$fixJasperstarterPermissions = @"
|
||||||
|
if [ -f $jasperstarterArg ]; then
|
||||||
|
sudo chmod 755 $jasperstarterArg
|
||||||
|
ls -l $jasperstarterArg
|
||||||
|
else
|
||||||
|
echo JASPERSTARTER_NOT_FOUND
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
$postDeployCommand = @"
|
||||||
|
set -e
|
||||||
|
sudo chown -R www-data:www-data $remoteAppArg
|
||||||
|
sudo find $remoteAppArg -type d -exec chmod 755 {} \;
|
||||||
|
sudo find $remoteAppArg -type f -exec chmod 644 {} \;
|
||||||
|
$fixJasperstarterPermissions
|
||||||
|
$($serviceRestartLines -join "`n")
|
||||||
|
sudo rm -f $remoteZipArg
|
||||||
|
$cleanupReportes
|
||||||
|
echo DEPLOY_OK
|
||||||
|
"@
|
||||||
|
|
||||||
|
Write-Step "Aplicando permisos, reiniciando servicios y limpiando temporales..."
|
||||||
|
$postDeployOutput = Invoke-Plink -Command $postDeployCommand
|
||||||
|
if ($postDeployOutput -notmatch "DEPLOY_OK") {
|
||||||
|
throw "El deploy reemplazo archivos, pero fallo el post-deploy. Salida: $postDeployOutput"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target.host -eq "4.246.183.98") {
|
||||||
|
if ($postDeployOutput -notmatch "jasperstarter") {
|
||||||
|
throw "El deploy finalizo, pero no se pudo verificar el permiso de jasperstarter. Salida: $postDeployOutput"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Success "Permiso 755 aplicado a jasperstarter."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Success "Deploy finalizado correctamente para version $Version en $($Target.name)."
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
$str_apl = 'app_Login';
|
||||||
|
if(is_file("_lib/_app_data/" . $str_apl . '_ini.php'))
|
||||||
|
{
|
||||||
|
require("_lib/_app_data/" . $str_apl . '_ini.php');
|
||||||
|
$str_apl = $arr_data['friendly_url'];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$str_apl = $str_apl . '/';
|
||||||
|
}
|
||||||
|
header("Location: " . $str_apl);
|
||||||
|
|
||||||
|
?>
|
||||||
Generated
+6686
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "deploy-master",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Interfaz simple para desplegar GT ARIES por SSH.",
|
||||||
|
"main": "src/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"dist": "electron-builder --win dir"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^30.5.1",
|
||||||
|
"electron-builder": "^24.13.3"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.gt.deploymaster",
|
||||||
|
"productName": "Deploy Master",
|
||||||
|
"asar": false,
|
||||||
|
"files": [
|
||||||
|
"src/**/*",
|
||||||
|
"deploy.ps1",
|
||||||
|
"targets.json",
|
||||||
|
"plink.exe",
|
||||||
|
"pscp.exe",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "dir",
|
||||||
|
"signAndEditExecutable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
## **Guía paso a paso – Despliegue GT ARIES / GT NOVA**
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
|
#### Preparación
|
||||||
|
|
||||||
|
Antes de comenzar, asegurarse de contar con el paquete de despliegue y los scripts necesarios.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
El archivo comprimido que se utilizará para el despliegue debe estar en el mismo directorio donde se encuentran los scripts.
|
||||||
|
|
||||||
|
Este archivo debe renombrarse obligatoriamente a GT\_ARIES.zip, ya que los scripts esperan exactamente ese nombre para poder ejecutarse correctamente.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Procedimiento
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Paso 1. Instalar PuTTY (si no esta instalado)
|
||||||
|
|
||||||
|
Descargar e instalar el cliente SSH PuTTY en la máquina desde la cual se realizará el despliegue.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Paso 2. Verificar la presencia de plink
|
||||||
|
|
||||||
|
Dentro del mismo paquete de PuTTY se incluye Plink, que es la versión de línea de comandos del cliente SSH.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Copiar el archivo plink.exe dentro del directorio donde se encuentran los scripts de despliegue (.ps1).
|
||||||
|
|
||||||
|
Esto es necesario para que los scripts puedan ejecutar comandos remotos sobre el servidor.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Paso 3. Ejecutar el script de backup
|
||||||
|
|
||||||
|
Abrir una consola de Windows PowerShell.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Navegar hasta el directorio donde se encuentran los scripts y ejecutar:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.\\0001-backup\_gt\_nova.ps1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Este script realizará un respaldo previo del sistema en el servidor antes de iniciar el proceso de despliegue.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Paso 4. Ejecutar el script de despliegue
|
||||||
|
|
||||||
|
Una vez finalizado correctamente el backup, ejecutar el segundo script desde la misma consola:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.\\0002-deploy\_gt\_nova\_v2.ps1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Este script realizará el despliegue del paquete GT\_ARIES.zip en el servidor.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Paso 5. Verificar configuración de base de datos
|
||||||
|
|
||||||
|
Si durante el despliegue se instalaron nuevamente todas las librerías del sistema, será obligatorio reconfigurar la conexión con el servidor de base de datos.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Esto implica revisar y actualizar los parámetros de conexión (servidor, base de datos, usuario y credenciales) dentro de la configuración de la aplicación.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Resultado esperado
|
||||||
|
|
||||||
|
Luego de completar estos pasos, el sistema debería quedar desplegado en el servidor con el nuevo paquete y con la configuración de base de datos correctamente establecida.
|
||||||
|
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
const { app, BrowserWindow, dialog, ipcMain } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
let deployProcess = null;
|
||||||
|
|
||||||
|
function getAppRoot() {
|
||||||
|
return app.getAppPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 460,
|
||||||
|
height: 760,
|
||||||
|
minWidth: 420,
|
||||||
|
minHeight: 680,
|
||||||
|
backgroundColor: "#111820",
|
||||||
|
title: "Deploy Master",
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.setMenuBarVisibility(false);
|
||||||
|
mainWindow.loadFile(path.join(__dirname, "renderer", "index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTargets() {
|
||||||
|
const targetsPath = path.join(getAppRoot(), "targets.json");
|
||||||
|
const content = fs.readFileSync(targetsPath, "utf8");
|
||||||
|
const config = JSON.parse(content);
|
||||||
|
return Array.isArray(config.targets)
|
||||||
|
? config.targets.map(target => ({
|
||||||
|
id: target.id,
|
||||||
|
name: target.name,
|
||||||
|
host: target.host,
|
||||||
|
port: target.port,
|
||||||
|
remoteAppPath: target.remoteAppPath
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendDeployEvent(payload) {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("deploy:event", payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("targets:list", () => readTargets());
|
||||||
|
|
||||||
|
ipcMain.handle("dialog:zip", async (_, options = {}) => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
title: options.title || "Seleccionar ZIP",
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [{ name: "ZIP", extensions: ["zip"] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled || result.filePaths.length === 0) return null;
|
||||||
|
return result.filePaths[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("deploy:start", async (_, payload) => {
|
||||||
|
if (deployProcess) {
|
||||||
|
throw new Error("Ya hay un deploy en ejecucion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = String(payload.version || "").trim();
|
||||||
|
const zipPath = String(payload.zipPath || "").trim();
|
||||||
|
const reportsZipPath = String(payload.reportsZipPath || "").trim();
|
||||||
|
const targetId = String(payload.targetId || "").trim();
|
||||||
|
|
||||||
|
if (!version) throw new Error("Completa el numero de version.");
|
||||||
|
if (!zipPath) throw new Error("Selecciona el archivo GT_ARIES.zip.");
|
||||||
|
if (!targetId) throw new Error("Selecciona un destino.");
|
||||||
|
|
||||||
|
const scriptPath = path.join(getAppRoot(), "deploy.ps1");
|
||||||
|
const targetsPath = path.join(getAppRoot(), "targets.json");
|
||||||
|
const args = [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
scriptPath,
|
||||||
|
"-Version",
|
||||||
|
version,
|
||||||
|
"-ZipPath",
|
||||||
|
zipPath,
|
||||||
|
"-TargetId",
|
||||||
|
targetId,
|
||||||
|
"-TargetsPath",
|
||||||
|
targetsPath
|
||||||
|
];
|
||||||
|
|
||||||
|
if (reportsZipPath) {
|
||||||
|
args.push("-ReportsZipPath", reportsZipPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDeployEvent({ type: "started" });
|
||||||
|
|
||||||
|
deployProcess = spawn("powershell.exe", args, {
|
||||||
|
cwd: getAppRoot(),
|
||||||
|
windowsHide: true
|
||||||
|
});
|
||||||
|
|
||||||
|
deployProcess.stdout.on("data", chunk => {
|
||||||
|
sendDeployEvent({ type: "stdout", text: chunk.toString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
deployProcess.stderr.on("data", chunk => {
|
||||||
|
sendDeployEvent({ type: "stderr", text: chunk.toString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
deployProcess.on("error", error => {
|
||||||
|
sendDeployEvent({ type: "error", text: error.message });
|
||||||
|
deployProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
deployProcess.on("close", code => {
|
||||||
|
sendDeployEvent({ type: "finished", code });
|
||||||
|
deployProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("deploy:stop", async () => {
|
||||||
|
if (!deployProcess) return { ok: true };
|
||||||
|
deployProcess.kill();
|
||||||
|
deployProcess = null;
|
||||||
|
sendDeployEvent({ type: "finished", code: -1 });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("deployMaster", {
|
||||||
|
listTargets: () => ipcRenderer.invoke("targets:list"),
|
||||||
|
chooseZip: options => ipcRenderer.invoke("dialog:zip", options),
|
||||||
|
startDeploy: payload => ipcRenderer.invoke("deploy:start", payload),
|
||||||
|
stopDeploy: () => ipcRenderer.invoke("deploy:stop"),
|
||||||
|
onDeployEvent: callback => {
|
||||||
|
const listener = (_, payload) => callback(payload);
|
||||||
|
ipcRenderer.on("deploy:event", listener);
|
||||||
|
return () => ipcRenderer.removeListener("deploy:event", listener);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Deploy Master</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shell">
|
||||||
|
<header class="titlebar">
|
||||||
|
<div class="brand">
|
||||||
|
<svg class="rocket" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M14.6 3.2c2.1-.9 4.4-1 6.2-.7.3 1.8.2 4.1-.7 6.2-1 2.4-2.9 4.6-5.9 6.7l-1.7-1.7-2.6 2.6-2.2-2.2 2.6-2.6-1.7-1.7c2.1-3 4.3-4.9 6.7-5.8ZM16 8.5a1.7 1.7 0 1 0 3.4 0A1.7 1.7 0 0 0 16 8.5ZM6.4 15.9l1.7 1.7c-.8 1.8-2.5 3.3-5.1 4 .7-2.6 2.2-4.3 4-5.1ZM4.6 9.7c1.2-2.3 2.7-3.9 4.8-4.8-.9 1.2-1.7 2.5-2.4 4L4.6 9.7Zm9.7 9.7.8-2.4c1.5-.7 2.8-1.5 4-2.4-.9 2.1-2.5 3.6-4.8 4.8Z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h1>Deploy Master</h1>
|
||||||
|
<span>v1.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="section-title">
|
||||||
|
<span>Deploy Configuration</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>Version a subir</span>
|
||||||
|
<input id="version" type="text" placeholder="1.7.3" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<span>GT_ARIES.zip</span>
|
||||||
|
<div class="file-row">
|
||||||
|
<input id="zip-path" type="text" placeholder="Seleccionar ZIP principal" readonly>
|
||||||
|
<button id="choose-zip" class="icon-button" type="button" title="Seleccionar GT_ARIES.zip">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H11l2 2h4.5A2.5 2.5 0 0 1 20 7.5v11A2.5 2.5 0 0 1 17.5 21h-11A2.5 2.5 0 0 1 4 18.5v-13Zm2.5-.5a.5.5 0 0 0-.5.5v13c0 .3.2.5.5.5h11c.3 0 .5-.2.5-.5v-11c0-.3-.2-.5-.5-.5h-5.3l-2-2H6.5Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<span>reportes_subidos_jasper.zip</span>
|
||||||
|
<div class="file-row">
|
||||||
|
<input id="reports-path" type="text" placeholder="Opcional" readonly>
|
||||||
|
<button id="choose-reports" class="icon-button" type="button" title="Seleccionar reportes">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H11l2 2h4.5A2.5 2.5 0 0 1 20 7.5v11A2.5 2.5 0 0 1 17.5 21h-11A2.5 2.5 0 0 1 4 18.5v-13Zm2.5-.5a.5.5 0 0 0-.5.5v13c0 .3.2.5.5.5h11c.3 0 .5-.2.5-.5v-11c0-.3-.2-.5-.5-.5h-5.3l-2-2H6.5Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="clear-reports" class="icon-button" type="button" title="Quitar reportes">x</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>Destino SSH</span>
|
||||||
|
<select id="target"></select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="steps" class="steps">
|
||||||
|
<div class="step pending" data-step="validate">
|
||||||
|
<div class="step-dot"></div>
|
||||||
|
<div>Validar</div>
|
||||||
|
</div>
|
||||||
|
<div class="step pending" data-step="backup">
|
||||||
|
<div class="step-dot"></div>
|
||||||
|
<div>Backup</div>
|
||||||
|
</div>
|
||||||
|
<div class="step pending" data-step="upload">
|
||||||
|
<div class="step-dot"></div>
|
||||||
|
<div>Subir</div>
|
||||||
|
</div>
|
||||||
|
<div class="step pending" data-step="deploy">
|
||||||
|
<div class="step-dot"></div>
|
||||||
|
<div>Deploy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="run-deploy" class="primary-button" type="button">Ejecutar Deploy</button>
|
||||||
|
<button id="stop-deploy" class="secondary-button hidden" type="button">Detener</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="console">
|
||||||
|
<div class="console-head">
|
||||||
|
<span>Consola de Logs</span>
|
||||||
|
<button id="clear-logs" class="clear-button" type="button" aria-label="Limpiar consola">x</button>
|
||||||
|
</div>
|
||||||
|
<pre id="logs"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
const state = {
|
||||||
|
running: false,
|
||||||
|
logs: [],
|
||||||
|
stepStatus: {
|
||||||
|
validate: "pending",
|
||||||
|
backup: "pending",
|
||||||
|
upload: "pending",
|
||||||
|
deploy: "pending"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionEl = document.getElementById("version");
|
||||||
|
const zipPathEl = document.getElementById("zip-path");
|
||||||
|
const reportsPathEl = document.getElementById("reports-path");
|
||||||
|
const targetEl = document.getElementById("target");
|
||||||
|
const chooseZipButton = document.getElementById("choose-zip");
|
||||||
|
const chooseReportsButton = document.getElementById("choose-reports");
|
||||||
|
const clearReportsButton = document.getElementById("clear-reports");
|
||||||
|
const runButton = document.getElementById("run-deploy");
|
||||||
|
const stopButton = document.getElementById("stop-deploy");
|
||||||
|
const clearLogsButton = document.getElementById("clear-logs");
|
||||||
|
const logsEl = document.getElementById("logs");
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
bindEvents();
|
||||||
|
await loadTargets();
|
||||||
|
resetSteps();
|
||||||
|
log("INFO", "Deploy Master listo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
chooseZipButton.addEventListener("click", async () => {
|
||||||
|
const path = await window.deployMaster.chooseZip({ title: "Seleccionar GT_ARIES.zip" });
|
||||||
|
if (path) zipPathEl.value = path;
|
||||||
|
});
|
||||||
|
|
||||||
|
chooseReportsButton.addEventListener("click", async () => {
|
||||||
|
const path = await window.deployMaster.chooseZip({ title: "Seleccionar reportes_subidos_jasper.zip" });
|
||||||
|
if (path) reportsPathEl.value = path;
|
||||||
|
});
|
||||||
|
|
||||||
|
clearReportsButton.addEventListener("click", () => {
|
||||||
|
reportsPathEl.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
runButton.addEventListener("click", runDeploy);
|
||||||
|
stopButton.addEventListener("click", stopDeploy);
|
||||||
|
|
||||||
|
clearLogsButton.addEventListener("click", () => {
|
||||||
|
state.logs = [];
|
||||||
|
renderLogs();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.deployMaster.onDeployEvent(handleDeployEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTargets() {
|
||||||
|
try {
|
||||||
|
const targets = await window.deployMaster.listTargets();
|
||||||
|
targetEl.innerHTML = targets
|
||||||
|
.map(target => `<option value="${escapeHtml(target.id)}">${escapeHtml(target.name)} - ${escapeHtml(target.host)}:${escapeHtml(String(target.port))}</option>`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
targetEl.innerHTML = '<option value="">Sin destinos configurados</option>';
|
||||||
|
log("WARN", "No hay destinos en targets.json.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
targetEl.innerHTML = '<option value="">Error leyendo destinos</option>';
|
||||||
|
log("ERROR", error.message || "No se pudieron cargar los destinos.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDeploy() {
|
||||||
|
if (state.running) return;
|
||||||
|
|
||||||
|
resetSteps();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
version: versionEl.value,
|
||||||
|
zipPath: zipPathEl.value,
|
||||||
|
reportsZipPath: reportsPathEl.value,
|
||||||
|
targetId: targetEl.value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setRunning(true);
|
||||||
|
log("INFO", "Deploy iniciado...");
|
||||||
|
setStep("validate", "running");
|
||||||
|
await window.deployMaster.startDeploy(payload);
|
||||||
|
} catch (error) {
|
||||||
|
setStep("validate", "error");
|
||||||
|
setRunning(false);
|
||||||
|
log("ERROR", error.message || "No se pudo iniciar el deploy.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopDeploy() {
|
||||||
|
await window.deployMaster.stopDeploy();
|
||||||
|
log("WARN", "Deploy detenido por el usuario.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeployEvent(event) {
|
||||||
|
if (event.type === "stdout" || event.type === "stderr") {
|
||||||
|
appendProcessOutput(event.text, event.type === "stderr");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "error") {
|
||||||
|
setCurrentStepError();
|
||||||
|
log("ERROR", event.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "finished") {
|
||||||
|
if (event.code === 0) {
|
||||||
|
Object.keys(state.stepStatus).forEach(step => setStep(step, "done"));
|
||||||
|
log("SUCCESS", "Proceso finalizado correctamente.");
|
||||||
|
} else if (event.code !== -1) {
|
||||||
|
setCurrentStepError();
|
||||||
|
log("ERROR", `El proceso termino con codigo ${event.code}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRunning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendProcessOutput(text, isError) {
|
||||||
|
const lines = text.split(/\r?\n/).filter(Boolean);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const level = detectLevel(line, isError);
|
||||||
|
log(level, line);
|
||||||
|
updateStepsFromLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectLevel(line, isError) {
|
||||||
|
if (isError) return "ERROR";
|
||||||
|
if (line.includes("[SUCCESS]")) return "SUCCESS";
|
||||||
|
if (line.includes("[WARN]")) return "WARN";
|
||||||
|
if (line.toLowerCase().includes("error")) return "ERROR";
|
||||||
|
return "INFO";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStepsFromLine(line) {
|
||||||
|
if (line.includes("Validando estructura")) {
|
||||||
|
setStep("validate", "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes("Conexion SSH OK")) {
|
||||||
|
setStep("validate", "done");
|
||||||
|
setStep("backup", "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes("Backup creado")) {
|
||||||
|
setStep("backup", "done");
|
||||||
|
setStep("upload", "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes("ZIP principal subido")) {
|
||||||
|
setStep("upload", "done");
|
||||||
|
setStep("deploy", "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes("Aplicacion reemplazada")) {
|
||||||
|
setStep("deploy", "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes("Deploy finalizado correctamente")) {
|
||||||
|
setStep("deploy", "done");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.toLowerCase().includes("error") || line.includes("Exception")) {
|
||||||
|
setCurrentStepError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSteps() {
|
||||||
|
Object.keys(state.stepStatus).forEach(step => {
|
||||||
|
state.stepStatus[step] = "pending";
|
||||||
|
});
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStep(step, status) {
|
||||||
|
state.stepStatus[step] = status;
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentStepError() {
|
||||||
|
const runningStep = Object.keys(state.stepStatus).find(step => state.stepStatus[step] === "running");
|
||||||
|
if (runningStep) setStep(runningStep, "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSteps() {
|
||||||
|
document.querySelectorAll("[data-step]").forEach(element => {
|
||||||
|
const status = state.stepStatus[element.dataset.step] || "pending";
|
||||||
|
element.classList.remove("pending", "running", "done", "error");
|
||||||
|
element.classList.add(status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRunning(isRunning) {
|
||||||
|
state.running = isRunning;
|
||||||
|
runButton.disabled = isRunning;
|
||||||
|
versionEl.disabled = isRunning;
|
||||||
|
chooseZipButton.disabled = isRunning;
|
||||||
|
chooseReportsButton.disabled = isRunning;
|
||||||
|
clearReportsButton.disabled = isRunning;
|
||||||
|
targetEl.disabled = isRunning;
|
||||||
|
stopButton.classList.toggle("hidden", !isRunning);
|
||||||
|
runButton.textContent = isRunning ? "Ejecutando..." : "Ejecutar Deploy";
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(level, message) {
|
||||||
|
const normalizedLevel = level === "WARN" ? "WARNING" : level;
|
||||||
|
state.logs.push({ level: normalizedLevel, line: message });
|
||||||
|
renderLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLogs() {
|
||||||
|
logsEl.innerHTML = state.logs
|
||||||
|
.map(entry => {
|
||||||
|
const className = entry.level === "SUCCESS"
|
||||||
|
? "log-success"
|
||||||
|
: entry.level === "ERROR"
|
||||||
|
? "log-error"
|
||||||
|
: entry.level === "WARNING"
|
||||||
|
? "log-warning"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `<span class="${className}">${escapeHtml(entry.line)}</span>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
logsEl.scrollTop = logsEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #111820;
|
||||||
|
--panel: #20303f;
|
||||||
|
--panel-2: #192633;
|
||||||
|
--line: #425569;
|
||||||
|
--text: #eef5ff;
|
||||||
|
--muted: #9aaabd;
|
||||||
|
--green: #36d787;
|
||||||
|
--blue: #3f9dff;
|
||||||
|
--error: #ff6b6b;
|
||||||
|
--warning: #ffd166;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 420px;
|
||||||
|
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 0%, rgba(73, 130, 190, 0.28), transparent 28%),
|
||||||
|
linear-gradient(180deg, #182230 0%, #111820 100%);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar {
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: linear-gradient(180deg, #253244 0%, #172232 100%);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand h1 {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand span {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rocket {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
fill: #a9bdd2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin: 14px 12px;
|
||||||
|
padding: 15px 17px 16px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
|
border-radius: 8px;
|
||||||
|
background:
|
||||||
|
linear-gradient(145deg, rgba(42, 60, 78, 0.92), rgba(25, 38, 51, 0.94)),
|
||||||
|
var(--panel);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 6px 18px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #f5f8fc;
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, #7b8da1, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
margin-bottom: 13px;
|
||||||
|
color: #d9e6f2;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field > span {
|
||||||
|
color: #b5c5d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||||
|
border-radius: 7px;
|
||||||
|
color: #edf4fa;
|
||||||
|
background: rgba(30, 46, 61, 0.92);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 0 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 0 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: rgba(91, 178, 255, 0.85);
|
||||||
|
box-shadow: 0 0 0 3px rgba(63, 157, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 42px;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row:has(#clear-reports) {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 42px 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button,
|
||||||
|
.clear-button {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 38px;
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||||
|
border-radius: 7px;
|
||||||
|
color: #edf4fa;
|
||||||
|
background: rgba(30, 46, 61, 0.78);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button svg {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
fill: #d8e4ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin: 16px 3px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-height: 70px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.12;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: calc(50% + 21px);
|
||||||
|
width: calc(100% - 14px);
|
||||||
|
height: 3px;
|
||||||
|
background: var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:nth-child(4)::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-dot {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 37px;
|
||||||
|
height: 37px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #5b6470;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-dot::before {
|
||||||
|
content: "";
|
||||||
|
width: 13px;
|
||||||
|
height: 7px;
|
||||||
|
border-left: 3px solid currentColor;
|
||||||
|
border-bottom: 3px solid currentColor;
|
||||||
|
transform: rotate(-45deg) translate(1px, -1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.pending .step-dot::before {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.38;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.running,
|
||||||
|
.step.done {
|
||||||
|
color: #eefaf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.running .step-dot,
|
||||||
|
.step.done .step-dot {
|
||||||
|
background: linear-gradient(180deg, #46d98b, #23b86a);
|
||||||
|
color: #153927;
|
||||||
|
box-shadow: 0 0 14px rgba(54, 215, 135, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.running .step-dot {
|
||||||
|
animation: pulse 900ms ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.error .step-dot {
|
||||||
|
background: var(--error);
|
||||||
|
color: #391414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.error .step-dot::before {
|
||||||
|
width: 16px;
|
||||||
|
height: 3px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
box-shadow: 0 0 0 0 currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.done::after,
|
||||||
|
.step.running::after {
|
||||||
|
background: var(--green);
|
||||||
|
box-shadow: 0 0 8px rgba(54, 215, 135, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button,
|
||||||
|
.secondary-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 45px;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
border: 1px solid rgba(180, 220, 255, 0.38);
|
||||||
|
background: linear-gradient(180deg, #5bb2ff 0%, #267de1 100%);
|
||||||
|
box-shadow: 0 0 12px rgba(63, 157, 255, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
margin-top: 9px;
|
||||||
|
border: 1px solid rgba(255, 107, 107, 0.44);
|
||||||
|
background: rgba(86, 38, 44, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:disabled,
|
||||||
|
.secondary-button:disabled {
|
||||||
|
opacity: 0.58;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
background: #1b1f22;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px 0 14px;
|
||||||
|
color: #d4d7da;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-head::before {
|
||||||
|
content: "";
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 6px solid #aab0b5;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-head span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
width: 26px;
|
||||||
|
min-width: 26px;
|
||||||
|
min-height: 26px;
|
||||||
|
border: 0;
|
||||||
|
color: #b7bdc3;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logs {
|
||||||
|
min-height: 190px;
|
||||||
|
max-height: 270px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 14px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #d7d7d7;
|
||||||
|
font: 12px/1.35 Consolas, "Courier New", monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-success {
|
||||||
|
color: #63dd8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-error {
|
||||||
|
color: #ff8585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-warning {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"id": "example-target",
|
||||||
|
"name": "Example Target",
|
||||||
|
"host": "example.com",
|
||||||
|
"port": 22,
|
||||||
|
"user": "deploy-user",
|
||||||
|
"password": "change-me",
|
||||||
|
"remoteUploadPath": "/home/deploy-user",
|
||||||
|
"remoteAppPath": "/var/www/html/app",
|
||||||
|
"backupDir": "/home/deploy-user/backups",
|
||||||
|
"services": ["php8.1-fpm", "nginx"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user