fix(ci): fail-safe registry verification + real cache self-heal + honest health-check (#2841 R2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped

Ревью R2 нашёл, что вся безопасность предыдущего фикса держалась на
недоказанной поддержке act_runner'ом steps.<id>.outcome: если раннер его
не заполняет, retry-шаг молча не бежит, continue-on-error проглатывает
падение сборки, job зелёный — а деплой тянет старый :latest на прод.

- Добавлен engine-agnostic verify-шаг после каждого retry (6 мест,
  deploy.yml + deploy-tradein.yml): `docker buildx imagetools inspect
  <image>:<sha>` без continue-on-error. Не зависит от того, поддерживает
  ли раннер outcome — проверяет реальное состояние registry напрямую.
  Если ни build, ни retry реально не запушили образ — шаг падает и job
  честно FAILURE независимо от семантики outcome.
- Вернул `cache-to` в retry-шаги (6 мест): без него битый buildcache-тег
  никогда не перезаписывался — retry всегда собирал без cache-to, значит
  cache-to не выполнялся НИКОГДА, и каждый следующий прогон снова падал
  на том же cache-from. Заявленное самолечение не работало ни разу.
- Health-check в deploy.yml (main-стек) под `set -e` не мог упасть:
  `curl ... && break` — curl не последняя команда &&-списка, POSIX
  освобождает такие команды от errexit, цикл дохаживал до sleep (exit 0)
  даже если curl ни разу не отдал 200. Приведено к паттерну
  deploy-tradein.yml: явный флаг healthy + `exit 1` после цикла.
  Подтверждено локальным bash-репро (mock curl, всегда failure): старая
  версия — exit 0, новая — exit 1; позитивный сценарий не сломан.

docker rm -f без -v в SSH-скриптах деплоя не тронут.
This commit is contained in:
bot-backend 2026-08-15 18:44:48 +03:00
parent 9b3889bb36
commit 8bce8cf5ae
2 changed files with 87 additions and 16 deletions

View file

@ -291,8 +291,10 @@ jobs:
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- name: Retry build & push tradein-backend без кеша (битый buildcache, #2841) - name: Retry build & push tradein-backend без кеша (битый buildcache, #2841)
# cache-to тоже опущен: следующий успешный прогон С кешем перезапишет # cache-from опущен (источник падения), cache-to ОСТАВЛЕН (ревью #2841 R2,
# buildcache-тег целиком (mode=max) и самолечит порчу. # issue #2): успешный ретрай перезаписывает битый buildcache-тег своими
# слоями (mode=max) — это и есть самолечение. Без cache-to здесь порча
# оставалась навсегда, следующий прогон снова падал на том же cache-from.
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@ -303,10 +305,20 @@ jobs:
APP_VERSION=${{ needs.changes.outputs.app_version }} APP_VERSION=${{ needs.changes.outputs.app_version }}
BUILD_SHA=${{ needs.changes.outputs.build_sha }} BUILD_SHA=${{ needs.changes.outputs.build_sha }}
BUILD_DATE=${{ needs.changes.outputs.build_date }} BUILD_DATE=${{ needs.changes.outputs.build_date }}
cache-to: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_BACKEND }}:latest ${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- name: Проверить, что tradein-backend:${{ github.sha }} реально в registry (fail-safe, #2841 R2)
# НЕ полагается на семантику steps.build.outcome/continue-on-error раннера —
# проверяет РЕАЛЬНОЕ состояние registry через buildx (уже настроен выше).
# Если act_runner не заполняет outcome, ретрай выше молча НЕ побежит при
# упавшем build — этот шаг единственный это заметит: манифеста с этим SHA
# не будет → шаг падает БЕЗ continue-on-error → job честно FAILURE → deploy
# ниже пропускается вместо накатки старого :latest на прод.
run: docker buildx imagetools inspect ${{ env.IMAGE_BACKEND }}:${{ github.sha }} > /dev/null
- name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон) - name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон)
# setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон. # setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон.
# Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на # Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на
@ -414,8 +426,9 @@ jobs:
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- name: Retry build & push tradein-frontend без кеша (битый buildcache, #2841) - name: Retry build & push tradein-frontend без кеша (битый buildcache, #2841)
# См. tradein-backend: cache-to опущен намеренно (следующий успешный # См. tradein-backend (issue #2, ревью R2): cache-from опущен, cache-to
# прогон с кешем перезапишет buildcache-тег целиком и самолечит порчу). # ОСТАВЛЕН — успешный ретрай перезаписывает битый buildcache-тег своими
# слоями (mode=max), это и есть самолечение.
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@ -427,10 +440,15 @@ jobs:
NEXT_PUBLIC_APP_VERSION=${{ needs.changes.outputs.app_version }} NEXT_PUBLIC_APP_VERSION=${{ needs.changes.outputs.app_version }}
NEXT_PUBLIC_BUILD_SHA=${{ needs.changes.outputs.build_sha }} NEXT_PUBLIC_BUILD_SHA=${{ needs.changes.outputs.build_sha }}
NEXT_PUBLIC_BUILD_DATE=${{ needs.changes.outputs.build_date }} NEXT_PUBLIC_BUILD_DATE=${{ needs.changes.outputs.build_date }}
cache-to: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_FRONTEND }}:latest ${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- name: Проверить, что tradein-frontend:${{ github.sha }} реально в registry (fail-safe, #2841 R2)
# См. tradein-backend выше — не полагается на steps.build.outcome раннера.
run: docker buildx imagetools inspect ${{ env.IMAGE_FRONTEND }}:${{ github.sha }} > /dev/null
- name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон) - name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон)
# setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон. # setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон.
# Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на # Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на
@ -518,17 +536,23 @@ jobs:
${{ env.IMAGE_BROWSER }}:${{ github.sha }} ${{ env.IMAGE_BROWSER }}:${{ github.sha }}
- name: Retry build & push tradein-browser без кеша (битый buildcache, #2841) - name: Retry build & push tradein-browser без кеша (битый buildcache, #2841)
# См. tradein-backend: cache-to опущен намеренно (следующий успешный # См. tradein-backend (issue #2, ревью R2): cache-from опущен, cache-to
# прогон с кешем перезапишет buildcache-тег целиком и самолечит порчу). # ОСТАВЛЕН — успешный ретрай перезаписывает битый buildcache-тег своими
# слоями (mode=max), это и есть самолечение.
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./tradein-mvp/browser context: ./tradein-mvp/browser
push: true push: true
cache-to: type=registry,ref=${{ env.IMAGE_BROWSER }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_BROWSER }}:latest ${{ env.IMAGE_BROWSER }}:latest
${{ env.IMAGE_BROWSER }}:${{ github.sha }} ${{ env.IMAGE_BROWSER }}:${{ github.sha }}
- name: Проверить, что tradein-browser:${{ github.sha }} реально в registry (fail-safe, #2841 R2)
# См. tradein-backend выше — не полагается на steps.build.outcome раннера.
run: docker buildx imagetools inspect ${{ env.IMAGE_BROWSER }}:${{ github.sha }} > /dev/null
- name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон) - name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон)
# setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон. # setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон.
# Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на # Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на

