---
name: release-react18
description: React 18-релиз triplex-next (1.Y.0) — вторая половина выпуска: бамп версии в ветке prerelease-X.Y.0, заготовка release notes на следующую версию, PR в main, мерж по зелёному CI (с учётом branch protection), GitHub Release, публикация в npm, после которой dist-tags.latest встаёт на 1.Y.0. Под-skill для release; запускается ПОСЛЕ подтверждённой публикации 0.Y.0. Ручной путь — еженедельный релиз делает release-weekly.yml по расписанию GitHub.
---

# release-react18

Выпускает React 18-версию `1.Y.0` из ветки `main`.

**Это вторая половина релиза.** Вызывается из skill
[`release`](../release/SKILL.md) после подтверждённой публикации `0.Y.0`.
Порядок важен: во внутренний npm registry пакет попадает зеркалированием,
а не `npm publish` — dist-tags туда не проносятся, и `latest` определяется
порядком появления версий. Публикуясь второй, `1.Y.0` забирает `latest`
себе — и тем закрывает промежуточное состояние, в котором он стоял
на `0.Y.0`.

Отсюда же следует срочность: пока эта половина не вышла, `npm install`
из внутреннего registry отдаёт всем React 17-сборку. На npmjs с TRI-146
этого не происходит — `0.Y.0` уходит под тегом `react17`, и `latest`
остаётся на прежней `1.x`.

Запустить отдельно можно, чтобы довыпустить пару к уже вышедшей `0.Y.0` —
этим же снимается зависший на ней `latest`.

Это **релиз в npm**. Точка невозврата — создание GitHub Release: после неё
[`release.yml`](../../../.github/workflows/release.yml) сам собирает пакет,
публикует его и деплоит Storybook. Опубликованную версию нельзя отозвать.
Гейты ниже обязательны, обходить их нельзя.

## Входные данные

- `VERSION` — релизная версия, формат `1.Y.0`. Выпускаются только минорные
  релизы: `Y` = текущий минор + 1, патч всегда `0`. Если запрошено иное
  (патч-релиз, скачок минора) — остановись и спроси.
- `NEXT` — `1.(Y+1).0`. На неё создаётся **заготовка** release notes, в которой
  будут копиться изменения до следующего релиза.
- `TASK` — номер задачи Linear на этот релиз, формат `TRI-XXX`. Идёт префиксом
  в релизный коммит по [`docs/ai/commits.md`](../../../docs/ai/commits.md).
  Если задачи нет — заведи её через skill `create-task` («Релиз
  <VERSION>») и возьми её номер; не выдумывай номер и не используй legacy-
  префикс `TRIPLEX-XXX`.
- `V0` — парная React 17-версия `0.Y.0`, выпущенная первой половиной.
  Нужна для гейта шага 1: без подтверждённой публикации `V0` релиз `1.Y.0`
  не создаётся.



## 1. Валидация

```bash
git status --porcelain
git rev-parse --abbrev-ref HEAD
git fetch origin main
git show origin/main:package.json | grep -m1 '"version"'
gh release view <VERSION> 2>&1 | head -3
npm view @sberbusiness/triplex-next@<V0> version
```

Проверки. **При любом провале — остановись и сообщи разработчику**, ничего
не чини молча:

- `VERSION` — валидный semver вида `1.Y.0`, минор ровно на 1 больше версии
  **в `origin/main`**. Сравнивать нужно именно с ней, а не с рабочей копией
  (`node -p "require('./package.json').version"`): на этом шаге чекаут ещё
  не переключён и может стоять на произвольной ветке.
