ci: build-containers зависит от build-contracts через workflow_run

Раньше при релизе (`chore(release): publish` + git tag) запускались
параллельно `build-contracts.yaml` (push веток) и `build-containers.yaml`
(push тэгов). Поскольку контейнеры собираются ~8 минут, а контракты ~11,
build-containers финишил первым и слал webhook на тестнет за 2-3 минуты
до того, как build-contracts успевал запушить новый `dicoop/contracts:dev`
в DockerHub. Ансибл `setup-contracts.yaml` подтягивал ПРЕДЫДУЩИЙ образ
и через `cleos set contract` перетирал чейн старым wasm.

Инцидент 2026-05-13: walletop-фикс ledger2 (коммит 2e3410b830),
вручную задеплоенный 12-05, был откачен ансиблом ровно по этой
причине — ансибл подтянул контракты сборки 12-05 04:54 (sha de0f6c79,
до моего фикса).

Изменения:
- `build-contracts.yaml` дополнительно триггерится на push любого тэга
  (без path-фильтра): нужен unconditional запуск, чтобы у workflow_run
  всегда был upstream-завершение даже когда коммит ничего не правит
  в `components/contracts/cpp/**`.
- `build-containers.yaml` переключён с `push: tags` на `workflow_run:
  Build contracts container completed`. Внутри: резолв тэга через
  `git describe --tags --exact-match $head_sha` — если на коммите тэга
  нет (обычный push в ветку без релиза), no-op. Если есть — собирает
  контейнеры и шлёт webhook ровно как раньше, но с гарантией что
  `dicoop/contracts:<branch>` уже свежий.

