Битый слой buildcache роняет сборку бэкенда — правка не доезжает до прода, а сигнала об этом нет #2841

Closed
opened 2026-08-12 15:31:19 +00:00 by bot-backend · 2 comments
Collaborator

Что случилось

12.08 в 15:04 смержен #2838. В 15:09 build-backend упал, деплой был корректно пропущен гейтом needs.build-backend.result != 'failure'. Прод остался на образе от 14:31 — проверено в контейнере: правка геокодера 14:26 есть (_city_substituted, 2 вхождения), правки #2838 (url_from_offer_id) нет.

Причина — не код

failed to compute cache key: failed to copy: httpReadSeeker: failed open:
  GET https://ghcr.io/v2/lekss361/…-tradein-backend/blobs/sha256:3b9dae91… → 403 Forbidden
  denied: permission_denied — Error from intermediary

Падает шаг cache-from: type=registry,ref=…:buildcache (deploy-tradein.yml:248). Токен ни при чём: build-frontend и build-browser используют тот же PAT и такую же пару cache-from/cache-to (строки 300-301, 332-333) и в том же прогоне прошли зелёными. Битый блоб именно в бэкендовом buildcache.

Отказ единичный: девять предыдущих мержей в main (10.08-12.08) собрались и доехали.

Почему это стоит починить, а не просто перезапустить

Два независимых свойства делают отказ незаметным:

  1. Кэш — ускорение, а не зависимость. Недоступный слой кэша обязан замедлять сборку, а не отменять её. Сейчас чтение кэша — жёсткая зависимость.
  2. Пропущенная задача в статусах Forgejo выглядит зелёной. Между отказом build-backend (15:09:28) и «успехом» deploy (15:09:30) — две секунды, для SSH-развёртывания невозможные. У пропуска нет своего цвета, поэтому в списке проверок деплой отображается успешным, хотя не выполнялся.

