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' # Ручной деплой: выбрать окружение и (опционально) версию/коммит. Нужен для # отката на старую версию, редеплоя без бампа и точечного hotfix в один контур — # всё, что FF-промоушн (scripts/promote.sh, только вперёд) сделать не может. workflow_dispatch: inputs: environment: description: 'Куда деплоить' type: choice options: - testnet - main required: true ref: description: 'Релизный тег/коммит для сборки (пусто = верхушка выбранного окружения)' required: false default: '' 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 # push → собираем пушнутый коммит; ручной запуск → выбранный ref # (пусто = верхушка ветки окружения, на которой запущен workflow). ref: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.ref) || github.sha }} - name: Resolve branch, build mode and version id: resolve run: | # Окружение: при ручном workflow_dispatch — из inputs.environment; # при push — это ветка push'а (testnet/main). Версия/сборка берутся # из реально вычекнутого дерева, не из ветки-триггера. if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then BRANCH="${{ github.event.inputs.environment }}" else BRANCH="${{ github.ref_name }}" fi SHA="$(git rev-parse HEAD)" # Версия — из закоммиченного 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 # schema.gql не коммитится и не создаётся ничем в этом job'е, а # @coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts) # читает components/controller/schema.gql и падает ENOENT без него — валя весь # lerna publish (атомарный) молча с 30 мая. Скрипт изолированный (GraphQLSchemaBuilderModule, # без TypeORM/Mongo/Redis) — не требует БД, но типизация controller'а резолвится через # dist соседних workspace-пакетов (cooptypes/factory/notifications/inter) — ОБЯЗАТЕЛЬНО # после `lerna run build`, иначе ts-node падает на их устаревших dist-типах. - name: Generate controller schema.gql (required by sdk prepublishOnly) working-directory: components/controller run: pnpm run generate-schema - 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 }}"}'