View file

@ -131,20 +131,35 @@ jobs:
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- name: Retry build & push backend без кеша (битый buildcache, #2841) - name: Retry build & push backend без кеша (битый buildcache, #2841)
# cache-to тоже опущен: следующий успешный прогон С кешем перезапишет # cache-from опущен (источник падения), а cache-to ОСТАВЛЕН: успешный
# buildcache-тег целиком (mode=max), это самолечит порчу. Если и retry # ретрай пушит свежие слои в buildcache-тег и тем самым сам перезаписывает
# упадёт — шаг красный БЕЗ continue-on-error, job честно FAILURE, и # битый blob (mode=max — полная перезапись манифеста). Раньше cache-to был
# deploy ниже корректно пропускается (уже настоящая причина, не кеш). # опущен и здесь тоже — но следующий обычный прогон опять получает cache-from
# на детерминированно битый тег и падает СНОВА: самолечения не было НИКОГДА
# (ревью #2841 R2, issue #2). Если и retry упадёт — шаг красный БЕЗ
# continue-on-error, job честно FAILURE, и deploy ниже корректно
# пропускается (уже настоящая причина, не кеш).
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./backend context: ./backend
target: runner target: runner
push: true push: true
cache-to: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_BACKEND }}:latest ${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- name: Проверить, что backend:${{ github.sha }} реально в registry (fail-safe, #2841 R2)
# НЕ полагается на семантику steps.build.outcome/continue-on-error раннера —
# проверяет РЕАЛЬНОЕ состояние registry напрямую через buildx (уже настроен
# выше). Если act_runner не заполняет outcome (не проверено живым прогоном,
# см. ревью), ретрай выше молча НЕ побежит при упавшем build, а этот шаг —
# единственный, кто это заметит: манифеста с этим SHA не будет → шаг падает
# БЕЗ continue-on-error → job честно FAILURE → deploy ниже пропускается
# вместо накатки старого :latest на прод.
run: docker buildx imagetools inspect ${{ env.IMAGE_BACKEND }}:${{ github.sha }} > /dev/null
- name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон) - name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон)
# setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон. # setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон.
# Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на # Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на
@ -231,18 +246,27 @@ jobs:
${{ env.IMAGE_WORKER }}:${{ github.sha }} ${{ env.IMAGE_WORKER }}:${{ github.sha }}
- name: Retry build & push worker без кеша (битый buildcache, #2841) - name: Retry build & push worker без кеша (битый buildcache, #2841)
# См. backend: cache-to опущен намеренно (следующий успешный прогон с # См. backend (issue #2, ревью R2): cache-from опущен, cache-to ОСТАВЛЕН —
# кешем перезапишет buildcache-тег целиком и самолечит порчу). # успешный ретрай перезаписывает битый buildcache-тег своими слоями
# (mode=max), это и есть самолечение. Без cache-to здесь порча оставалась
# навсегда — следующий прогон снова падал на том же cache-from.
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./backend context: ./backend
target: runner-with-chromium target: runner-with-chromium
push: true push: true
cache-to: type=registry,ref=${{ env.IMAGE_WORKER }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_WORKER }}:latest ${{ env.IMAGE_WORKER }}:latest
${{ env.IMAGE_WORKER }}:${{ github.sha }} ${{ env.IMAGE_WORKER }}:${{ github.sha }}
- name: Проверить, что worker:${{ github.sha }} реально в registry (fail-safe, #2841 R2)
# См. backend выше — не полагается на steps.build.outcome раннера, проверяет
# реальное состояние registry, чтобы молча пропущенный ретрай (если outcome
# не поддержан) честно уронил job вместо зелёного прогона с непушнутым образом.
run: docker buildx imagetools inspect ${{ env.IMAGE_WORKER }}:${{ github.sha }} > /dev/null
- name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон) - name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон)
# setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон. # setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон.
# Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на # Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на
@ -331,8 +355,10 @@ jobs:
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- name: Retry build & push frontend без кеша (битый buildcache, #2841) - name: Retry build & push frontend без кеша (битый buildcache, #2841)
# См. backend: cache-to опущен намеренно (следующий успешный прогон с # См. backend (issue #2, ревью R2): cache-from опущен, cache-to ОСТАВЛЕН —
# кешем перезапишет buildcache-тег целиком и самолечит порчу). # успешный ретрай перезаписывает битый buildcache-тег своими слоями
# (mode=max), это и есть самолечение. Без cache-to здесь порча оставалась
# навсегда — следующий прогон снова падал на том же cache-from.
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@ -341,10 +367,17 @@ jobs:
build-args: | build-args: |
NEXT_PUBLIC_GLITCHTIP_DSN=${{ secrets.GLITCHTIP_FRONTEND_DSN }} NEXT_PUBLIC_GLITCHTIP_DSN=${{ secrets.GLITCHTIP_FRONTEND_DSN }}
NEXT_PUBLIC_ENVIRONMENT=production NEXT_PUBLIC_ENVIRONMENT=production
cache-to: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache,mode=max
tags: | tags: |
${{ env.IMAGE_FRONTEND }}:latest ${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- name: Проверить, что frontend:${{ github.sha }} реально в registry (fail-safe, #2841 R2)
# См. backend выше — не полагается на steps.build.outcome раннера, проверяет
# реальное состояние registry, чтобы молча пропущенный ретрай (если outcome
# не поддержан) честно уронил job вместо зелёного прогона с непушнутым образом.
run: docker buildx imagetools inspect ${{ env.IMAGE_FRONTEND }}:${{ github.sha }} > /dev/null
- name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон) - name: Убрать buildx-билдер (#2869 — иначе копятся по одному на прогон)
# setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон. # setup-buildx-action создаёт билдер `docker-container` на КАЖДЫЙ прогон.
# Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на # Его post-step под Forgejo act_runner не срабатывает, поэтому к 13.08 на
@ -679,11 +712,25 @@ jobs:
docker image prune -af || true docker image prune -af || true
docker builder prune -af || true docker builder prune -af || true
# Health check # Health check — деплой ВАЛИТСЯ, если backend не поднялся (см. #2214,
# уже сделано так в deploy-tradein.yml; ревью #2841 R2 issue #3).
# `curl ... && break` под set -e НЕ мог провалить скрипт: curl — не
# последняя команда &&-списка, а POSIX прямо освобождает от errexit
# все команды AND/OR-списка кроме последней. После 30 неуспешных
# попыток цикл завершался кодом последнего sleep (0) — скрипт тихо
# продолжался, деплой уходил success с мёртвым бэкендом.
healthy=""
for i in $(seq 1 30); do for i in $(seq 1 30); do
curl -fsS http://localhost:8000/health && break if curl -fsS http://localhost:8000/health >/dev/null 2>&1; then
healthy="yes"; break
fi
sleep 1 sleep 1
done done
if [ -z "$healthy" ]; then
echo "ERROR: backend не ответил на /health за 30s — деплой FAILED"
exit 1
fi
echo "→ backend healthy на /health."
# Честный итог прогона (#2841). ПРОБЛЕМА: `deploy` пропускается своим `if:` # Честный итог прогона (#2841). ПРОБЛЕМА: `deploy` пропускается своим `if:`
# молча (result=skipped), когда build падает (например, битый blob в # молча (result=skipped), когда build падает (например, битый blob в