44e45dfb00
Релизный флоу переведён на линейную fast-forward модель — устраняет регулярные конфликты на 20 package.json при релизе. Корень проблемы: publish-alpha.sh/publish-prod.sh бампали версию НА КАЖДОЙ ветке (alpha-N на testnet, чистую на main) через `git merge -X theirs` + back-merge. Два независимых bump-коммита за цикл + merge'и плодили расхождение веток → конфликты (особенно при гонке push/pull). Новая модель: - Версию бампает lerna ОДИН раз на dev (scripts/cut-release.sh). - Тот же коммит едет вверх по FF: scripts/promote.sh testnet|main (server-side fast-forward push, рабочее дерево не трогается). - testnet/main не несут своих коммитов → ветки не диверджатся → конфликты структурно невозможны. release.yaml: - Триггер: push в testnet/main с изменением lerna.json (вместо тега v*). Окружение определяет ВЕТКА (main→prod, testnet→staging), не суффикс -alpha. - Версия читается из закоммиченного lerna.json (едет с коммитом по FF). - Гейты npm-publish/доки: branch == main (вместо !contains '-alpha'). - Образы/webhook по-прежнему версия-тегированы → playbooks/приёмник деплоя не затрагиваются. Удалены publish-alpha.sh/publish-prod.sh и мёртвые дубли тех же merge-X-theirs скриптов (root sync:main, components/contracts production/testnet/docs-publish). Документация — scripts/RELEASE.md + CLAUDE.md PR-flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
415 lines
19 KiB
YAML
415 lines
19 KiB
YAML
name: Release
|
||
|
||
# Релизный workflow по push'у в testnet/main (FF-промоушн-модель, см.
|
||
# scripts/RELEASE.md).
|
||
#
|
||
# Порядок (через jobs.needs):
|
||
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
|
||
# 2) publish-packages — npm publish через lerna (только ветка main)
|
||
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
|
||
# Gitea (только ветка main); он сам тянет mono и
|
||
# пересобирает доки.
|
||
#
|
||
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
|
||
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
|
||
#
|
||
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
|
||
# если релиз контрактов или контейнеров провалился.
|
||
#
|
||
# Триггер — push в testnet/main с изменением lerna.json. Версию бампает
|
||
# `lerna version` ОДИН раз на dev (scripts/cut-release.sh); тот же коммит едет
|
||
# вверх по fast-forward (scripts/promote.sh). lerna.json меняется только на
|
||
# релизном бампе, поэтому обычные feature-пуши деплой не триггерят. dev в
|
||
# триггере НЕТ: cut на dev только бампает версию, деплой начинается с testnet
|
||
# (staging) и завершается main (production).
|
||
#
|
||
# Окружение определяет ВЕТКА (main → production, testnet → staging), а не
|
||
# суффикс версии: единая версия едет во все среды, поэтому source of truth —
|
||
# ветка, на которую сделан промоушн. Версия читается из закоммиченного
|
||
# lerna.json. Гейты npm-publish/доки — `branch == main`.
|
||
|
||
on:
|
||
push:
|
||
branches: [testnet, main]
|
||
paths:
|
||
- 'lerna.json'
|
||
workflow_dispatch:
|
||
|
||
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 version
|
||
id: resolve
|
||
run: |
|
||
# Ветка — это ветка push'а (или выбранная в workflow_dispatch).
|
||
BRANCH="${{ github.ref_name }}"
|
||
SHA="${{ github.sha }}"
|
||
|
||
# Версия — из закоммиченного lerna.json. Её бампит lerna version ОДИН
|
||
# раз на dev (scripts/cut-release.sh), и тот же коммит едет по FF в
|
||
# testnet/main. Тэг как триггер больше не используется.
|
||
RAW_VERSION="$(sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' lerna.json | head -1)"
|
||
if [ -z "$RAW_VERSION" ]; then
|
||
echo "::error::Не удалось прочитать version из lerna.json"
|
||
exit 1
|
||
fi
|
||
TAG_NAME="v${RAW_VERSION}"
|
||
|
||
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=
|
||
;;
|
||
*)
|
||
echo "::error::Неподдерживаемая ветка $BRANCH (ожидались testnet/main/dev)"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# Окружение деплоя определяет ВЕТКА: main → production, иначе → testnet.
|
||
# IS_PROD управляет деплой-webhook (PRODUCTION vs TESTNET) и тэгом :latest
|
||
# у образов. Раньше критерием был суффикс -alpha у тэга; теперь единая
|
||
# версия едет во все среды, поэтому источник истины — ветка промоушна.
|
||
if [ "$BRANCH" = "main" ]; then
|
||
IS_PROD=true
|
||
WEBHOOK_URL='${{ vars.PRODUCTION_WEBHOOK_URL }}'
|
||
else
|
||
IS_PROD=false
|
||
WEBHOOK_URL='${{ vars.TESTNET_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).
|
||
# Гейт: ветка main. Версия для publish — из закоммиченного package.json
|
||
# (lerna publish from-package); её бампит cut-release.sh на dev, тот же коммит
|
||
# доезжает до main по FF.
|
||
# Жёстко прибиваем версию pnpm, которой сгенерён lockfile (lockfileVersion 9.0
|
||
# = pnpm 9/10). Иначе action-setup может подтянуть другую версию и переписать
|
||
# pnpm-lock.yaml → Lerna EUNCOMMIT.
|
||
# ============================================================================
|
||
publish-packages:
|
||
needs: release
|
||
if: ${{ needs.release.outputs.branch == 'main' }}
|
||
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.
|
||
# Гейт: ветка 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: ${{ 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
|
||
# (приёмник деплоит доки на своей стороне). Гейт: ветка main.
|
||
# ============================================================================
|
||
trigger-mono-docs:
|
||
needs: release
|
||
if: ${{ 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 }}"}'
|