ci(release): независимый деплой окружений — testnet/main как FF-указатели на теги dev
Паровоз 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:
@@ -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
@@ -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
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user