From c2e2590cbd28ad6ba4b01730225571044bbefaee Mon Sep 17 00:00:00 2001 From: "devops (jokerserver)" Date: Fri, 28 Aug 2026 17:10:14 +0000 Subject: [PATCH 1/5] fix(ci): poll de previews por estado de deployment, no app-status El app-status 'exited:unhealthy' es el placeholder pre-start en apps dockerimage recien creadas: el poll anterior abortaba antes de que /start siquiera despachara. Ahora se consulta GET /deployments/applications/{uuid} (queued/in progress/finished/ failed) y solo al 'finished' se exige app-status running. Run de evidencia: 33192352373. --- .github/workflows/pr-deploy-coolify.yml | 52 +++++++++++++++---------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pr-deploy-coolify.yml b/.github/workflows/pr-deploy-coolify.yml index 946803c..29abe74 100644 --- a/.github/workflows/pr-deploy-coolify.yml +++ b/.github/workflows/pr-deploy-coolify.yml @@ -214,19 +214,25 @@ jobs: fi echo "Iniciando deploy (POST /start)..." api -X POST "${COOLIFY_URL}/api/v1/applications/${APP_UUID}/start" | jq -r '.message // "start enviado"' || true - STATUS="" - for i in $(seq 1 72); do - STATUS=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') - echo "[poll ${i}/72] status=${STATUS}" - case "${STATUS}" in - running*) break ;; - exited*) echo "ERROR: contenedor exited"; exit 1 ;; + DSTAT="" + for i in $(seq 1 90); do + DSTAT=$(api "${COOLIFY_URL}/api/v1/deployments/applications/${APP_UUID}" | jq -r '(.deployments // .) | sort_by(.created_at // "") | last | .status // empty') + ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + echo "[poll ${i}/90] deployment=${DSTAT} app=${ASTAT}" + case "${DSTAT}" in + finished*) break ;; + failed|cancelled) echo "ERROR: deployment ${DSTAT}"; exit 1 ;; esac sleep 5 done - case "${STATUS}" in - running*) echo "preview RUNNING" ;; - *) echo "ERROR: timeout esperando running (ultimo=${STATUS})"; exit 1 ;; + case "${DSTAT}" in + finished*) echo "deployment finished" ;; + *) echo "ERROR: timeout esperando deployment (ultimo=${DSTAT})"; exit 1 ;; + esac + ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + case "${ASTAT}" in + running*) echo "preview RUNNING (${ASTAT})" ;; + *) echo "ERROR: app no running tras deployment finished (${ASTAT})"; exit 1 ;; esac CID=$(docker ps -q --filter "ancestor=${IMG}:${IMG_TAG}" | head -1) if [ -z "${CID}" ]; then echo "ERROR: contenedor no encontrado (${IMG}:${IMG_TAG})"; exit 1; fi @@ -329,19 +335,25 @@ jobs: fi echo "Iniciando deploy (POST /start)..." api -X POST "${COOLIFY_URL}/api/v1/applications/${APP_UUID}/start" | jq -r '.message // "start enviado"' || true - STATUS="" - for i in $(seq 1 72); do - STATUS=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') - echo "[poll ${i}/72] status=${STATUS}" - case "${STATUS}" in - running*) break ;; - exited*) echo "ERROR: contenedor exited"; exit 1 ;; + DSTAT="" + for i in $(seq 1 90); do + DSTAT=$(api "${COOLIFY_URL}/api/v1/deployments/applications/${APP_UUID}" | jq -r '(.deployments // .) | sort_by(.created_at // "") | last | .status // empty') + ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + echo "[poll ${i}/90] deployment=${DSTAT} app=${ASTAT}" + case "${DSTAT}" in + finished*) break ;; + failed|cancelled) echo "ERROR: deployment ${DSTAT}"; exit 1 ;; esac sleep 5 done - case "${STATUS}" in - running*) echo "preview RUNNING" ;; - *) echo "ERROR: timeout esperando running (ultimo=${STATUS})"; exit 1 ;; + case "${DSTAT}" in + finished*) echo "deployment finished" ;; + *) echo "ERROR: timeout esperando deployment (ultimo=${DSTAT})"; exit 1 ;; + esac + ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + case "${ASTAT}" in + running*) echo "preview RUNNING (${ASTAT})" ;; + *) echo "ERROR: app no running tras deployment finished (${ASTAT})"; exit 1 ;; esac CID=$(docker ps -q --filter "ancestor=${IMG}:${IMG_TAG}" | head -1) if [ -z "${CID}" ]; then echo "ERROR: contenedor no encontrado (${IMG}:${IMG_TAG})"; exit 1; fi From a23578e61b8942a6ec4ea0a7be0b723b75b04e9a Mon Sep 17 00:00:00 2001 From: "devops (jokerserver)" Date: Fri, 28 Aug 2026 17:31:56 +0000 Subject: [PATCH 2/5] =?UTF-8?q?fix(ci):=20definir=20REGISTRY=20en=20env=20?= =?UTF-8?q?de=20workflow=20(cherry=20del=20fix=20de=20#182=20=E2=80=94=20f?= =?UTF-8?q?altaba=20en=20el=20merge-ref=20de=20esta=20rama)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pr-deploy-coolify.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-deploy-coolify.yml b/.github/workflows/pr-deploy-coolify.yml index 29abe74..2a04944 100644 --- a/.github/workflows/pr-deploy-coolify.yml +++ b/.github/workflows/pr-deploy-coolify.yml @@ -9,6 +9,7 @@ on: env: PYTHON_VERSION: "3.11" NODE_VERSION: "26" + REGISTRY: localhost:5000 # NOTE: preview deploys corren en runner self-hosted (label coolify-deploy) — card 52a85645. # NOTE: the SECURITY GATE (security-gate job) IS blocking/fail-closed (card 650387a1, R3): From 6ffd144e78ff727e61bf05295b2c117b2d0fa0fb Mon Sep 17 00:00:00 2001 From: "devops (jokerserver)" Date: Fri, 28 Aug 2026 17:49:17 +0000 Subject: [PATCH 3/5] fix(ci): context correcto del build backend en gate + probe frontend :3000 + retry de app-status - El gate construia la imagen backend con context=. (raiz): COPY requirements.txt cogia el de la raiz (framework QA, pytest/bandit) y no el del backend -> sin uvicorn/fastapi -> contenedor 'exec: uvicorn not found' (deployment failed, queue 10). - El probe frontend apuntaba a 5173 pero vite configura 3000 (evidencia: logs del contenedor: 'VITE ready, Local: http://localhost:3000/'). - El app-status 'exited:unhealthy' puede persistir por lag de Coolify tras 'deployment finished': retry 60s; el probe in-container es el check autoritativo. --- .github/workflows/pr-deploy-coolify.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-deploy-coolify.yml b/.github/workflows/pr-deploy-coolify.yml index 2a04944..1f89d8b 100644 --- a/.github/workflows/pr-deploy-coolify.yml +++ b/.github/workflows/pr-deploy-coolify.yml @@ -55,7 +55,7 @@ jobs: # Same build contexts the deploy jobs use, so we gate the artifact actually deployed - name: Build backend image (scan target — deploy context) - run: docker build -t qa-framework-backend:gate -f dashboard/backend/Dockerfile . + run: docker build -t qa-framework-backend:gate -f dashboard/backend/Dockerfile dashboard/backend - name: Build frontend image (scan target — deploy context) run: docker build -t qa-framework-frontend:gate -f dashboard/frontend/Dockerfile dashboard/frontend @@ -230,10 +230,15 @@ jobs: finished*) echo "deployment finished" ;; *) echo "ERROR: timeout esperando deployment (ultimo=${DSTAT})"; exit 1 ;; esac - ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + ASTAT="" + for i in $(seq 1 12); do + ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + case "${ASTAT}" in running*) break ;; esac + sleep 5 + done case "${ASTAT}" in running*) echo "preview RUNNING (${ASTAT})" ;; - *) echo "ERROR: app no running tras deployment finished (${ASTAT})"; exit 1 ;; + *) echo "WARN: app-status=${ASTAT} (posible lag); el probe in-container decide" ;; esac CID=$(docker ps -q --filter "ancestor=${IMG}:${IMG_TAG}" | head -1) if [ -z "${CID}" ]; then echo "ERROR: contenedor no encontrado (${IMG}:${IMG_TAG})"; exit 1; fi @@ -327,7 +332,7 @@ jobs: APP_UUID=$(api "${COOLIFY_URL}/api/v1/applications" | jq -r --arg n "${APP}" '.[] | select(.name==$n) | .uuid' | head -1) if [ -z "${APP_UUID}" ]; then echo "Creando app ${APP} en Coolify..." - RESP=$(api -X POST "${COOLIFY_URL}/api/v1/applications/dockerimage" -d "{\"project_uuid\":\"${PROJECT_UUID}\",\"server_uuid\":\"${SERVER_UUID}\",\"environment_name\":\"production\",\"name\":\"${APP}\",\"docker_registry_image_name\":\"${IMG}\",\"docker_registry_image_tag\":\"${IMG_TAG}\",\"ports_exposes\":\"5173\"}") + RESP=$(api -X POST "${COOLIFY_URL}/api/v1/applications/dockerimage" -d "{\"project_uuid\":\"${PROJECT_UUID}\",\"server_uuid\":\"${SERVER_UUID}\",\"environment_name\":\"production\",\"name\":\"${APP}\",\"docker_registry_image_name\":\"${IMG}\",\"docker_registry_image_tag\":\"${IMG_TAG}\",\"ports_exposes\":\"3000\"}") APP_UUID=$(echo "${RESP}" | jq -r '.uuid // empty') if [ -z "${APP_UUID}" ]; then echo "ERROR creando app: ${RESP}"; exit 1; fi else @@ -351,14 +356,19 @@ jobs: finished*) echo "deployment finished" ;; *) echo "ERROR: timeout esperando deployment (ultimo=${DSTAT})"; exit 1 ;; esac - ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + ASTAT="" + for i in $(seq 1 12); do + ASTAT=$(api "${COOLIFY_URL}/api/v1/applications/${APP_UUID}" | jq -r '.status') + case "${ASTAT}" in running*) break ;; esac + sleep 5 + done case "${ASTAT}" in running*) echo "preview RUNNING (${ASTAT})" ;; - *) echo "ERROR: app no running tras deployment finished (${ASTAT})"; exit 1 ;; + *) echo "WARN: app-status=${ASTAT} (posible lag); el probe in-container decide" ;; esac CID=$(docker ps -q --filter "ancestor=${IMG}:${IMG_TAG}" | head -1) if [ -z "${CID}" ]; then echo "ERROR: contenedor no encontrado (${IMG}:${IMG_TAG})"; exit 1; fi - docker exec "${CID}" wget -q -O /dev/null http://127.0.0.1:5173/ && echo "probe / (vite dev 5173) OK" + docker exec "${CID}" wget -q -O /dev/null http://127.0.0.1:3000/ && echo "probe / (vite dev 3000) OK" echo "status=${STATUS}" >> "${GITHUB_OUTPUT}" echo "app_uuid=${APP_UUID}" >> "${GITHUB_OUTPUT}" rm -f "${HDR}" From 31f2ffc3e014fa58c31ec927d6a51aaaee004135 Mon Sep 17 00:00:00 2001 From: "devops (jokerserver)" Date: Fri, 28 Aug 2026 17:56:48 +0000 Subject: [PATCH 4/5] fix(ci): output status usa ASTAT (STATUS fue renombrado en el fix del poll) --- .github/workflows/pr-deploy-coolify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-deploy-coolify.yml b/.github/workflows/pr-deploy-coolify.yml index 1f89d8b..1827bae 100644 --- a/.github/workflows/pr-deploy-coolify.yml +++ b/.github/workflows/pr-deploy-coolify.yml @@ -244,7 +244,7 @@ jobs: if [ -z "${CID}" ]; then echo "ERROR: contenedor no encontrado (${IMG}:${IMG_TAG})"; exit 1; fi docker exec "${CID}" python3 -c "import urllib.request; r=urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5); print('probe /health ->', r.status)" >/dev/null echo "probe /health OK" - echo "status=${STATUS}" >> "${GITHUB_OUTPUT}" + echo "status=${ASTAT}" >> "${GITHUB_OUTPUT}" echo "app_uuid=${APP_UUID}" >> "${GITHUB_OUTPUT}" rm -f "${HDR}" @@ -369,7 +369,7 @@ jobs: CID=$(docker ps -q --filter "ancestor=${IMG}:${IMG_TAG}" | head -1) if [ -z "${CID}" ]; then echo "ERROR: contenedor no encontrado (${IMG}:${IMG_TAG})"; exit 1; fi docker exec "${CID}" wget -q -O /dev/null http://127.0.0.1:3000/ && echo "probe / (vite dev 3000) OK" - echo "status=${STATUS}" >> "${GITHUB_OUTPUT}" + echo "status=${ASTAT}" >> "${GITHUB_OUTPUT}" echo "app_uuid=${APP_UUID}" >> "${GITHUB_OUTPUT}" rm -f "${HDR}" From 905cd6133f3f43b6f143a4fbdcaf4129cf36ecf8 Mon Sep 17 00:00:00 2001 From: "devops (jokerserver)" Date: Fri, 28 Aug 2026 18:03:52 +0000 Subject: [PATCH 5/5] fix(ci): permissions pull-requests:write en jobs deploy + comments no-fatales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El comment step moria con 403 'Resource not accessible by integration' (GITHUB_TOKEN default read-only en este repo) DESPUES de un deploy correcto, teñiendo de rojo un preview sano. Ahora: permissions minimos por job y el paso cosmico de comentario es continue-on-error (la verdad del deploy es la API + probe, no el comentario). --- .github/workflows/pr-deploy-coolify.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/pr-deploy-coolify.yml b/.github/workflows/pr-deploy-coolify.yml index 1827bae..c3f04f8 100644 --- a/.github/workflows/pr-deploy-coolify.yml +++ b/.github/workflows/pr-deploy-coolify.yml @@ -166,6 +166,9 @@ jobs: name: Deploy Backend Preview needs: security-gate runs-on: [self-hosted, coolify-deploy] + permissions: + contents: read + pull-requests: write if: github.event_name == 'pull_request' && github.event.action != 'closed' && vars.COOLIFY_CONFIGURED == 'true' outputs: app_uuid: ${{ steps.deploy.outputs.app_uuid }} @@ -249,6 +252,7 @@ jobs: rm -f "${HDR}" - name: Comment PR with Backend Preview status + continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -292,6 +296,9 @@ jobs: name: Deploy Frontend Preview needs: security-gate runs-on: [self-hosted, coolify-deploy] + permissions: + contents: read + pull-requests: write if: github.event_name == 'pull_request' && github.event.action != 'closed' && vars.COOLIFY_CONFIGURED == 'true' outputs: app_uuid: ${{ steps.deploy.outputs.app_uuid }} @@ -374,6 +381,7 @@ jobs: rm -f "${HDR}" - name: Comment PR with Frontend Preview status + continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -417,6 +425,9 @@ jobs: cleanup-previews: name: Cleanup Preview Deployments runs-on: [self-hosted, coolify-deploy] + permissions: + contents: read + pull-requests: write if: github.event_name == 'pull_request' && github.event.action == 'closed' && vars.COOLIFY_CONFIGURED == 'true' steps: - name: Delete preview apps via Coolify API @@ -453,6 +464,7 @@ jobs: echo "Cleanup completo" - name: Comment PR with cleanup confirmation + continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: |