deploy_skill: soporte para Coolify v4.1.2 (API /applications 404) + flujo UI Playwright

Aprendido desplegando cotizador (Next.js+FastAPI+Postgres, Docker Compose,
repo privado) en coolify.urieljareth.org (v4.1.2):

- Documenta que /applications/* devuelve 404 en esta instancia y que configurar
  apps git build-from-source solo es posible por la UI web.
- Nuevos scripts scripts/coolify-ui/: coolify-login.mjs (guarda storageState) y
  Configure-CoolifyComposeApp.mjs (build pack -> Docker Compose, Reload Compose
  File, env vars por Developer view, dominios por servicio).
- references/coolify-4.1.2-notes.md: mapa de API funcional/404, gotchas (BOM
  UTF-8 rompe el parser YAML de Coolify; Reload Compose File obligatorio; pin de
  dominios por servicio o 503; no encolar deploys concurrentes; PowerShell
  [IO.File]::ReadAllText para payloads de llaves).
- env.local.template.ps1: COOLIFY_EMAIL/COOLIFY_PASSWORD para la UI.
- SKILL.md/TOOLS.md: seccion "instance reality" y flujo UI paso a paso.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
urieljareth
2026-07-08 00:42:12 -06:00
co-authored by Claude Opus 4.8
parent cd998ce6b0
commit e7d1c33cd5
22 changed files with 2935 additions and 0 deletions
@@ -0,0 +1,241 @@
<#
.SYNOPSIS
Create or update a Coolify application from a Git repository and deploy it.
.DESCRIPTION
Orchestration wrapper around Invoke-CoolifyApi.ps1 that performs the full
Coolify onboarding flow for an existing Git repository:
1. Resolve or create the target Project (by name).
2. Resolve or create the target Environment (by name within the project).
3. Resolve the local server UUID (first reachable server).
4. Create the application (public repo by default, or via deploy key).
5. Set the FQDN and ports_exposes on the new app.
6. Push env vars (key=value pairs) onto the application.
7. Trigger an initial deploy and return the deployment UUIDs.
Each mutating step asks for explicit confirmation unless -Force is supplied.
The script NEVER writes secrets to stdout: env values are sent to the API
but not echoed.
.PARAMETER RepoUrl
HTTPS clone URL of the Git repository (e.g. https://github.com/user/repo).
.PARAMETER Branch
Branch to deploy. Defaults to 'main'.
.PARAMETER Fqdn
Public URL Coolify will expose, e.g. https://myapp.urieljareth.org.
.PARAMETER ProjectName
Coolify project name. If it does not exist, it will be created. Default: 'default'.
.PARAMETER EnvironmentName
Coolify environment name within the project. Default: 'production'.
.PARAMETER PortsExposes
Internal port the app listens on (Traefik targets this). Default: '3000'.
.PARAMETER BuildPack
Optional Coolify build pack override (nixpacks, dockerfile, dockercompose).
.PARAMETER IsStatic
Switch. Marks the app as a static site (Coolify will use a static_image).
.PARAMETER EnvFile
Optional path to a .env-formatted file. Each KEY=VALUE line is pushed to
Coolify as an environment variable. Lines starting with '#' are ignored.
.PARAMETER ApplicationUuid
Optional. If the app already exists in Coolify, pass its UUID to update it
in place (FQDN, ports, env) and redeploy, instead of creating a new one.
.PARAMETER Force
Skip per-step confirmation prompts. Use only after validating the plan.
.EXAMPLE
$envs = "./.env.coolify"
.\New-CoolifyApplication.ps1 -RepoUrl https://github.com/urieljarethbusiness-cpu/my-app `
-Fqdn https://myapp.urieljareth.org -PortsExposes 8080 -EnvFile $envs -Force
#>
param(
[Parameter(Mandatory = $true, ParameterSetName = "Create")]
[string]$RepoUrl,
[string]$Branch = "main",
[Parameter(Mandatory = $true)]
[string]$Fqdn,
[string]$ProjectName = "default",
[string]$EnvironmentName = "production",
[string]$PortsExposes = "3000",
[string]$BuildPack,
[switch]$IsStatic,
[string]$EnvFile,
[Parameter(ParameterSetName = "Update")]
[string]$ApplicationUuid,
[switch]$Force
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$coolifyApi = Join-Path $here "..\..\coolify_skill\scripts\Invoke-CoolifyApi.ps1"
if (-not (Test-Path -LiteralPath $coolifyApi)) {
throw "Invoke-CoolifyApi.ps1 not found at: $coolifyApi"
}
function Confirm-Step {
param([string]$Action)
if ($Force) { return $true }
$reply = Read-Host "Confirm action: $Action`nProceed? (y/N)"
return ($reply -match '^(y|yes|s|si|sí)$')
}
function Get-CoolifyResource {
param([string]$Path)
& $coolifyApi -Path $Path -ErrorAction Stop
}
function Resolve-Project {
param([string]$Name)
$projects = Get-CoolifyResource -Path "/projects"
$match = $projects | Where-Object { $_.name -eq $Name } | Select-Object -First 1
if (-not $match) {
if (-not (Confirm-Step "Create project '$Name' in Coolify")) {
throw "Aborted by user before creating project."
}
$body = @{ name = $Name } | ConvertTo-Json -Compress
$created = & $coolifyApi -Method POST -Path "/projects" -BodyJson $body -ErrorAction Stop
Write-Host "Project created: $($created.uuid)" -ForegroundColor Green
return $created
}
return $match
}
function Resolve-Environment {
param([string]$ProjectUuid, [string]$Name)
$envs = Get-CoolifyResource -Path "/projects/$ProjectUuid/environments"
$match = $envs | Where-Object { $_.name -eq $Name } | Select-Object -First 1
if (-not $match) {
if (-not (Confirm-Step "Create environment '$Name' in project $ProjectUuid")) {
throw "Aborted by user before creating environment."
}
$body = @{ name = $Name } | ConvertTo-Json -Compress
$created = & $coolifyApi -Method POST -Path "/projects/$ProjectUuid/environments" -BodyJson $body -ErrorAction Stop
Write-Host "Environment created: $($created.uuid)" -ForegroundColor Green
return $created
}
return $match
}
function Resolve-ServerUuid {
$servers = Get-CoolifyResource -Path "/servers"
if (-not $servers) { throw "No Coolify servers configured." }
$srv = $servers | Where-Object { $_.settings.is_usable -eq $true } | Select-Object -First 1
if (-not $srv) { $srv = $servers | Select-Object -First 1 }
return $srv.uuid
}
function Resolve-AppUuid {
param([string]$Uuid)
if ($Uuid) {
$app = Get-CoolifyResource -Path "/applications/$Uuid"
return @{ uuid = $app.uuid; created = $false }
}
return $null
}
Write-Host "Resolving Coolify project '$ProjectName'..." -ForegroundColor Cyan
$proj = Resolve-Project -Name $ProjectName
Write-Host "Resolving environment '$EnvironmentName'..." -ForegroundColor Cyan
$env = Resolve-Environment -ProjectUuid $proj.uuid -Name $EnvironmentName
Write-Host "Resolving server..." -ForegroundColor Cyan
$serverUuid = Resolve-ServerUuid
Write-Host "Using server: $serverUuid" -ForegroundColor Cyan
$existing = Resolve-AppUuid -Uuid $ApplicationUuid
if ($existing) {
Write-Host "Updating existing application: $($existing.uuid)" -ForegroundColor Cyan
$appUuid = $existing.uuid
$patch = [ordered]@{
domains = $Fqdn
ports_exposes = $PortsExposes
}
if ($BuildPack) { $patch.build_pack = $BuildPack }
if (-not (Confirm-Step "PATCH /applications/$appUuid (domain=$Fqdn, ports=$PortsExposes)")) {
throw "Aborted by user."
}
& $coolifyApi -Method PATCH -Path "/applications/$appUuid" -BodyJson ($patch | ConvertTo-Json -Compress) -ErrorAction Stop | Out-Null
} else {
$createBody = [ordered]@{
project_uuid = $proj.uuid
environment_name = $env.name
server_uuid = $serverUuid
git_repository = $RepoUrl
git_branch = $Branch
ports_exposes = $PortsExposes
domains = $Fqdn
name = Split-Path -Leaf ($RepoUrl -replace '\.git$', '')
is_static = [bool]$IsStatic
instant_deploy = $false
}
if ($BuildPack) { $createBody.build_pack = $BuildPack }
if (-not (Confirm-Step "POST /applications/public with repo=$RepoUrl, branch=$Branch, fqdn=$Fqdn, ports=$PortsExposes")) {
throw "Aborted by user."
}
$created = & $coolifyApi -Method POST -Path "/applications/public" -BodyJson ($createBody | ConvertTo-Json -Compress) -ErrorAction Stop
$appUuid = $created.uuid
Write-Host "Application created: $appUuid" -ForegroundColor Green
}
if ($EnvFile -and (Test-Path -LiteralPath $EnvFile)) {
if (-not (Confirm-Step "Push env vars from $EnvFile to application $appUuid")) {
Write-Host "Skipping env push." -ForegroundColor Yellow
} else {
Get-Content -LiteralPath $EnvFile | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith("#")) { return }
$eq = $line.IndexOf("=")
if ($eq -le 0) { return }
$k = $line.Substring(0, $eq).Trim()
$v = $line.Substring($eq + 1).Trim().Trim('"').Trim("'")
$body = [ordered]@{
key = $k
value = $v
is_literal = $true
is_shown_once = $false
} | ConvertTo-Json -Compress
& $coolifyApi -Method POST -Path "/applications/$appUuid/envs" -BodyJson $body -ErrorAction Stop | Out-Null
Write-Host " env: $k set" -ForegroundColor DarkGray
}
Write-Host "Env vars pushed." -ForegroundColor Green
}
}
if (-not (Confirm-Step "Trigger DEPLOY of $appUuid")) {
Write-Host "Skipping deploy trigger. Application is created but not deployed." -ForegroundColor Yellow
return
}
$deployResult = & $coolifyApi -Method POST -Path "/deploy" -BodyJson (@{ uuid = $appUuid } | ConvertTo-Json -Compress) -ErrorAction Stop
Write-Host "Deploy triggered." -ForegroundColor Green
$deployResult
[PSCustomObject]@{
project_uuid = $proj.uuid
environment_name = $env.name
server_uuid = $serverUuid
application_uuid = $appUuid
fqdn = $Fqdn
repo = $RepoUrl
branch = $Branch
} | Format-List