# Release Automation

Ordinis uses **semantic-release** для автоматического создания tag'ов на основе
conventional commits. После merge в `main` CI анализирует commits, решает
bump (patch/minor/major), генерирует `CHANGELOG.md` и пушит tag `vX.Y.Z`.

Tag pipeline далее собирает images и ждёт manual gate на `trigger-deploy-prod`.
Prod deploy остаётся под human control — semantic-release создаёт tag,
human кликает Deploy.

## One-time setup

1. **Create Project Access Token** (GitLab UI):
   - `Settings → Access Tokens → Add new token`
   - Name: `semantic-release`
   - Role: `Maintainer`
   - Scopes: `write_repository`, `api`
   - Expiry: 1 год (renew annually)

2. **Add CI variable**:
   - `Settings → CI/CD → Variables → Add variable`
   - Key: `GITLAB_TOKEN`
   - Value: (token из шага 1)
   - Type: `Variable`
   - Flags: `Masked` ✅, `Protected` ✅ (только main + tag pipelines)

После этого release job в `.gitlab-ci.yml` будет работать.

### Auto-PR в ordinis-infra (опционально)

`propose-prod-image-bump` job открывает MR в `ordinis-infra` после каждого
release tag — bump'ит `image.tag` в `values-prod.yaml` на свежий immutable
SHA tag (e.g. `v2-16-0-abc12345`). Чинит historical bug когда manual deploys
через infra repo получали `:latest` (stale cache → split-brain pods).

Setup:

3. **Create Group Access Token** в `2-6/2-6-4/terravault`:
   - `Group → Settings → Access Tokens → Add new token`
   - Name: `ordinis-ci-infra-bump`
   - Role: `Developer`
   - Scopes: `write_repository`, `api`
   - Expiry: 1 год

4. **Add CI variable в ordinis project (367)**:
   - Key: `INFRA_PROJECT_TOKEN`
   - Value: (token из шага 3)
   - Flags: `Masked` ✅, `Protected` ✅

Без этой переменной job exits early (`allow_failure: true`), tag pipeline
не блокируется. Manual MR в `ordinis-infra` остаётся всегда доступен как
fallback.

## Commit conventions

Conventional Commits (https://www.conventionalcommits.org/):

| Type | Bump | Section в CHANGELOG |
|------|------|---------------------|
| `feat:` | **minor** | ✨ Features |
| `fix:` | patch | 🐛 Fixes |
| `perf:` | patch | ⚡ Performance |
| `refactor:` | patch | ♻️ Refactoring |
| `revert:` | patch | ⏪ Reverts |
| `docs:` | none | 📝 Docs |
| `chore:` | none | hidden |
| `test:` | none | hidden |
| `style:` | none | hidden |
| `ci:` | none | hidden |
| `build:` | none | hidden |

**Breaking changes** — добавь `BREAKING CHANGE:` в footer commit message → major bump.

```
feat(api): drop legacy v1 endpoint

BREAKING CHANGE: /api/v1/* paths больше не поддерживаются.
Все клиенты должны мигрировать на /api/v2.
```

> **⚠ Lesson learned (v2.0.0 false-major):** до 2026-05-12 в .releaserc.json
> noteKeywords содержал bare `"BREAKING"`. Слово «Breaking» в обычном
> commit body (например «Breaking down PATCH into atomic ops…») триггерило
> major bump. Конфиг исправлен — оставлены только canonical
> `BREAKING CHANGE` / `BREAKING CHANGES`. Когда нужен major — используй
> точно эти триггеры в footer.

## Skip release for specific commit

Если хочешь смержить `feat:` или `fix:` но **не** делать release сейчас —
используй non-release type (`chore:`, `docs:`, etc).

```
chore(deps): bump axios 1.7 -> 1.8
```

> ⚠ **НЕ используй `[skip ci]`** в commit message — см. раздел
> «Skip-ci marker trap» ниже.

## Loop prevention

Release job создаёт commit `chore(release): vX.Y.Z`. Loop prevention работает
через releaseRules в `.releaserc.json` — chore-тип имеет `release: false` →
semantic-release на release commit'е detect'ит «no releasable commits» →
exit 0 → нет нового tag → нет loop'а.

> **Historical note:** до MR !154 (2026-05-12) сюда добавлялось `[skip ci]`.
> Это работало для main pipeline'а, НО skip'ало и tag pipeline на том же SHA
> → images с tag'ом `v*-*-*-<sha>` не собирались → `propose-prod-image-bump`
> auto-PR не запускался → manual ручной MR в infra-repo. Удалено.

## ⚠ Skip-ci marker trap

GitLab pipeline pattern matcher honors `[skip ci]` в commit subject ИЛИ body —
**где угодно в message'е**. semantic-release auto-генерирует release notes
из subject+body всех commit'ов с момента последнего tag'а.

**Trap:** если хоть один commit body содержит литеральное `[skip ci]` (даже
описывая фикс этого marker'а в комментариях/changelog), эта строка попадёт
в release commit body → GitLab пропустит tag pipeline → нет images →
manual escape через `glab api -X POST projects/367/pipeline?ref=vX.Y.Z`.

**Реальный случай:** v2.16.1 (2026-05-12). Commit `fix(release): remove
[skip ci] из semantic-release commit message` — literal `[skip ci]` в
subject. Release notes v2.16.1 содержали эту строку → tag pipeline 6831
skipped. Pipeline пришлось trigger вручную.

**Защита:** CI job `commit-msg-lint` (см. `.gitlab-ci.yml`) fail'ит любой
commit с substring `[skip ci]` в subject или body. Запускается на MR
pipeline'ах и feature-branch push'ах.

**Если нужно упомянуть skip-ci в commit message:**

- ✅ `fix: remove skip-ci marker` (дефис вместо пробела)
- ✅ `docs: explain GitLab pipeline skip syntax`
- ✅ `chore: remove the skip-ci directive from .releaserc`
- ❌ `fix: remove [skip ci]` (literal — fails lint)
- ❌ `update [skip ci] policy` (literal — fails lint)

## Manually trigger skipped tag pipeline

Если tag pipeline всё-таки попал в trap (legacy commits до commit-msg-lint
или CI bypass через --no-verify):

```bash
GITLAB_HOST=git.nstart.cloud glab api -X POST "projects/367/pipeline?ref=vX.Y.Z"
```

Создаёт новый pipeline на той же tag-ref. Escape hatch, не regular workflow.

## Branch policy

Релизы создаются ТОЛЬКО с `main`. Feature branches (`feat/*`, `fix/*`) собираются
для staging но НЕ создают tag'и. Релиз = merge в main + успешный staging deploy +
наличие релизных commits.

## Local dry-run

Проверить что semantic-release решит сделать без push:

```bash
cd ordinis-admin-ui
pnpm install
npx semantic-release --dry-run --no-ci
```

Покажет следующую версию + preview CHANGELOG entries.

## Rollback

Если случайно зарелизили — удали tag:

```bash
git tag -d v1.3.1
git push origin :refs/tags/v1.3.1
```

GitLab Releases UI — удали release object отдельно.