- Релиза/тега `VERSION` ещё нет (`gh release view` отвечает `release not found`).
- **Парная `V0` уже опубликована в npm** — `npm view @sberbusiness/triplex-next@<V0> version`
  резолвится. Это гейт порядка: React 17-половина обязана выйти первой, иначе
  `dist-tags.latest` останется на ней. **Исключений нет:** нет `V0`
  в реестре — остановись.

  Но «нет» — только после ожидания индексации. npm принимает publish сразу,
  а отдаёт версию через минуты (TRI-147 — 2–3, TRI-151 — 8,5). Если `V0`
  выпущена только что — при догоне половины идут подряд, — один `npm view`
  даст ложный отказ. Ждать — по [циклу ниже](#ожидание-индексации-npm)
  с `<V0>`; останавливайся, только если версия не появилась за 15 минут.

  Отдельный запуск ради починки зависшего `latest` этому не противоречит:
  `latest` может застрять на `0.x` только после публикации `V0`, то есть гейт
  в такой ситуации заведомо пройден. Если `V0` не опубликована, а `latest`
  всё равно указывает на `0.x` — это другая проблема, и решать её выпуском
  `1.Y.0` нельзя.
- **Содержимое `main` целиком вошло в `V0`.** `V0` собрана из `release-0`,
  куда перелит `main`; если после перелития в `main` что-то влили, `VERSION`
  соберётся вместе с этим и разойдётся с `V0` (TRI-155). Сверяем с тегом
  `V0`, а не с вершиной `release-0`: её могли уже перелить под следующую
  неделю, и сверка с ней прошла бы ложно.

  ```bash
  git fetch origin main --tags
  BASE=$(git merge-base origin/main "<V0>")
  git diff --quiet "$BASE" origin/main \
    && echo "ok: содержимое main целиком в <V0>" \
    || git log --oneline "$BASE..origin/main"
  ```

  Сравнивается содержимое, а не история: откаченный (revert) в `main` мерж
  проверку проходит — дерево то же. Есть расхождение — остановись и покажи
  разработчику список коммитов: либо откатить их в `main` до выпуска,
  либо осознанно выпустить `VERSION` с ними и записать в release notes,
  чего нет в `V0`. Решает разработчик.
- `stories/release-notes/v1/<VERSION>.mdx` существует **и не пустой** по
  предикату заготовки (ниже). Если файла нет или он заготовка — **публиковать
  нечего**, остановись.
- Файла `stories/release-notes/v1/<NEXT>.mdx` ещё нет.
- Рабочее дерево чистое. Исключение: уже созданный вручную untracked
  `<NEXT>.mdx` — он войдёт в релизный коммит.

### Предикат заготовки

Общий канон для обоих под-скилов: `release-react17` применяет его к
`v0/<V0>.mdx`, решая, переносить ли туда notes из `v1`.

> Выброси из файла строки `import …`, `<Meta … />`, `<Title>…</Title>`,
> одиночные `<Heading>…</Heading>` и пустые строки. Если ничего не осталось —
> это заготовка; если что-то осталось — файл заполнен.

Побайтовое сравнение с шаблоном использовать нельзя: любая ручная правка
пробелов или перевода строки его ломает.

## 2. Ветка `prerelease-<VERSION>`

Релизная ветка называется `prerelease-<VERSION>` (например `prerelease-1.40.0`).
В ней же обычно уже лежат фиксы, вошедшие в релиз.

```bash
git rev-parse --verify --quiet prerelease-<VERSION>            # локально
git ls-remote --heads origin prerelease-<VERSION>              # на origin
```

- Текущая ветка уже `prerelease-<VERSION>` → работаем в ней.
- Ветка существует, но мы не в ней → `git checkout prerelease-<VERSION>`,
  затем `git pull` (если есть upstream).
- Ветки нет → создать от актуального main:
  ```bash
  git checkout main && git pull origin main && git checkout -b prerelease-<VERSION>
  ```

Если ветка существует и отстала от `origin/main` — влей main в неё
(`git merge origin/main`). Конфликты здесь — сигнал, что что-то не так:
остановись и покажи разработчику.

## 3. Бамп версии

```bash
npm version <VERSION> --no-git-tag-version
```

Обновляет `package.json` и `package-lock.json`. Флаг `--no-git-tag-version`
обязателен — тег создаётся отдельно, на шаге GitHub Release.

## 4. Заготовка release notes на следующую версию

Создай `stories/release-notes/v1/<NEXT>.mdx` (если разработчик уже создал его
вручную — оставь как есть, не перезаписывай):

```mdx
import { Meta, Title, Heading } from "@storybook/addon-docs/blocks";

<Meta title="release-notes/v1/<NEXT>" />

<Title>v<NEXT></Title>

<Heading>Изменения</Heading>
```

Подставь `<NEXT>` во все три места. Файл заканчивается переводом строки.

## 5. Коммит и PR

Стейдж **только** релизные файлы — не `git add -A` / `git add .`:

```bash
git add package.json package-lock.json stories/release-notes/v1/<NEXT>.mdx
```

Коммит от бота, если задан токен (канон — [`docs/ai/commits.md`](../../../docs/ai/commits.md)):

```bash
if [ -n "$TRIPLEX_BOT_GH_TOKEN" ]; then
  BOT_LOGIN=$(GH_TOKEN="$TRIPLEX_BOT_GH_TOKEN" gh api user -q .login)
  BOT_EMAIL=$(GH_TOKEN="$TRIPLEX_BOT_GH_TOKEN" gh api user -q '"\(.id)+\(.login)@users.noreply.github.com"')
  git -c user.name="$BOT_LOGIN" -c user.email="$BOT_EMAIL" commit -m "<TASK> Подготовка релиза <VERSION>"
else
  git commit -m "<TASK> Подготовка релиза <VERSION>"
fi
```

Если упал pre-commit hook — исправь причину. `--no-verify` запрещён.

```bash
git push -u origin prerelease-<VERSION>
if [ -n "$TRIPLEX_BOT_GH_TOKEN" ]; then export GH_TOKEN="$TRIPLEX_BOT_GH_TOKEN"; fi
gh pr create --base main --head prerelease-<VERSION> --title "New release <VERSION>" \
  --body "Релиз <VERSION>: версия в package.json + заготовка release notes <NEXT>."
```

## 6. Дождаться зелёного CI и смержить

```bash
gh pr checks <PR_NUMBER> --watch
```


**Гейт.** Мержить можно только при полностью зелёных проверках. Красный CI —
остановись, покажи разработчику лог упавшей проверки и **не мержь**. Не
перезапускай проверки повторно в надежде на «моргнуло».

```bash
gh pr merge <PR_NUMBER> --merge --delete-branch
git checkout main && git pull origin main
```

На `main` включена branch protection: пушить и мержить могут только
аккаунты из списка restrictions, плюс требуется апрув code owner'а. PR,
созданный ботом, апрува не получит — бот не может апрувить сам себя.

Если мерж отклонён политикой (`base branch policy prohibits the merge` /
`Merging into a protected base branch is not permitted`) — **не обходи**
через прямые пуши. Мерж делает человек с правами администратора:

```bash
gh pr merge <PR_NUMBER> --merge --delete-branch --admin
```

Флаг `--admin` обходит требования защиты, включая апрув code owner'а; без
админ-прав он не сработает. Если прав нет — остановись и отдай эту команду
человеку в отчёте.

Тянуть с мержем нельзя: к этому моменту `0.Y.0` уже опубликована и `latest`
стоит на ней (см. «Если `latest` завис на 0.x» в
[`release-react17`](../release-react17/SKILL.md)).

Убедись, что версия в main действительно обновилась:

```bash
node -p "require('./package.json').version"   # должно быть <VERSION>
```

## 7. Собрать текст release notes

Преобразуй `stories/release-notes/v1/<VERSION>.mdx` в markdown по канону из
[`release`](../release/SKILL.md) (раздел «Преобразование MDX → release notes»):
убрать `import` / `<Meta>` / `<Title>`, `<Heading>X</Heading>` → `## X`.
Результат — во временный файл в скретчпаде, не в репозиторий.

Покажи готовый текст разработчику перед созданием релиза.

## 8. Создать GitHub Release

**Точка невозврата** — после этого шага пакет уедет в npm.

Этот skill — **ручной путь**. Еженедельный релиз целиком делает
[`release-weekly.yml`](../../../.github/workflows/release-weekly.yml) по
расписанию GitHub: из облачной сессии релиз не выпустить, прокси режет
`gh release create`, `gh workflow run` и push тегов (см.
[`docs/ai/commits.md`](../../../docs/ai/commits.md), «Облачные сессии»).

```bash
node scripts/releaseNotesMd.js stories/release-notes/v1/<VERSION>.mdx > /tmp/notes-<VERSION>.md
gh release create <VERSION> --target main --title "<VERSION>" --latest \
  --notes-file /tmp/notes-<VERSION>.md
```

Флаг `--latest` для `1.x` обязателен: именно он возвращает пометку Latest
с React 17-сборки, на которой она стоит с публикации `0.Y.0`.


## 9. Дождаться публикации и проверить npm

[`release.yml`](../../../.github/workflows/release.yml) делает всё остальное:
сборку, `npm publish`, `dist-<VERSION>.zip` в ассеты, деплой Storybook.
Запускается событием `release: published` от созданного тобой релиза.

```bash
gh run list --workflow=release.yml --limit 1
gh run watch <RUN_ID> --exit-status
```

Если `gh run watch` не дождался завершения — отчёт: «релиз создан,
публикация не подтверждена». Сам релиз уже существует, отзывать его нельзя.

После успешного run — проверь, что версия реально в реестре. Сначала дождись
индексации: сразу после publish npm ещё отдаёт прежний `latest`, и одиночная
проверка дала бы ложную тревогу — именно так 25.09 упал `verify`
еженедельного прогона (TRI-153). Цикл — [ниже](#ожидание-индексации-npm),
с `<VERSION>` и `latest`. Потом:

```bash
npm view @sberbusiness/triplex-next@<VERSION> version
npm view @sberbusiness/triplex-next dist-tags --json
npm view @sberbusiness/triplex-next dist-tags --json \
  --@sberbusiness:registry=https://registry.npmjs.org/ \
  --registry=https://registry.npmjs.org/
```

Ожидается: версия резолвится и **`dist-tags.latest` = `<VERSION>`** в обеих
командах. Это главная проверка всего релиза: во внутреннем registry `latest`
до публикации соответствовал `<V0>`, и именно этот шаг возвращает его
на React 18-сборку.

Если `latest` не встал на `<VERSION>` **и после ожидания индексации** —
сообщи немедленно и не считай релиз завершённым: `npm install` продолжает
отдавать React 17-версию. Если сама `<VERSION>` в реестре так и не
появилась, `dist-tag add` не сработает — публикация не дошла, смотри лог
run. Починка, когда версия есть, а тег не встал:

```bash
npm dist-tag add @sberbusiness/triplex-next@<VERSION> latest \
  --@sberbusiness:registry=https://registry.npmjs.org/ \
  --registry=https://registry.npmjs.org/
```

Оба флага обязательны: пакет публикуется в `registry.npmjs.org`, а локальный
`@sberbusiness:registry` у разработчика смотрит во внутренний registry.
Scoped-настройка приоритетнее, и одного `--registry` не хватит — тег уехал бы
не туда. Сверься с эффективным конфигом перед запуском:

```bash
npm config get @sberbusiness:registry
```

Команду выполняй **только с явным подтверждением разработчика** — она меняет
`latest` для всех потребителей пакета. То же правило в
[`release-react17`](../release-react17/SKILL.md), «Если `latest` завис на 0.x».

`dist-tags.react17` = парная `0.Y.0` — это вывод **скоупнутой** команды выше:
теги живут только на npmjs, и с TRI-146 React 17-половина публикуется с явным
`--tag react17`. Во внутреннем registry тегов нет, там всё решает порядок
публикаций.

**Если workflow упал** — остановись и покажи разработчику лог. Не создавай
релиз повторно и не удаляй тег без явного разрешения. Частая причина —
несовпадение версии в `dist/package.json` и тега; workflow проверяет это явно.

Storybook релиза: `https://storybook.triplex-dev.ru/releases/<VERSION>`.

## Итог

Верни разработчику (или вызвавшему оркестратору):

- версию, ссылку на смерженный PR и на GitHub Release;
- подтверждение из npm: версия резолвится, **`dist-tags.latest` = `VERSION`**;
- ссылку на Storybook релиза;
- путь к созданной заготовке `<NEXT>.mdx`;
- если ветку `prerelease-<VERSION>` удалить не удалось — напоминание сделать
  это вручную.

Если skill запускался напрямую, а не из [`release`](../release/SKILL.md) —
скажи, для чего: довыпуск пары к вышедшей `0.Y.0` (тогда релиз завершён) или
починка зависшего `latest`.

## Ожидание индексации npm

npm отдаёт свежую версию и `dist-tags` не сразу после publish. Опрашивай
npmjs напрямую, в обход кеша: `npm view` кеширует пакумент на 5 минут
и может отдавать устаревший `latest`. Один заход — не дольше 5 минут даже
при зависшем реестре (5 попыток по ≤ 60 с, лимит Bash-команды 10 минут);
не появилось — повтори, всего до трёх заходов (15 минут). Только после
этого делай вывод «не опубликована» / «тег не встал».

```bash
# версия появилась в реестре
URL='https://registry.npmjs.org/@sberbusiness%2Ftriplex-next/<ВЕРСИЯ>'
for _ in $(seq 1 5); do
  curl -fsS --max-time 30 -H 'Cache-Control: no-cache' "$URL" >/dev/null 2>&1 && { echo "есть"; break; }
  sleep 30
done

# latest встал на версию
TAGS='https://registry.npmjs.org/-/package/@sberbusiness%2Ftriplex-next/dist-tags'
for _ in $(seq 1 5); do
  L=$(curl -fsS --max-time 30 -H 'Cache-Control: no-cache' "$TAGS" 2>/dev/null | jq -r '.latest // ""' 2>/dev/null)
  [ "$L" = "<ВЕРСИЯ>" ] && { echo "latest = $L"; break; }
  echo "latest = ${L:-?}, ждём"; sleep 30
done
```

Во внутреннем registry версия появляется зеркалированием, со своей
задержкой — голый `npm view` проверяй после того, как npmjs отдал версию.

## Жёсткие ограничения

- Не запускать без явно указанной версии.
- Не создавать релиз `1.Y.0`, пока не подтверждена публикация парной `0.Y.0`
  в npm — от порядка зависит, на чём останется `dist-tags.latest`.
- Не считать релиз завершённым, пока `dist-tags.latest` не равен `VERSION`.
- Пустые/отсутствующие release notes `<VERSION>.mdx` — стоп, релиза нет.
- Не мержить PR с красным CI и не создавать релиз до мержа PR.
- Не редактировать содержимое `<VERSION>.mdx` — notes писались по ходу
  разработки.
- Не удалять и не пересоздавать уже опубликованные теги и релизы, не
  публиковать в npm руками в обход workflow.
- Не форс-пушить.
- `--no-verify` запрещён: упал hook — чини причину.
- Токен `TRIPLEX_BOT_GH_TOKEN` не печатать и не логировать.
