Files
urieljarethandClaude Opus 4.8 e7d1c33cd5 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]>
2026-07-08 00:42:12 -06:00

178 lines
7.0 KiB
PowerShell

<#
.SYNOPSIS
Validate a project directory against the Coolify hard rules before pushing.
.DESCRIPTION
Implements the pre-deploy checklist from docs/AGENTS-coolify-apps.md as an
automated gate. Run this BEFORE pushing to GitHub and BEFORE creating the
Coolify application. Reports each rule as PASS / WARN / FAIL with the
concrete fix for any failure.
The script does NOT modify files. It only inspects.
.PARAMETER Path
Project directory to validate. Defaults to the current directory.
.PARAMETER ExpectedPort
Port the app is expected to listen on. Used to cross-check against
ports_exposes if a compose file declares it, and against Dockerfile EXPOSE.
.PARAMETER Strict
Treat WARN as failures (exit code 1). Default: only FAIL fails.
.EXAMPLE
.\Test-PreDeployChecklist.ps1 -Path .\my-app -ExpectedPort 8080
#>
param(
[string]$Path = ".",
[string]$ExpectedPort,
[switch]$Strict
)
$ErrorActionPreference = "Stop"
$root = (Resolve-Path -LiteralPath $Path).Path
$results = New-Object System.Collections.Generic.List[object]
function Add-Result {
param([string]$Id, [string]$Status, [string]$Message, [string]$Fix = "")
$results.Add([PSCustomObject]@{
Id = $Id
Status = $Status
Message = $Message
Fix = $Fix
})
}
$composeFiles = @("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml") |
ForEach-Object { Join-Path $root $_ } |
Where-Object { Test-Path -LiteralPath $_ }
$dockerfile = @("Dockerfile", "Dockerfile.dev") |
ForEach-Object { Join-Path $root $_ } |
Where-Object { Test-Path -LiteralPath $_ } |
Select-Object -First 1
# Rule 2.1 - DB host is service name, not localhost
foreach ($cf in $composeFiles) {
$txt = Get-Content -LiteralPath $cf -Raw
if ($txt -match 'localhost|127\.0\.0\.1') {
$svc = ($txt -split "`n" | Select-String -Pattern 'localhost|127\.0\.0\.1' | Select-Object -First 1)
Add-Result "2.1-sibling-dns" "WARN" "$cf references localhost/127.0.0.1; if it is a DB/cache host for a sibling service, use the Docker service name." "Replace localhost with the sibling service name (see AGENTS-coolify-apps.md 2.1)."
}
}
if ($results | Where-Object Id -eq '2.1-sibling-dns') {
# already added
} else {
Add-Result "2.1-sibling-dns" "PASS" "No localhost/127.0.0.1 reference detected in compose files."
}
# Rule 2.2 - coolify external network if app references shared services
foreach ($cf in $composeFiles) {
$txt = Get-Content -LiteralPath $cf -Raw
if (($txt -match 'postgres|redis|mysql|mariadb|mongodb') -and ($txt -notmatch '(?ms)networks:.*?coolify:.*?external:\s*true')) {
Add-Result "2.2-coolify-network" "WARN" "$cf references a DB/cache but does not declare the external 'coolify' network." "If the app must reach shared Coolify services, add the external coolify network (see AGENTS-coolify-apps.md 2.2)."
}
}
if (-not ($results | Where-Object Id -eq '2.2-coolify-network')) {
Add-Result "2.2-coolify-network" "PASS" "No external coolify network requirement detected, or it is correctly declared."
}
# Rule 2.3 - no published 80/443
$badPorts = $false
foreach ($cf in $composeFiles) {
$txt = Get-Content -LiteralPath $cf -Raw
if ($txt -match '(?m)^\s*-\s*"?80:80"?|^\s*-\s*"?443:443"?|"80:80"|"443:443"|ports_exposes.*\b80\b.*\b443\b') {
Add-Result "2.3-no-80-443-publish" "FAIL" "$cf publishes host ports 80 or 443 (owned by coolify-proxy)." "Remove the host port mapping; let Traefik route (AGENTS-coolify-apps.md 2.3)."
$badPorts = $true
}
}
if (-not $badPorts) {
Add-Result "2.3-no-80-443-publish" "PASS" "No host 80/443 publishing found."
}
# Rule 2.4 - FQDN hint (informational)
Add-Result "2.4-fqdn-set" "INFO" "Remember to set FQDN to https://<name>.urieljareth.org BEFORE first deploy (not via Coolify's auto UUID subdomain)." "Set -Fqdn when calling New-CoolifyApplication.ps1."
# Rule 2.5 - no obvious secrets in tree
$secretPatterns = @(
'AKIA[0-9A-Z]{16}',
'ghp_[A-Za-z0-9]{36}',
'github_pat_[A-Za-z0-9_]{82}',
'-----BEGIN (RSA |EC |OPENSSH |)PRIVATE KEY-----',
'postgres(ql)?://[^:\s]+:[^@\s]+@'
)
$found = $false
Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|vendor|dist|build)\\' } |
ForEach-Object {
try {
$content = Get-Content -LiteralPath $_.FullName -Raw -ErrorAction Stop
foreach ($pat in $secretPatterns) {
if ($content -match $pat) {
Add-Result "2.5-no-secrets-in-repo" "FAIL" "Potential secret matching pattern '$pat' found in $($_.FullName)." "Remove the secret and inject it as env via Coolify (AGENTS-coolify-apps.md 2.5)."
$found = $true
}
}
} catch {}
}
if (-not $found) {
Add-Result "2.5-no-secrets-in-repo" "PASS" "No obvious hardcoded secrets detected."
}
# Rule 4 - healthcheck present
$hcFound = $false
foreach ($cf in $composeFiles) {
$txt = Get-Content -LiteralPath $cf -Raw
if ($txt -match 'healthcheck:') { $hcFound = $true }
}
if ($dockerfile) {
$txt = Get-Content -LiteralPath $dockerfile -Raw
if ($txt -match 'HEALTHCHECK') { $hcFound = $true }
}
if ($hcFound) {
Add-Result "4-healthcheck" "PASS" "Healthcheck detected."
} else {
Add-Result "4-healthcheck" "WARN" "No healthcheck found in compose or Dockerfile." "Add a healthcheck on a DB-independent endpoint (AGENTS-coolify-apps.md section 4)."
}
# Rule 5 - named volumes for persistence
foreach ($cf in $composeFiles) {
$txt = Get-Content -LiteralPath $cf -Raw
if ($txt -match 'volumes:' -and $txt -notmatch '(?ms)^volumes:\s*\n\s+\S+:') {
Add-Result "5-named-volumes" "WARN" "$cf has volumes but no top-level named volumes section." "Declare named volumes to survive redeploys (AGENTS-coolify-apps.md section 5)."
}
}
if (-not ($results | Where-Object Id -eq '5-named-volumes')) {
Add-Result "5-named-volumes" "PASS" "Volume declarations look consistent."
}
# Cross-check ExpectedPort vs Dockerfile EXPOSE
if ($ExpectedPort -and $dockerfile) {
$txt = Get-Content -LiteralPath $dockerfile -Raw
if ($txt -match 'EXPOSE\s+(\d+)') {
$exposed = $matches[1]
if ($exposed -ne $ExpectedPort) {
Add-Result "ports-match" "WARN" "Dockerfile EXPOSE=$exposed but ExpectedPort=$ExpectedPort." "Make ports_exposes match the real listening port."
} else {
Add-Result "ports-match" "PASS" "Dockerfile EXPOSE matches ExpectedPort ($ExpectedPort)."
}
}
}
$results | Format-Table -AutoSize
$failed = $results | Where-Object Status -eq 'FAIL'
$warned = $results | Where-Object Status -eq 'WARN'
if ($failed) {
Write-Host "`nFAIL: $($failed.Count) hard rule(s) violated. Fix before pushing." -ForegroundColor Red
exit 1
}
if ($warned) {
Write-Host "`nWARN: $($warned.Count) soft warning(s). Review before deploy." -ForegroundColor Yellow
if ($Strict) { exit 1 }
}
Write-Host "`nChecklist complete." -ForegroundColor Green