Важно: workflow_run-триггер берёт definition из default-ветки (main),
поэтому новый build-containers.yaml начнёт работать только после мержа
этого коммита в main. До тех пор сохраняется старое поведение dev-ветки
(workflow_run от build-contracts на dev запустит build-containers из
main; если там старая версия — всё ещё через push: tags).
This commit is contained in:
coopops
2026-05-13 16:24:12 +00:00
parent b2a304e9de
commit 5735e1fc36
2 changed files with 98 additions and 50 deletions
+89 -50
View File
@@ -1,97 +1,130 @@
name: Build Docker Images
# Триггерится по успешному завершению `Build contracts container`. Это
# гарантирует, что к моменту webhook'а на тестнет/прод образ
# `dicoop/contracts:<branch>` уже обновлён в DockerHub.
#
# Раньше workflow висел на `push: tags`, и при релизе (`chore(release):
# publish` + git tag) запускался параллельно с build-contracts. Поскольку
# build-containers (~8 мин) короче build-contracts (~11 мин), webhook
# отлетал раньше окончания build-contracts на 2-3 минуты — ансибл на
# тестнете подтягивал ПРЕДЫДУЩИЙ образ контрактов и перетирал чейн
# старым wasm. Инцидент 2026-05-13 (ledger2 фикс walletop sweep'а
# откатился ансиблом).
#
# Теперь: build-contracts триггерится и по push веток, и по push тэгов
# (см. tags в build-contracts.yaml). После его success этот workflow
# резолвит — есть ли тэг на коммите, и если есть — собирает контейнеры
# и шлёт webhook. Если тэга нет (обычный push в ветку без релиза) —
# делаем no-op.
on:
push:
tags:
- '*'
workflow_run:
workflows: ["Build contracts container"]
types: [completed]
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout repository
- name: Checkout repository at triggering SHA
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Debug info
- name: Resolve tag for triggering commit
id: resolve_tag
run: |
echo "Текущая ветка:"
git branch --show-current
HEAD_SHA="${{ github.event.workflow_run.head_sha }}"
# `git describe --exact-match` отдаёт имя тэга, лежащего ровно
# на этом коммите; код 128 если тэга нет — тогда выходим без
# сборки контейнеров (push в ветку без релизного тэга).
TAG_NAME=$(git describe --tags --exact-match "$HEAD_SHA" 2>/dev/null || true)
if [ -z "$TAG_NAME" ]; then
echo "На коммите $HEAD_SHA тэга нет — релиза не было, контейнеры не собираем."
echo "has_tag=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Найден тэг: $TAG_NAME"
echo "has_tag=true" >> $GITHUB_OUTPUT
echo "DOCKER_TAG=$TAG_NAME" >> $GITHUB_ENV
# Проверяем, является ли тег продакшн-тегом (не содержит alpha, beta, rc и т.д.)
if [[ ! $TAG_NAME =~ -(alpha|beta|rc|test) ]]; then
echo "IS_PRODUCTION_TAG=true" >> $GITHUB_ENV
echo "Это продакшн тег, будем добавлять latest"
else
echo "IS_PRODUCTION_TAG=false" >> $GITHUB_ENV
echo "Это не продакшн тег, latest не добавляем"
fi
- name: Debug info
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "Триггер: workflow_run от 'Build contracts container'"
echo "Триггерящий коммит: ${{ github.event.workflow_run.head_sha }}"
echo "Ветка/ref триггера: ${{ github.event.workflow_run.head_branch }}"
echo "Резолвенный тэг: ${{ env.DOCKER_TAG }}"
echo "Последние коммиты:"
git log -n 3 --oneline
echo "Проверяем файлы в директории components/desktop/src-ssr:"
ls -la components/desktop/src-ssr/ || echo "Директория не найдена!"
echo "Проверяем файлы в middlewares:"
ls -la components/desktop/src-ssr/middlewares/ || echo "Директория middlewares не найдена!"
- name: Set docker tags
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
TAG_NAME=${GITHUB_REF#refs/tags/}
echo "DOCKER_TAG=$TAG_NAME" >> $GITHUB_ENV
# Проверяем, является ли тег продакшн-тегом (не содержит alpha, beta, rc и т.д.)
if [[ ! $TAG_NAME =~ -(alpha|beta|rc|test) ]]; then
echo "IS_PRODUCTION_TAG=true" >> $GITHUB_ENV
echo "Это продакшн тег, будем добавлять latest"
else
echo "IS_PRODUCTION_TAG=false" >> $GITHUB_ENV
echo "Это не продакшн тег, latest не добавляем"
fi
else
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
echo "IS_PRODUCTION_TAG=false" >> $GITHUB_ENV
fi
- name: Login to DockerHub
if: steps.resolve_tag.outputs.has_tag == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Сначала собираем базовый образ с runtime
- name: Build base image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
docker build --target runtime -t dicoop/mono-base:${{ env.DOCKER_TAG }} .
docker push dicoop/mono-base:${{ env.DOCKER_TAG }}
# Если это продакшн тег, добавляем latest
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/mono-base:${{ env.DOCKER_TAG }} dicoop/mono-base:latest
docker push dicoop/mono-base:latest
fi
# Создаем сервисные образы на основе базового
- name: Build desktop image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.desktop
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/desktop\", \"run\", \"start\"]" >> Dockerfile.desktop
docker build -t dicoop/desktop:${{ env.DOCKER_TAG }} -f Dockerfile.desktop .
docker push dicoop/desktop:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/desktop:${{ env.DOCKER_TAG }} dicoop/desktop:latest
docker push dicoop/desktop:latest
fi
- name: Build controller image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.coopback
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/controller\", \"run\", \"start\"]" >> Dockerfile.coopback
docker build -t dicoop/coopback:${{ env.DOCKER_TAG }} -f Dockerfile.coopback .
docker push dicoop/coopback:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/coopback:${{ env.DOCKER_TAG }} dicoop/coopback:latest
docker push dicoop/coopback:latest
fi
# TEMP: парсер используется существующим деплоем как dicoop/cooparser,
# пока не мигрировали на dicoop/parser из отдельного coopenomics/parser repo.
# Оставляем сборку и пуш образа до завершения миграции потребителей.
- name: Build parser image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.cooparser
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/parser\", \"run\", \"start\"]" >> Dockerfile.cooparser
@@ -104,39 +137,44 @@ jobs:
fi
- name: Build notificator image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.notificator
echo "CMD [\"pnpm\", \"-F\", \"coop-notificator\", \"run\", \"start\"]" >> Dockerfile.notificator
docker build -t dicoop/notificator:${{ env.DOCKER_TAG }} -f Dockerfile.notificator .
docker push dicoop/notificator:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/notificator:${{ env.DOCKER_TAG }} dicoop/notificator:latest
docker push dicoop/notificator:latest
fi
- name: Build notifications image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.notifications
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/notifications\", \"run\", \"sync\"]" >> Dockerfile.notifications
docker build -t dicoop/notifications:${{ env.DOCKER_TAG }} -f Dockerfile.notifications .
docker push dicoop/notifications:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/notifications:${{ env.DOCKER_TAG }} dicoop/notifications:latest
docker push dicoop/notifications:latest
fi
# Отправка хука для деплоя
# Отправка хука для деплоя. К этому моменту build-contracts.yaml
# уже завершился успешно (мы и есть его workflow_run-зависимый
# потребитель), значит образ `dicoop/contracts:<branch>` гарантированно
# обновлён в DockerHub — гонки 2026-05-13 больше не воспроизвести.
- name: Trigger deployment webhook
if: ${{ success() }}
if: success() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
if [[ $GITHUB_REF == refs/tags/*alpha* ]]; then
if [[ "${{ env.DOCKER_TAG }}" == *alpha* ]]; then
# Хук для тестнета (alpha теги)
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
elif [[ $GITHUB_REF == refs/tags/* ]]; then
else
# Хук для продакшена (остальные теги)
curl -X POST ${{ vars.PRODUCTION_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
@@ -145,22 +183,23 @@ jobs:
# Уведомление в Telegram об успехе
- name: Telegram notify success
if: ${{ success() }}
if: success() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
ADDITIONAL_INFO=" (с тегом latest)"
else
ADDITIONAL_INFO=""
fi
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="✅ [GITHUB MONO] Успешная сборка контейнеров: $GITHUB_REPOSITORY ($GITHUB_REF) [${{ env.DOCKER_TAG }}]$ADDITIONAL_INFO"
-d text="✅ [GITHUB MONO] Успешная сборка контейнеров: $GITHUB_REPOSITORY (sha=${{ github.event.workflow_run.head_sha }}) [${{ env.DOCKER_TAG }}]$ADDITIONAL_INFO"
# Уведомление в Telegram об ошибке
# Уведомление в Telegram об ошибке (только если до этого был
# резолвен тэг — иначе no-op запуск без сборки и без алертов).
- name: Telegram notify failure
if: ${{ failure() }}
if: failure() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ [GITHUB MONO] Ошибка при сборке контейнеров: $GITHUB_REPOSITORY ($GITHUB_REF) [${{ env.DOCKER_TAG }}]"
-d text="❌ [GITHUB MONO] Ошибка при сборке контейнеров: $GITHUB_REPOSITORY (sha=${{ github.event.workflow_run.head_sha }}) [${{ env.DOCKER_TAG }}]"
+9
View File
@@ -31,6 +31,15 @@ on:
- 'components/contracts/build-all.sh'
- 'components/contracts/docker/**'
- '.github/workflows/build-contracts.yaml'
# Дополнительно ловим все push'и тэгов: build-containers.yaml зависит от
# успеха этой сборки через workflow_run, и без unconditional запуска по
# тегу build-containers не получит триггер для коммитов, где .cpp не
# менялся (например `chore(release): publish` правит только версии
# пакетов). См. историю гонки 2026-05-13: webhook деплоя сработал на
# 2:34 быстрее обновления `dicoop/contracts:testnet`, на чейн уехал
# старый wasm.
tags:
- '*'
workflow_dispatch:
jobs: