ci(release): независимый деплой окружений — testnet/main как FF-указатели на теги dev
Typecheck / desktop (pull_request) Successful in 13m58s
Typecheck / controller (pull_request) Successful in 13m15s

Паровоз dev→testnet→main заменён независимыми указателями: каждое окружение —
самостоятельный fast-forward-указатель на выбранный релизный тег dev. Прод и тест
катятся разным темпом и на разные версии одновременно, без диверджа и конфликтов
(оба — FF-указатели на линейную историю dev).

- promote.sh <testnet|main> [ref]: FF выбранного тега (по умолчанию — последний на
  origin/dev) на ветку окружения; main больше не зависит от testnet; только вперёд.
- release.yaml: workflow_dispatch получил inputs environment+ref — откат на старую
  версию / редеплой без бампа / hotfix в один контур (то, что FF не умеет). Сборка
  идёт из вычекнутого ref, окружение — из inputs.environment. Авто-триггер по
  изменению lerna.json сохранён.
- RELEASE.md: модель и инварианты переписаны под независимые указатели.

Принципы деплоя сохранены: бамп версии один раз на dev, образы по версии,
BUILD_MODE по окружению, webhooks per env, npm publish/доки только на main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-16 06:49:12 +00:00
parent 07ec6d9a5b
commit 9c2729d243
3 changed files with 132 additions and 62 deletions
+27 -3
View File
@@ -33,7 +33,22 @@ on:
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
@@ -50,13 +65,22 @@ jobs:
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: |
# Ветка — это ветка push'а (или выбранная в workflow_dispatch).
BRANCH="${{ github.ref_name }}"
SHA="${{ github.sha }}"
# Окружение: при ручном 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 в
+61 -41
View File
@@ -1,64 +1,84 @@
# Релизный флоу: FF-промоушн dev → testnet → main
# Релизный флоу: независимый деплой окружений (FF-указатели на теги dev)
Линейная модель без merge и без конфликтов. Версию бампает `lerna` один раз на
`dev`, и тот же коммит едет вверх по **fast-forward**. `testnet` и `main` не
несут собственных коммитов — это указатели на протестированный коммит `dev`,
поэтому ветки никогда не диверджатся и merge-конфликтов не бывает.
Линейная модель без merge и без конфликтов. Версию бампает `lerna` **один раз на
`dev`** (вешает тег `vYYYY.M.D`), и дальше каждое окружение — **самостоятельный
fast-forward-указатель** на выбранный релизный тег. `testnet` и `main` не несут
собственных коммитов и не образуют паровоз: это два независимых указателя на
ЛИНЕЙНУЮ историю `dev`, поэтому ветки не диверджатся и merge-конфликтов не бывает,
**и при этом прод и тест можно катить разным темпом и на разные версии
одновременно**.
## Как релизить
```bash
# 1. Нарезать релиз на dev (бамп версии lerna, БЕЗ деплоя)
scripts/cut-release.sh
# 1. Нарезать релиз на dev (бамп версии lerna + тег, БЕЗ деплоя)
pnpm run cut # = scripts/cut-release.sh
# 2. Промоут на staging (деплой в testnet)
scripts/promote.sh testnet
# 2. Выкатить в окружения — НЕЗАВИСИМО, в любом порядке и темпе
pnpm run testnet # последний тег → testnet (staging)
pnpm run production # последний тег → main (production)
# 3. Промоут в production (деплой main + npm publish + доки)
scripts/promote.sh main
# Точечно: конкретную версию в конкретный контур
scripts/promote.sh main v2026.6.16 # эту версию в прод
scripts/promote.sh main testnet # ровно то, что сейчас на testnet, в прод
```
- `cut-release.sh` — строго на `dev`. Считает версию `vYYYY.M.D` (повторный
релиз того же дня → суффикс `-N`), вызывает `lerna version` (бамп всех
package.json + lerna.json, коммит `chore(release): publish`, тег, push `dev`).
**Деплой не запускает** — CI не слушает `dev`.
- `promote.sh testnet|main` — server-side **fast-forward** push
`origin/<source>``origin/<target>`, рабочее дерево не трогается.
Рекомендуемая практика (не обязательная): сначала `promote.sh testnet`, проверить
на staging, затем `promote.sh main <та же версия>`. Это уже не топологический
гард (как в старом паровозе), а ваш процесс — контуры независимы.
- `cut-release.sh` — строго на `dev`. Считает версию `vYYYY.M.D` (повторный релиз
того же дня → суффикс `-N`), вызывает `lerna version` (бамп всех package.json +
lerna.json, коммит `chore(release): publish`, тег, push `dev`). **Деплой не
запускает** — CI не слушает `dev`.
- `promote.sh <testnet|main> [<версия|ref>]` — server-side **fast-forward** push
выбранного релизного тега на ветку окружения. `REF` по умолчанию — последний тег
на линии `origin/dev`. Рабочее дерево не трогается. Только вперёд (FF).
## Что триггерит деплой
`.github/workflows/release.yaml` слушает **push в `testnet`/`main` с изменением
`lerna.json`**. lerna.json меняется только при релизном бампе (на `dev`), и этот
коммит входит в диапазон FF-промоушна — поэтому деплоятся именно релизы, а не
обычные feature-пуши.
`.github/workflows/release.yaml`:
Окружение определяет **ветка**, не суффикс версии:
1. **Авто** — push в `testnet`/`main` с изменением `lerna.json`. lerna.json
меняется только при релизном бампе (на `dev`), и этот коммит входит в диапазон
FF-промоушна — поэтому деплоятся именно релизы, а не обычные feature-пуши. Это
путь `promote.sh`.
2. **Ручной** (`workflow_dispatch`) — выбрать `environment` (testnet/main) и
опционально `ref` (тег/коммит). Для того, что FF не умеет: **откат** на старую
версию, **редеплой** без бампа, **hotfix** в один контур. Запуск — кнопкой в
Gitea (Actions → Release → Run workflow) или через `tea`/API.
| Ветка | Окружение | Деплой-webhook | npm publish / доки |
|-----------|----------------------|------------------------|--------------------|
| `testnet` | staging | `TESTNET_WEBHOOK_URL` | нет |
| `main` | production | `PRODUCTION_WEBHOOK_URL` | да (`main`) |
Окружение определяет **выбранный контур** (ветка push'а или `inputs.environment`),
не суффикс версии:
Версия для образов (`dicoop/*:vX`), webhook-payload и `lerna publish
from-package` читается из закоммиченного `lerna.json` — едет с коммитом по FF.
Для приёмника деплоя (playbooks) ничего не меняется: webhook по-прежнему
получает версию-строку, образы тегаются версией.
| Контур | Окружение | Деплой-webhook | npm publish / доки |
|-----------|----------------------|--------------------------|--------------------|
| `testnet` | staging | `TESTNET_WEBHOOK_URL` | нет |
| `main` | production | `PRODUCTION_WEBHOOK_URL` | да (`main`) |
Версия для образов (`dicoop/*:vX`), webhook-payload и `lerna publish from-package`
читается из закоммиченного `lerna.json` вычекнутого дерева — она «приезжает» с
тегом. Для приёмника деплоя (playbooks) ничего не меняется.
## Инварианты (не нарушать)
- **В `testnet`/`main` — только FF-промоушн, никаких прямых коммитов.** Прямой
коммит сделает ветку не-FF; `promote.sh` тогда отвергнется push'ем (это гард,
а не баг — выровняй ветку).
- **В `testnet`/`main` — только FF на тег dev, никаких прямых коммитов.** Прямой
коммит сделает ветку не-FF; `promote.sh` тогда отвергнется push'ем (гард, не
баг — выровняй ветку на нужный тег).
- **Версия бампается только на `dev`** (`cut-release.sh`). Никаких повторных
бампов на `testnet`/`main` — иначе вернутся расхождение и конфликты.
- Тег (`vX`), который вешает `lerna version`, — маркер версии и основа для
GitHub Release (`scripts/publish-release.sh`); триггером деплоя он **не**
является.
- **`promote.sh` двигает только вперёд.** Откат назад на старую версию — через
`workflow_dispatch` (environment + ref), а не force-push ветки окружения.
- Тег (`vX`) — маркер версии и основа для GitHub Release
(`scripts/publish-release.sh`); сам по себе триггером деплоя он не является.
## Откуда ушли (история)
Старые `publish-alpha.sh`/`publish-prod.sh` бампали версию **на каждой ветке**
(`-alpha-N` на testnet, чистую на main) через `git merge -X theirs` +
back-merge. Два независимых бампа за цикл + merge'и плодили расхождение → 20
package.json регулярно конфликтовали (особенно при гонке push/pull). FF-модель
это устраняет конструктивно.
- Старые `publish-alpha.sh`/`publish-prod.sh` бампали версию **на каждой ветке**
через `git merge -X theirs` + back-merge → два бампа за цикл плодили
расхождение и 20 конфликтов package.json. FF-модель это устранила конструктивно.
- Затем был **паровоз** `dev → testnet → main`: один тег ехал строго линейно,
`main` мог получить только то, что уже на `testnet`. Безопасно, но неудобно —
нельзя катить прод и тест разным темпом, нет независимого отката/hotfix.
Заменён на **независимые указатели**: `main` промоутится из `dev` (а не из
`testnet`), оба контура — самостоятельные FF-указатели на теги одной линии.
+44 -18
View File
@@ -1,35 +1,61 @@
#!/bin/bash
set -e
# Fast-forward промоушн БЕЗ merge: target-ветка просто двигается на коммит
# source-ветки. testnet и main не несут собственных коммитов — это указатели на
# протестированный коммит dev. Ветки не диверджатся → конфликты невозможны.
# Независимый деплой по окружениям. testnet и main — НЕ паровоз: каждый из них
# самостоятельный fast-forward-указатель на выбранный релизный тег dev. Можно
# катить в прод и в тест разным темпом и на разные версии одновременно —
# расхождений и конфликтов нет, потому что оба указателя живут на ЛИНЕЙНОЙ
# истории dev (двигаются только вперёд по FF).
#
# scripts/promote.sh testnet # dev → testnet (staging-деплой)
# scripts/promote.sh main # testnet → main (production-деплой)
# scripts/promote.sh testnet # последний релиз (тег) → testnet
# scripts/promote.sh main # последний релиз (тег) → main
# scripts/promote.sh main v2026.6.16 # конкретную версию → main
# scripts/promote.sh main testnet # ровно то, что сейчас на testnet → main
#
# Если push отвергнут как non-fast-forward — это ГАРД, не баг: значит в target
# попал прямой коммит, и target обогнал source. Выровняй вручную (в testnet/main
# прямых коммитов быть не должно — только FF-промоушн).
# REF по умолчанию — последний релизный тег, достижимый из origin/dev (его вешает
# scripts/cut-release.sh). Деплой стартует push'ем в ветку с изменением lerna.json
# (бамп-коммит входит в диапазон FF) — слушает .github/workflows/release.yaml.
#
# Только FF (без --force): сервер отвергнет non-fast-forward — это ГАРД, не баг
# (в target попал прямой коммит, либо ты пытаешься откатить ветку назад).
# Откат на более старую версию или редеплой без движения ветки — через
# workflow_dispatch в release.yaml (inputs: environment + ref), НЕ через этот скрипт.
usage() { echo "Использование: $0 <testnet|main> [<версия|ref>]"; }
TARGET="$1"
case "$TARGET" in
testnet) SOURCE="dev" ;;
main) SOURCE="testnet" ;;
testnet | main) ;;
*)
echo "Использование: $0 <testnet|main>"
usage
exit 1
;;
esac
git fetch origin
git fetch --tags --force origin
echo "FF-промоушн: origin/$SOURCE → origin/$TARGET"
REF="$2"
if [ -z "$REF" ]; then
# Последний релизный тег на линии origin/dev (vYYYY.M.D[-N] от cut-release.sh).
REF="$(git describe --tags --abbrev=0 origin/dev)"
fi
# Server-side fast-forward: двигаем удалённый $TARGET на коммит удалённого $SOURCE.
# Рабочее дерево не трогаем (никаких checkout/merge). Без --force: сервер
# отвергнет, если это не fast-forward.
git push origin "origin/${SOURCE}:refs/heads/${TARGET}"
# REF может быть именем ветки (testnet/main/dev), тегом или sha — разрешаем единообразно.
if git show-ref --verify --quiet "refs/remotes/origin/$REF"; then
SRC="origin/$REF"
else
SRC="$REF"
fi
REF_SHA="$(git rev-parse --verify "${SRC}^{commit}")"
REF_VERSION="$(git show "${REF_SHA}:lerna.json" \
| sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)"
echo "Деплой: $REF (v${REF_VERSION}) → $TARGET"
# Server-side fast-forward: двигаем удалённый TARGET на REF. Рабочее дерево не трогаем
# (никаких checkout/merge). Без --force: сервер отвергнет, если это не fast-forward.
git push origin "${REF_SHA}:refs/heads/${TARGET}"
if [ "$TARGET" = "main" ]; then
ENV_LABEL="PRODUCTION (+ npm publish + доки)"
@@ -38,5 +64,5 @@ else
fi
echo
echo "Готово. origin/$TARGET = origin/$SOURCE."
echo "Готово. origin/$TARGET = $REF (v${REF_VERSION})."
echo "CI задеплоит: $ENV_LABEL."