ci(deploy): честный статус деплоя + нефатальный buildcache (#2841)

Зелёная галка прогона не отличима от пропущенного деплоя: если build падает
из-за битого blob в удалённом buildcache, шаг deploy молча пропускается
(if-условие даёт result=skipped), а прогон в целом не подсвечен как FAILED.

- deploy-status: новая job в конце deploy.yml и deploy-tradein.yml, всегда
  бежит (if: always() && !cancelled()) и падает явно, если deploy.result !=
  success — неважно, пропущен он (upstream build/test упал) или упал сам.
- cache-from нефатален: каждый build-push-action-шаг получил id + continue-
  on-error, и ретрай без cache-from/cache-to при steps.build.outcome ==
  'failure'. Битый remote-кеш больше не роняет саму сборку; следующий
  успешный прогон с кешем перезаписывает buildcache-тег целиком (mode=max)
  и самолечит порчу. Реальные ошибки сборки (не кеш) по-прежнему валят job
  на ретрае — deploy-status их тоже поймает.

Гейт против публикации services-портов на VPS (та же задача, проблема 1)
уже покрыт scripts/check-workflow-ports.py + шагом в ci.yml (#2757/#2759,
слит ранее) — сканирует все .forgejo/workflows/*.yml, включая эти два файла;
новых правок не потребовалось.

docker rm -f БЕЗ -v в SSH-скриптах деплоя не тронут — эти вызовы намеренно
без -v (боевые тома), правка их не касается.
This commit is contained in:
bot-backend 2026-08-15 17:55:17 +03:00
parent e8fe9faa13
commit 9b3889bb36
2 changed files with 176 additions and 0 deletions

View file

@ -264,6 +264,12 @@ jobs:
id: buildx id: buildx
- name: Build & push tradein-backend - name: Build & push tradein-backend
# id + continue-on-error: битый blob в удалённом buildcache-манифесте
# валит весь шаг ДО push нового образа — деплой тогда молча
# пропускается (#2841), хотя собрать образ можно и без кеша. Ретрай
# без cache-from — ниже.
id: build
continue-on-error: true
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
# Context = tradein-mvp/ (uv workspace root): образу нужен packages/scraper-kit # Context = tradein-mvp/ (uv workspace root): образу нужен packages/scraper-kit
@ -284,6 +290,23 @@ jobs:
${{ env.IMAGE_BACKEND }}:latest ${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- name: Retry build & push tradein-backend без кеша (битый buildcache, #2841)
# cache-to тоже опущен: следующий успешный прогон С кешем перезапишет
# buildcache-тег целиком (mode=max) и самолечит порчу.
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6
with:
context: ./tradein-mvp
file: ./tradein-mvp/backend/Dockerfile
push: true
build-args: |
APP_VERSION=${{ needs.changes.outputs.app_version }}
BUILD_SHA=${{ needs.changes.outputs.build_sha }}
BUILD_DATE=${{ needs.changes.outputs.build_date }}
tags: |
${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- 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 на
@ -362,6 +385,10 @@ jobs:
run: cp tradein-mvp/CHANGELOG.md tradein-mvp/frontend/CHANGELOG.md run: cp tradein-mvp/CHANGELOG.md tradein-mvp/frontend/CHANGELOG.md
- name: Build & push tradein-frontend - name: Build & push tradein-frontend
# id + continue-on-error — см. tradein-backend (#2841): битый blob в
# удалённом buildcache не должен ронять сборку и молча пропускать деплой.
id: build
continue-on-error: true
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./tradein-mvp/frontend context: ./tradein-mvp/frontend
@ -386,6 +413,24 @@ jobs:
${{ env.IMAGE_FRONTEND }}:latest ${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- name: Retry build & push tradein-frontend без кеша (битый buildcache, #2841)
# См. tradein-backend: cache-to опущен намеренно (следующий успешный
# прогон с кешем перезапишет buildcache-тег целиком и самолечит порчу).
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6
with:
context: ./tradein-mvp/frontend
push: true
build-args: |
NEXT_PUBLIC_BASE_PATH=/trade-in
NEXT_PUBLIC_API_BASE_URL=/trade-in
NEXT_PUBLIC_APP_VERSION=${{ needs.changes.outputs.app_version }}
NEXT_PUBLIC_BUILD_SHA=${{ needs.changes.outputs.build_sha }}
NEXT_PUBLIC_BUILD_DATE=${{ needs.changes.outputs.build_date }}
tags: |
${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- 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 на
@ -458,6 +503,10 @@ jobs:
id: buildx id: buildx
- name: Build & push tradein-browser - name: Build & push tradein-browser
# id + continue-on-error — см. tradein-backend выше (#2841): битый blob
# в удалённом buildcache не должен ронять сборку и молча пропускать деплой.
id: build
continue-on-error: true
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./tradein-mvp/browser context: ./tradein-mvp/browser
@ -468,6 +517,18 @@ jobs:
${{ env.IMAGE_BROWSER }}:latest ${{ env.IMAGE_BROWSER }}:latest
${{ env.IMAGE_BROWSER }}:${{ github.sha }} ${{ env.IMAGE_BROWSER }}:${{ github.sha }}
- name: Retry build & push tradein-browser без кеша (битый buildcache, #2841)
# См. tradein-backend: cache-to опущен намеренно (следующий успешный
# прогон с кешем перезапишет buildcache-тег целиком и самолечит порчу).
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6
with:
context: ./tradein-mvp/browser
push: true
tags: |
${{ env.IMAGE_BROWSER }}:latest
${{ env.IMAGE_BROWSER }}:${{ github.sha }}
- 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 на
@ -1019,3 +1080,33 @@ jobs:
# The changes job reads this file on the next run to compute cumulative diff. # The changes job reads this file on the next run to compute cumulative diff.
echo "$GITHUB_SHA" > /opt/gendesign/.tradein-deployed-sha echo "$GITHUB_SHA" > /opt/gendesign/.tradein-deployed-sha
echo "→ Deployed SHA marker updated: $GITHUB_SHA" echo "→ Deployed SHA marker updated: $GITHUB_SHA"
# Честный итог прогона (#2841). ПРОБЛЕМА: `deploy` пропускается своим `if:`
# молча (result=skipped), когда `test` или один из build-* падает (например,
# битый blob в buildcache роняет `docker/build-push-action` — до ретрая
# выше, #2841). skipped-job не красит прогон явным «FAILED» так, чтобы это
# было видно на первый взгляд — итог выглядит зелёным/нейтральным, хотя
# tradein-стек на проде не обновился. Эта job бежит ВСЕГДА (`if: always()`,
# кроме отмены прогона) и сама падает, если deploy не завершился success —
# неважно, пропущен он (test/build упали) или упал сам (SSH/миграция/
# health-check/сверка образов #2679). Красная точка встаёт именно там, где
# решение реально принято, а не там, где она случайно оказалась по цепочке if.
deploy-status:
runs-on: ubuntu-latest
needs: [test, build-backend, build-frontend, build-browser, deploy]
if: always() && !cancelled()
steps:
- name: Итог прогона — деплой обязан быть success, не skipped/failure
run: |
echo "test: ${{ needs.test.result }}"
echo "build-backend: ${{ needs.build-backend.result }}"
echo "build-frontend: ${{ needs.build-frontend.result }}"
echo "build-browser: ${{ needs.build-browser.result }}"
echo "deploy: ${{ needs.deploy.result }}"
if [ "${{ needs.deploy.result }}" != "success" ]; then
echo "::error::деплой НЕ прошёл (deploy.result=${{ needs.deploy.result }})." \
"Прогон должен читаться как FAILED, а не как пропущенный шаг (#2841)." \
"Смотри логи test/build-backend/build-frontend/build-browser/deploy выше."
exit 1
fi
echo "✓ деплой прошёл успешно"

View file

@ -113,6 +113,12 @@ jobs:
id: buildx id: buildx
- name: Build & push backend (lean — без Chromium) - name: Build & push backend (lean — без Chromium)
# id + continue-on-error: битый blob в удалённом buildcache-манифесте
# (registry cache, не local) валит весь шаг ДО push нового образа —
# деплой тогда молча пропускается (#2841), хотя код собрать можно, просто
# без кеша. cache-from нефатален: при падении ретраим БЕЗ него ниже.
id: build
continue-on-error: true
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./backend context: ./backend
@ -124,6 +130,21 @@ jobs:
${{ env.IMAGE_BACKEND }}:latest ${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }} ${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- name: Retry build & push backend без кеша (битый buildcache, #2841)
# cache-to тоже опущен: следующий успешный прогон С кешем перезапишет
# buildcache-тег целиком (mode=max), это самолечит порчу. Если и retry
# упадёт — шаг красный БЕЗ continue-on-error, job честно FAILURE, и
# deploy ниже корректно пропускается (уже настоящая причина, не кеш).
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6
with:
context: ./backend
target: runner
push: true
tags: |
${{ env.IMAGE_BACKEND }}:latest
${{ env.IMAGE_BACKEND }}:${{ github.sha }}
- 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 на
@ -194,6 +215,10 @@ jobs:
id: buildx id: buildx
- name: Build & push worker (с Chromium для Playwright) - name: Build & push worker (с Chromium для Playwright)
# id + continue-on-error — см. build-backend выше (#2841): битый blob в
# удалённом buildcache не должен ронять сборку и молча пропускать деплой.
id: build
continue-on-error: true
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./backend context: ./backend
@ -205,6 +230,19 @@ jobs:
${{ env.IMAGE_WORKER }}:latest ${{ env.IMAGE_WORKER }}:latest
${{ env.IMAGE_WORKER }}:${{ github.sha }} ${{ env.IMAGE_WORKER }}:${{ github.sha }}
- name: Retry build & push worker без кеша (битый buildcache, #2841)
# См. backend: cache-to опущен намеренно (следующий успешный прогон с
# кешем перезапишет buildcache-тег целиком и самолечит порчу).
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6
with:
context: ./backend
target: runner-with-chromium
push: true
tags: |
${{ env.IMAGE_WORKER }}:latest
${{ env.IMAGE_WORKER }}:${{ github.sha }}
- 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 на
@ -275,6 +313,10 @@ jobs:
id: buildx id: buildx
- name: Build & push frontend - name: Build & push frontend
# id + continue-on-error — см. build-backend выше (#2841): битый blob в
# удалённом buildcache не должен ронять сборку и молча пропускать деплой.
id: build
continue-on-error: true
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./frontend context: ./frontend
@ -288,6 +330,21 @@ jobs:
${{ env.IMAGE_FRONTEND }}:latest ${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- name: Retry build & push frontend без кеша (битый buildcache, #2841)
# См. backend: cache-to опущен намеренно (следующий успешный прогон с
# кешем перезапишет buildcache-тег целиком и самолечит порчу).
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@v6
with:
context: ./frontend
push: true
build-args: |
NEXT_PUBLIC_GLITCHTIP_DSN=${{ secrets.GLITCHTIP_FRONTEND_DSN }}
NEXT_PUBLIC_ENVIRONMENT=production
tags: |
${{ env.IMAGE_FRONTEND }}:latest
${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
- 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 на
@ -627,3 +684,31 @@ jobs:
curl -fsS http://localhost:8000/health && break curl -fsS http://localhost:8000/health && break
sleep 1 sleep 1
done done
# Честный итог прогона (#2841). ПРОБЛЕМА: `deploy` пропускается своим `if:`
# молча (result=skipped), когда build падает (например, битый blob в
# buildcache роняет `docker/build-push-action` — до ретрая выше, #2841).
# skipped-job НЕ красит прогон явным «FAILED» так, чтобы это было видно на
# первый взгляд — итог выглядит зелёным/нейтральным, хотя прод не обновился.
# Эта job бежит ВСЕГДА (`if: always()`, кроме отмены прогона) и сама падает,
# если deploy не завершился success — неважно, пропущен он (build упал) или
# упал сам (SSH/миграция/health-check). Красная точка встаёт именно там, где
# решение реально принято, а не там, где она случайно оказалась по цепочке if.
deploy-status:
runs-on: ubuntu-latest
needs: [build-backend, build-worker, build-frontend, deploy]
if: always() && !cancelled()
steps:
- name: Итог прогона — деплой обязан быть success, не skipped/failure
run: |
echo "build-backend: ${{ needs.build-backend.result }}"
echo "build-worker: ${{ needs.build-worker.result }}"
echo "build-frontend: ${{ needs.build-frontend.result }}"
echo "deploy: ${{ needs.deploy.result }}"
if [ "${{ needs.deploy.result }}" != "success" ]; then
echo "::error::деплой НЕ прошёл (deploy.result=${{ needs.deploy.result }})." \
"Прогон должен читаться как FAILED, а не как пропущенный шаг (#2841)." \
"Смотри логи build-backend/build-worker/build-frontend/deploy выше."
exit 1
fi
echo "✓ деплой прошёл успешно"