Сложенные вместе: правка смержена, коммит показывает зелёный деплой, кода на проде нет. Единственный честный признак — общий итог прогона failure, а получателя у него нет (#2673: ни одного уведомления за всю историю продукта).

Что предлагается

  1. Сделать чтение кэша нефатальным — так, чтобы 403/404 на блобе давал холодную сборку, а не отказ. После первой же успешной сборки cache-to,mode=max перезапишет манифест, и битый слой уйдёт сам.
  2. Отдельно решить, чем отличать «деплой пропущен» от «деплой выполнен» на уровне проверок коммита. Пока не отличается — зелёная галка про деплой не значит, что код на проде.

Проверять надо кодом в контейнере, а не цветом проверкиdocker exec tradein-backend grep ….

Сделано сейчас

Прогон перезапущен вручную (workflow_dispatch, main) в 15:2x UTC после проверки scrape_runs WHERE status='running' → 0, чтобы деплой ничего не убил. Это разовое лечение, а не починка.

Правка pipeline — не самомержится (governance).

## Что случилось 12.08 в 15:04 смержен #2838. В 15:09 `build-backend` упал, деплой был **корректно пропущен** гейтом `needs.build-backend.result != 'failure'`. Прод остался на образе от 14:31 — проверено в контейнере: правка геокодера 14:26 есть (`_city_substituted`, 2 вхождения), правки #2838 (`url_from_offer_id`) нет. ## Причина — не код ``` failed to compute cache key: failed to copy: httpReadSeeker: failed open: GET https://ghcr.io/v2/lekss361/…-tradein-backend/blobs/sha256:3b9dae91… → 403 Forbidden denied: permission_denied — Error from intermediary ``` Падает шаг `cache-from: type=registry,ref=…:buildcache` (deploy-tradein.yml:248). Токен ни при чём: `build-frontend` и `build-browser` используют **тот же** PAT и такую же пару `cache-from`/`cache-to` (строки 300-301, 332-333) и в том же прогоне прошли зелёными. Битый блоб именно в бэкендовом `buildcache`. Отказ единичный: девять предыдущих мержей в main (10.08-12.08) собрались и доехали. ## Почему это стоит починить, а не просто перезапустить Два независимых свойства делают отказ незаметным: 1. **Кэш — ускорение, а не зависимость.** Недоступный слой кэша обязан замедлять сборку, а не отменять её. Сейчас чтение кэша — жёсткая зависимость. 2. **Пропущенная задача в статусах Forgejo выглядит зелёной.** Между отказом `build-backend` (15:09:28) и «успехом» `deploy` (15:09:30) — две секунды, для SSH-развёртывания невозможные. У пропуска нет своего цвета, поэтому в списке проверок деплой отображается успешным, хотя не выполнялся. Сложенные вместе: правка смержена, коммит показывает зелёный деплой, кода на проде нет. Единственный честный признак — общий итог прогона `failure`, а получателя у него нет (#2673: ни одного уведомления за всю историю продукта). ## Что предлагается 1. Сделать чтение кэша нефатальным — так, чтобы 403/404 на блобе давал холодную сборку, а не отказ. После первой же успешной сборки `cache-to,mode=max` перезапишет манифест, и битый слой уйдёт сам. 2. Отдельно решить, чем отличать «деплой пропущен» от «деплой выполнен» на уровне проверок коммита. Пока не отличается — зелёная галка про деплой не значит, что код на проде. **Проверять надо кодом в контейнере, а не цветом проверки** — `docker exec tradein-backend grep …`. ## Сделано сейчас Прогон перезапущен вручную (workflow_dispatch, main) в 15:2x UTC после проверки `scrape_runs WHERE status='running'` → 0, чтобы деплой ничего не убил. Это разовое лечение, а не починка. *Правка pipeline — не самомержится (governance).*
Author
Collaborator

Поправка: «битый блоб» опровергнут, отказ преходящий

Перезапуск (workflow_dispatch, main, без единого изменения — тот же cache-from, тот же блоб) прошёл успешно:

образ пересобран      15:35 UTC (был 14:31)
контейнер пересоздан  15:36:43
url_from_offer_id     7 вхождений в /app/app/tasks/yandex_detail_backfill.py
_city_substituted     2 (правка геокодера не потерялась)

Значит формулировка выше — «битый блоб именно в бэкендовом buildcache» — неверна. Блоб цел, тем же токеном по тому же адресу он читается. Это был преходящий 403 на границе реестра («Error from intermediary» в тексте ошибки указывал ровно на это, а я прочитал его как подтверждение порчи).

Это моя собственная диагностика со знаком вопроса: гипотеза записана как вывод и дальше читалась бы как факт. Опровергается одним перезапуском, который стоило сделать до формулировки, а не после.

Что меняется в предложении

Суть остаётся, обоснование другое — и оно сильнее:

  • Было: «блоб битый, после первой успешной сборки cache-to,mode=max перезапишет манифест, и слой уйдёт сам». Самолечения нет — лечить нечего.
  • Стало: преходящий отказ реестра роняет деплой целиком. Это хуже битого блоба: битый чинится один раз, а преходящий вернётся, и предсказать когда нельзя. Чтение кэша обязано быть ускорением, а не зависимостью — 403/404/таймаут на слое кэша должен давать холодную сборку, а не отказ.

Частота: один отказ на десять мержей (12.08 из 10.08-12.08). Цена каждого — правка не на проде и ни одного сигнала об этом.

Второй пункт в силе без изменений

«Деплой пропущен» и «деплой выполнен» по-прежнему неразличимы в проверках коммита: 15:09:28 отказ сборки → 15:09:30 «успех» деплоя, две секунды. Гейт сработал верно, деплой не выполнялся, но в списке проверок он зелёный.

Проверять — только кодом в живом контейнере.

## Поправка: «битый блоб» опровергнут, отказ преходящий Перезапуск (workflow_dispatch, main, без единого изменения — тот же `cache-from`, тот же блоб) **прошёл успешно**: ``` образ пересобран 15:35 UTC (был 14:31) контейнер пересоздан 15:36:43 url_from_offer_id 7 вхождений в /app/app/tasks/yandex_detail_backfill.py _city_substituted 2 (правка геокодера не потерялась) ``` Значит формулировка выше — «битый блоб именно в бэкендовом buildcache» — **неверна**. Блоб цел, тем же токеном по тому же адресу он читается. Это был преходящий 403 на границе реестра («Error from intermediary» в тексте ошибки указывал ровно на это, а я прочитал его как подтверждение порчи). Это моя собственная **диагностика со знаком вопроса**: гипотеза записана как вывод и дальше читалась бы как факт. Опровергается одним перезапуском, который стоило сделать до формулировки, а не после. ## Что меняется в предложении Суть остаётся, обоснование другое — и оно сильнее: - Было: «блоб битый, после первой успешной сборки `cache-to,mode=max` перезапишет манифест, и слой уйдёт сам». **Самолечения нет** — лечить нечего. - Стало: **преходящий отказ реестра роняет деплой целиком**. Это хуже битого блоба: битый чинится один раз, а преходящий вернётся, и предсказать когда нельзя. Чтение кэша обязано быть ускорением, а не зависимостью — 403/404/таймаут на слое кэша должен давать холодную сборку, а не отказ. Частота: один отказ на десять мержей (12.08 из 10.08-12.08). Цена каждого — правка не на проде и ни одного сигнала об этом. ## Второй пункт в силе без изменений «Деплой пропущен» и «деплой выполнен» по-прежнему неразличимы в проверках коммита: 15:09:28 отказ сборки → 15:09:30 «успех» деплоя, две секунды. Гейт сработал верно, деплой не выполнялся, но в списке проверок он зелёный. Проверять — только кодом в живом контейнере.
lekss361 added the
bug
ci
observability
scope/devops
labels 2026-08-16 10:25:22 +00:00
Owner

Закрываю по итогам разбора трекера 16.08.2026

Вердикт: сделано кодом.

Оба свойства отказа закрыты: чтение кэша стало нефатальным (с самолечением), а пропущенный деплой больше не выглядит зелёным.

Доказательство: 9b3889bb ci(deploy): честный статус деплоя + нефатальный buildcache (#2841); 8bce8cf5 fix(ci): fail-safe registry verification + real cache self-heal + honest health-check (#2841 R2)

Независимая проверка. Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его:

Оба пункта задачи закрыты и живут на проде. Пункт 2 (отличить 'деплой пропущен' от 'деплой выполнен'): job deploy-status добавлен в ОБА workflow (deploy-tradein.yml:1118-1136 и deploy.yml:744-761), if: always() && !cancelled(), exit 1 при deploy.result != success. Проверил, что он реально бежит: в actions/tasks сегодня 16.08 — run 20660 'deploy-status | main | success' (10:16, sha b474a5f4) и 20640 (07:45, sha d38e83df), т.е. job не декоративный. Ложных красных на docs-only пушах не будет — оба workflow триггерятся по paths, deploy при этом успешен даже когда часть build'ов skipped (наблюдал в прогоне 20635-20640). Пункт 1 (нефатальный buildcache): у всех шести build-push-шагов id + continue-on-error, ретрай без cache-from но С cache-to (mode=max — самолечение, возвращено ревью R2 8bce8cf5), и engine-agnostic страховка 'docker buildx imagetools inspect :' БЕЗ continue-on-error после каждого ретрая — так что даже если раннер не заполняет steps..outcome, образ не запушен → job честно падает, а не отдаёт прод старому :latest. Оговорка (не блокер): сам путь 'ретрай без кэша' живьём с 15.08 ни разу не сработал (нового 403 от реестра не было), так что нефатальность кэша подтверждена кодом и локальным репро health-check'а, но не боевым инцидентом.

Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.

## Закрываю по итогам разбора трекера 16.08.2026 **Вердикт:** сделано кодом. Оба свойства отказа закрыты: чтение кэша стало нефатальным (с самолечением), а пропущенный деплой больше не выглядит зелёным. **Доказательство:** 9b3889bb ci(deploy): честный статус деплоя + нефатальный buildcache (#2841); 8bce8cf5 fix(ci): fail-safe registry verification + real cache self-heal + honest health-check (#2841 R2) **Независимая проверка.** Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его: > Оба пункта задачи закрыты и живут на проде. Пункт 2 (отличить 'деплой пропущен' от 'деплой выполнен'): job deploy-status добавлен в ОБА workflow (deploy-tradein.yml:1118-1136 и deploy.yml:744-761), if: always() && !cancelled(), exit 1 при deploy.result != success. Проверил, что он реально бежит: в actions/tasks сегодня 16.08 — run 20660 'deploy-status | main | success' (10:16, sha b474a5f4) и 20640 (07:45, sha d38e83df), т.е. job не декоративный. Ложных красных на docs-only пушах не будет — оба workflow триггерятся по paths, deploy при этом успешен даже когда часть build'ов skipped (наблюдал в прогоне 20635-20640). Пункт 1 (нефатальный buildcache): у всех шести build-push-шагов id + continue-on-error, ретрай без cache-from но С cache-to (mode=max — самолечение, возвращено ревью R2 8bce8cf5), и engine-agnostic страховка 'docker buildx imagetools inspect <image>:<sha>' БЕЗ continue-on-error после каждого ретрая — так что даже если раннер не заполняет steps.<id>.outcome, образ не запушен → job честно падает, а не отдаёт прод старому :latest. Оговорка (не блокер): сам путь 'ретрай без кэша' живьём с 15.08 ни разу не сработал (нового 403 от реестра не было), так что нефатальность кэша подтверждена кодом и локальным репро health-check'а, но не боевым инцидентом. Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2841
No description provided.