a77fa945a6
Релей переехал в отдельный репозиторий C9S/email-relay, собирается локально плейбуком (playbooks/email-relay) — релиз mono его больше не пересобирает. Прод-образ mono-base (prune --prod) не подходил для standalone-сервиса. Удалено: components/email-relay, build_service в release.yaml, dev-сервис в docker-compose. Контроллерный relay-режим (EMAIL_RELAY_URL/TOKEN в email-channel.adapter + config) ОСТАЁТСЯ — он и активирует отправку через релей. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
407 lines
18 KiB
YAML
407 lines
18 KiB
YAML
name: Release
|
||
|
||
# Единый релизный workflow по push'у тэга `v*`.
|
||
#
|
||
# Порядок (через jobs.needs):
|
||
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
|
||
# 2) publish-packages — npm publish через lerna (если не -alpha)
|
||
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
|
||
# Gitea (если не -alpha + ветка main); он сам тянет
|
||
# mono и пересобирает доки.
|
||
#
|
||
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
|
||
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
|
||
#
|
||
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
|
||
# если релиз контрактов или контейнеров провалился. Раньше четыре workflow'а
|
||
# стартовали параллельно по одному тэгу — отсюда гонка из инцидента 2026-05-13
|
||
# с ledger2 walletop sweep'ом (см. историю release.yaml до объединения).
|
||
#
|
||
# Гейт `-alpha` унифицирован: пакеты + доки + триггер docs пропускают только
|
||
# не-alpha тэги. -beta/-rc/-test в проекте не используются (см. CLAUDE.md),
|
||
# поэтому проще иметь один паттерн `!contains(github.ref, '-alpha')`.
|
||
# IS_PROD в release-job остаётся на `(alpha|beta|rc|test)` — он определяет
|
||
# деплой-webhook (testnet vs production) и тэг `:latest` у образов;
|
||
# намеренно строже, чтобы случайный -rc-тэг не уехал на прод.
|
||
#
|
||
# Резолв ветки. Тэг семантики окружения не несёт (например `v2026.5.13-alpha-2`
|
||
# может лежать на testnet или dev — определяет именно ветка, на которую сделан
|
||
# merge перед тэгом). Поэтому ищем первое совпадение SHA с remote-ветками
|
||
# main/testnet/dev. branch экспортируется как outputs.branch для downstream jobs.
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*'
|
||
|
||
permissions:
|
||
contents: write
|
||
|
||
jobs:
|
||
release:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
tag_name: ${{ steps.resolve.outputs.tag_name }}
|
||
branch: ${{ steps.resolve.outputs.branch }}
|
||
is_prod: ${{ steps.resolve.outputs.is_prod }}
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Resolve branch, build mode and tags
|
||
id: resolve
|
||
run: |
|
||
TAG_NAME="${{ github.ref_name }}"
|
||
SHA="${{ github.sha }}"
|
||
BRANCH=""
|
||
for candidate in main testnet dev; do
|
||
if git branch -r --contains "$SHA" 2>/dev/null | grep -qE "^[[:space:]]*origin/${candidate}$"; then
|
||
BRANCH="$candidate"
|
||
break
|
||
fi
|
||
done
|
||
if [ -z "$BRANCH" ]; then
|
||
echo "::error::Тэг $TAG_NAME ($SHA) не лежит ни на одной из dev/testnet/main"
|
||
exit 1
|
||
fi
|
||
|
||
case "$BRANCH" in
|
||
main)
|
||
BUILD_MODE=prod
|
||
CONTRACTS_TAG=main
|
||
CONTRACTS_EXTRA=latest
|
||
;;
|
||
testnet)
|
||
BUILD_MODE=test
|
||
CONTRACTS_TAG=testnet
|
||
CONTRACTS_EXTRA=
|
||
;;
|
||
dev)
|
||
BUILD_MODE=test
|
||
CONTRACTS_TAG=dev
|
||
CONTRACTS_EXTRA=
|
||
;;
|
||
esac
|
||
|
||
# IS_PROD определяет webhook деплоя (PRODUCTION vs TESTNET) и тэг :latest.
|
||
# Сознательно строже гейта publish-* — случайный -rc/-beta тэг не должен
|
||
# уехать на прод-вебхук, даже если в npm/docs его опубликовали.
|
||
if [[ "$TAG_NAME" =~ -(alpha|beta|rc|test) ]]; then
|
||
IS_PROD=false
|
||
WEBHOOK_URL='${{ vars.TESTNET_WEBHOOK_URL }}'
|
||
else
|
||
IS_PROD=true
|
||
WEBHOOK_URL='${{ vars.PRODUCTION_WEBHOOK_URL }}'
|
||
fi
|
||
|
||
{
|
||
echo "TAG_NAME=$TAG_NAME"
|
||
echo "SHA=$SHA"
|
||
echo "BRANCH=$BRANCH"
|
||
echo "BUILD_MODE=$BUILD_MODE"
|
||
echo "CONTRACTS_TAG=$CONTRACTS_TAG"
|
||
echo "CONTRACTS_EXTRA=$CONTRACTS_EXTRA"
|
||
echo "IS_PROD=$IS_PROD"
|
||
echo "WEBHOOK_URL=$WEBHOOK_URL"
|
||
} >> "$GITHUB_ENV"
|
||
|
||
{
|
||
echo "tag_name=$TAG_NAME"
|
||
echo "branch=$BRANCH"
|
||
echo "is_prod=$IS_PROD"
|
||
} >> "$GITHUB_OUTPUT"
|
||
|
||
echo "Release $TAG_NAME → branch=$BRANCH, build_mode=$BUILD_MODE, contracts:$CONTRACTS_TAG, prod=$IS_PROD"
|
||
|
||
- name: Login to DockerHub
|
||
uses: docker/login-action@v3
|
||
with:
|
||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||
|
||
# === Этап 1: контракты ===
|
||
# CDT ставим из .deb (C9S/cdt v4.2.0) прямо в окружение job'а и
|
||
# компилируем напрямую — БЕЗ вложенного docker. Почему не build-all.sh:
|
||
# тот монтирует $(pwd):/project в sibling-контейнер, а под Gitea
|
||
# act_runner сам job исполняется в контейнере → хостовый демон не видит
|
||
# этот путь, /project пуст, cmake падает "no CMakeLists.txt".
|
||
# build-all.sh остаётся для локальной сборки (оборачивает тот же
|
||
# build_contracts_cdt.sh в docker).
|
||
# CMakeLists хардкодит toolchain /cdt/build/...; .deb кладёт CDT в
|
||
# /usr/opt/cdt/4.2.0, а сам CDTWasmToolchain.cmake указывает на /usr
|
||
# абсолютно — поэтому симлинк /cdt/build → /usr/opt/cdt/4.2.0 сводит пути
|
||
# без правки CMakeLists.
|
||
|
||
- name: Install CDT 4.2.0 toolchain (.deb)
|
||
run: |
|
||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||
$SUDO apt-get update
|
||
# build-essential — для host-компилятора (project() в CMakeLists);
|
||
# libz3-4/libtinfo6/libxml2/zlib1g — рантайм бинарей CDT (clang-9,
|
||
# ld.lld и т.д.). Пакет cdt объявляет только libcurl4-gnutls-dev,
|
||
# поэтому остальные .so ставим явно — в образе dicoop/blockchain они
|
||
# были из сборки исходников, из .deb не тянутся.
|
||
$SUDO apt-get install -y --no-install-recommends \
|
||
curl ca-certificates cmake make build-essential \
|
||
libz3-4 libtinfo6 libxml2 zlib1g
|
||
curl -fsSL -o /tmp/cdt.deb \
|
||
https://git.coopenomics.world/C9S/cdt/releases/download/v4.2.0/cdt_4.2.0-1_amd64.deb
|
||
$SUDO apt-get install -y /tmp/cdt.deb
|
||
$SUDO mkdir -p /cdt
|
||
$SUDO ln -sfn /usr/opt/cdt/4.2.0 /cdt/build
|
||
cdt-cpp --version || true
|
||
|
||
- name: Compile contracts
|
||
working-directory: components/contracts
|
||
run: |
|
||
./build_contracts_cdt.sh "$BUILD_MODE"
|
||
echo "--- build/contracts ---"
|
||
ls -la build/contracts/
|
||
|
||
- name: Stage contracts docker context
|
||
working-directory: components/contracts
|
||
run: |
|
||
ROOT="docker/.context"
|
||
rm -rf "$ROOT"
|
||
mkdir -p "$ROOT/contracts"
|
||
|
||
# Имена контрактов = аргументы add_contract_build(...) только в
|
||
# незакомменченных строках (CMake-комментарий начинается с `#`).
|
||
NAMES=$(grep -E '^[[:space:]]*add_contract_build\(' CMakeLists.txt \
|
||
| sed -E 's/^[[:space:]]*add_contract_build\(([^)]*)\).*/\1/')
|
||
echo "Contracts to package:"
|
||
echo "$NAMES"
|
||
|
||
MANIFEST="$ROOT/contracts/manifest.json"
|
||
|
||
pack_one() {
|
||
local NAME="$1" WASM="$2" ABI="$3"
|
||
if [ ! -f "$WASM" ] || [ ! -f "$ABI" ]; then
|
||
echo "::warning::Skipping $NAME: artifacts missing ($WASM / $ABI)" >&2
|
||
return 1
|
||
fi
|
||
mkdir -p "$ROOT/contracts/$NAME"
|
||
cp "$WASM" "$ROOT/contracts/$NAME/$NAME.wasm"
|
||
cp "$ABI" "$ROOT/contracts/$NAME/$NAME.abi"
|
||
local SHA_WASM=$(sha256sum "$WASM" | awk '{print $1}')
|
||
local SHA_ABI=$(sha256sum "$ABI" | awk '{print $1}')
|
||
printf ' {"name":"%s","sha256_wasm":"%s","sha256_abi":"%s"}' \
|
||
"$NAME" "$SHA_WASM" "$SHA_ABI"
|
||
}
|
||
|
||
{
|
||
printf '{\n'
|
||
printf ' "build_mode": "%s",\n' "$BUILD_MODE"
|
||
printf ' "git_sha": "%s",\n' "$SHA"
|
||
printf ' "tag": "%s",\n' "$TAG_NAME"
|
||
printf ' "branch": "%s",\n' "$BRANCH"
|
||
printf ' "built_at": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
printf ' "contracts": [\n'
|
||
|
||
FIRST=1
|
||
emit() {
|
||
entry=$(pack_one "$@") || return 0
|
||
[ $FIRST -eq 0 ] && printf ',\n'
|
||
FIRST=0
|
||
printf '%s' "$entry"
|
||
}
|
||
|
||
for c in $NAMES; do
|
||
if [ "$c" = "system" ]; then
|
||
# У system многоконтрактная сборка: разносим под-контракты
|
||
# eosio.* как полноценные элементы каталога, сам "system"
|
||
# как имя пропускаем.
|
||
for sub in build/contracts/system/contracts/eosio.*/; do
|
||
[ -d "$sub" ] || continue
|
||
sub_name=$(basename "$sub")
|
||
emit "$sub_name" "$sub/$sub_name.wasm" "$sub/$sub_name.abi"
|
||
done
|
||
else
|
||
emit "$c" "build/contracts/$c/$c.wasm" "build/contracts/$c/$c.abi"
|
||
fi
|
||
done
|
||
|
||
printf '\n ]\n'
|
||
printf '}\n'
|
||
} > "$MANIFEST"
|
||
|
||
cp docker/Dockerfile "$ROOT/Dockerfile"
|
||
cp docker/entrypoint.sh "$ROOT/entrypoint.sh"
|
||
chmod +x "$ROOT/entrypoint.sh"
|
||
|
||
echo "--- manifest.json ---"
|
||
cat "$MANIFEST"
|
||
|
||
- name: Build and push contracts image
|
||
working-directory: components/contracts
|
||
run: |
|
||
# Ретрай push'а: резолвинг registry-1.docker.io на runner'е изредка
|
||
# моргает (DNS-таймаут к 127.0.0.53) — 3 попытки с паузой.
|
||
dpush() {
|
||
local ref="$1" n=1
|
||
until docker push "$ref"; do
|
||
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
|
||
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
|
||
n=$((n+1)); sleep 10
|
||
done
|
||
}
|
||
|
||
IMAGE="dicoop/contracts"
|
||
SHORT_SHA="${SHA::7}"
|
||
docker build \
|
||
--label "org.opencontainers.image.revision=$SHA" \
|
||
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||
--label "build.mode=$BUILD_MODE" \
|
||
-t "$IMAGE:$CONTRACTS_TAG" \
|
||
./docker/.context
|
||
dpush "$IMAGE:$CONTRACTS_TAG"
|
||
|
||
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
|
||
dpush "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
|
||
|
||
if [ -n "$CONTRACTS_EXTRA" ]; then
|
||
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
|
||
dpush "$IMAGE:$CONTRACTS_EXTRA"
|
||
fi
|
||
|
||
- name: Verify pushed contracts image
|
||
run: |
|
||
docker run --rm "dicoop/contracts:$CONTRACTS_TAG" list
|
||
echo "---"
|
||
docker run --rm "dicoop/contracts:$CONTRACTS_TAG" sha256
|
||
|
||
# === Этап 2: контейнеры приложений ===
|
||
|
||
- name: Build and push base image
|
||
run: |
|
||
dpush() {
|
||
local ref="$1" n=1
|
||
until docker push "$ref"; do
|
||
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
|
||
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
|
||
n=$((n+1)); sleep 10
|
||
done
|
||
}
|
||
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
|
||
dpush "dicoop/mono-base:$TAG_NAME"
|
||
if [ "$IS_PROD" = "true" ]; then
|
||
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
|
||
dpush dicoop/mono-base:latest
|
||
fi
|
||
|
||
- name: Build and push service images
|
||
run: |
|
||
dpush() {
|
||
local ref="$1" n=1
|
||
until docker push "$ref"; do
|
||
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
|
||
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
|
||
n=$((n+1)); sleep 10
|
||
done
|
||
}
|
||
build_service() {
|
||
local SVC="$1" PKG="$2" CMD="$3"
|
||
local DOCKERFILE="Dockerfile.$SVC"
|
||
{
|
||
echo "FROM dicoop/mono-base:$TAG_NAME"
|
||
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
|
||
} > "$DOCKERFILE"
|
||
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
|
||
dpush "dicoop/$SVC:$TAG_NAME"
|
||
if [ "$IS_PROD" = "true" ]; then
|
||
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
|
||
dpush "dicoop/$SVC:latest"
|
||
fi
|
||
}
|
||
|
||
build_service desktop '@coopenomics/desktop' start
|
||
build_service coopback '@coopenomics/controller' start
|
||
# TEMP: cooparser — пока не мигрировали потребителей на dicoop/parser
|
||
# из отдельного coopenomics/parser repo.
|
||
build_service cooparser '@coopenomics/parser' start
|
||
build_service notificator 'coop-notificator' start
|
||
build_service notifications '@coopenomics/notifications' sync
|
||
|
||
# === Этап 3: webhook деплоя ===
|
||
|
||
- name: Trigger deployment webhook
|
||
run: |
|
||
curl -X POST "$WEBHOOK_URL" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "$TAG_NAME"
|
||
|
||
# ============================================================================
|
||
# publish-packages — npm publish (бывший publish-packages.yaml).
|
||
# Гейт: не-alpha. Не зависит от ветки.
|
||
# Жёстко прибиваем версию pnpm, которой сгенерён lockfile (lockfileVersion 9.0
|
||
# = pnpm 9/10). Иначе action-setup может подтянуть другую версию и переписать
|
||
# pnpm-lock.yaml → Lerna EUNCOMMIT.
|
||
# ============================================================================
|
||
publish-packages:
|
||
needs: release
|
||
if: ${{ !contains(github.ref, '-alpha') }}
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 10.33.0
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 24
|
||
registry-url: https://registry.npmjs.org
|
||
cache: pnpm
|
||
|
||
- run: pnpm install --frozen-lockfile
|
||
- run: pnpm lerna run build
|
||
- run: pnpm lerna publish from-package --yes
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||
|
||
# ============================================================================
|
||
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
|
||
# Гейт: не-alpha + ветка main.
|
||
#
|
||
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
|
||
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
|
||
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
|
||
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
|
||
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
|
||
# Целевой workflow слушает workflow_dispatch с входами
|
||
# mono_sha/mono_ref.
|
||
# ============================================================================
|
||
trigger-coopenomics-docs:
|
||
needs: release
|
||
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
|
||
run: |
|
||
curl -fsSL -X POST \
|
||
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
|
||
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
|
||
|
||
# ============================================================================
|
||
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
|
||
# webhook DOCS_DEPLOY_WEBHOOK_URL. Отдельная от coopenomics публикация —
|
||
# обе доки уезжают синхронно по релизу. Извлечён из бывшего publish-docs:
|
||
# gh-pages на github.com выпилен как мёртвый, остался реальный deploy-webhook
|
||
# (приёмник деплоит доки на своей стороне). Гейт: не-alpha + ветка main.
|
||
# ============================================================================
|
||
trigger-mono-docs:
|
||
needs: release
|
||
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Trigger docs deployment webhook
|
||
run: |
|
||
curl -fsSL -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
|