From 64e6df8703ffdf6ba8638eeec6ce3ea43341f8e7 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 4 Sep 2026 13:43:48 +0700 Subject: [PATCH] =?UTF-8?q?ci(hooks):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20commit-msg=20=D0=B8=20=D1=88=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=BD=20Conventional=20Commits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Без type/scope semantic-release не ставил тег и пропускал bake. --- .cursor/rules/commit-messages-ru.mdc | 44 +++++++++++++++-------- .gitmessage | 12 +++++++ .husky/commit-msg | 3 ++ AGENTS.md | 2 ++ CONTRIBUTING.md | 17 +++++++-- commitlint.config.cjs | 39 ++++++++++++++++---- docs/releasing.md | 10 +++++- scripts/commit/verify-release-commits.mjs | 18 +++++++++- 8 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 .gitmessage create mode 100644 .husky/commit-msg diff --git a/.cursor/rules/commit-messages-ru.mdc b/.cursor/rules/commit-messages-ru.mdc index c17e291..df6e12f 100644 --- a/.cursor/rules/commit-messages-ru.mdc +++ b/.cursor/rules/commit-messages-ru.mdc @@ -1,36 +1,50 @@ --- -description: Conventional commits на русском языке +description: Conventional commits на русском — обязательно для semantic-release globs: "**/*" -alwaysApply: false +alwaysApply: true --- # Commit messages (русский) -Формат: `[optional scope]: <описание>` +Формат: `тип(scope): описание на русском` + +semantic-release на push в `main` создаёт тег и публикует Docker **только** если среди коммитов с прошлого тега есть `feat` / `fix` / `perf` / `ci` / `refactor` (или BREAKING). +Сообщения вроде `Init Commit`, `Update CDN Manager: …`, `Enhance …` **не парсятся** → CD пишет `No releasable commits — skipping image publish`. ## Типы - `feat` — только новая UX-фича для пользователя -- `fix` — исправление бага -- `chore` — конфиг, зависимости, правила, CI -- `refactor` — рефакторинг без изменения поведения -- `docs` — документация -- `test` — тесты -- `perf` — производительность +- `fix` — исправление бага / регрессии / вёрстки +- `chore` / `docs` / `build` — конфиг, правила, документация (**без** релиза) +- `ci` — CI/CD, hooks (patch-релиз) +- `refactor` — рефакторинг без смены UX (patch) +- `test` / `style` / `perf` — как обычно ## Правила +- Префикс `type` и `scope` — **английский**; текст после `:` — **только русский** (кириллица) - Subject в **императиве**, без точки в конце -- Subject и body — **на русском** -- Body (опционально) — что и зачем, не как -- Scope в скобках при необходимости: `feat(domains): добавить фильтр по статусу` +- Scope — **один** идентификатор **без запятых**: `web`, `api`, `ci`, `docker`, `fleet` +- Локальный hook `.husky/commit-msg` гоняет `commitlint` ## Примеры ``` -fix(frontend): заменить raw table на shadcn Table на странице доменов +feat(fleet): добавить CRUD нод и алиасов -feat(certificates): добавить предупреждение об истечении срока +fix(web): выровнять AttentionQueue на дашборде -chore(rules): консолидировать правила shadcn/ui для Cursor +ci(hooks): добавить commit-msg для Conventional Commits + +chore(docs): описать Traefik deploy +``` + +## Запрещено + +``` +Init Commit +Update CDN Manager: Add … +Enhance CDN Manager: … +Refactor: Remove … +feat: add nodes ``` diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..e92c32b --- /dev/null +++ b/.gitmessage @@ -0,0 +1,12 @@ +# Conventional Commits — шаблон для git commit -t / commit.template +# +# Формат: type(scope): описание на русском +# Примеры: +# feat(fleet): добавить CRUD нод и алиасов +# fix(web): исправить фильтр синхронизации +# ci(hooks): добавить commit-msg для commitlint +# +# Releasable (semantic-release → Docker): feat | fix | perf | ci | refactor +# Без релиза: docs | chore | test | style | build +# +# НЕ использовать: Init Commit | Update … | Enhance … | английский subject diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..fe1d1b3 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# Enforce Conventional Commits (semantic-release + commitlint). +pnpm exec commitlint --edit "$1" diff --git a/AGENTS.md b/AGENTS.md index f541912..3e818e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,3 +76,5 @@ GET /api/v1/cloudflare/zones Gitea: `.gitea/workflows/{ci,cd,quality}.yaml`. Образы: `deploy/docker` (`cdnmanager` + alias `cdn-manager`). Prod: [`docs/deploy-traefik.md`](docs/deploy-traefik.md) — `deploy/docker-compose.traefik.yml` + `deploy/env.traefik.example`. Без Traefik: [`docs/deploy-docker.md`](docs/deploy-docker.md) · Compose: `docker-compose.yml` / `deploy/compose/docker-compose.example.yaml`. + +**Коммиты:** только Conventional Commits (`feat`/`fix`/… + русский subject). Иначе CD: `No releasable commits — skipping image publish`. См. [`.cursor/rules/commit-messages-ru.mdc`](.cursor/rules/commit-messages-ru.mdc), [`docs/releasing.md`](docs/releasing.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df112d9..ca19f8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,9 +10,22 @@ ## Commits -Format: `type(scope): description` +**Обязательно** Conventional Commits — иначе semantic-release на `main` не создаст тег и **не опубликует Docker-образ**. -Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` +Формат: `type(scope): описание на русском` + +| Type | Релиз | +|------|--------| +| `feat` | minor | +| `fix`, `perf`, `ci`, `refactor` | patch | +| `docs`, `chore`, `test`, `style`, `build` | нет | + +Scope — один идентификатор без запятых (`web`, `api`, `ci`, `docker`, `fleet`). +Локально: husky `.husky/commit-msg` → `commitlint`. Шаблон: [`.gitmessage`](.gitmessage) (`git config commit.template .gitmessage`). +Правила Cursor: [`.cursor/rules/commit-messages-ru.mdc`](.cursor/rules/commit-messages-ru.mdc). + +Плохо: `Init Commit`, `Update CDN Manager: Add…`. +Хорошо: `feat(fleet): добавить CRUD нод и алиасов`. ## Pull requests diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 5bf754c..55497c1 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,17 +8,44 @@ module.exports = { if (scope && scope.includes(',')) { return [ false, - 'scope must not contain commas (semantic-release will not parse the commit type)' + 'scope must not contain commas (semantic-release will not parse the commit type)', ]; } return [true]; - } - } - } + }, + /** Subject after «:» must contain Cyrillic (project convention). */ + 'subject-cyrillic': ({ subject }) => { + if (!subject) return [true]; + if (/[а-яёА-ЯЁ]/.test(subject)) return [true]; + return [ + false, + 'subject after «:» must be in Russian (Cyrillic), e.g. feat(web): добавить экран нод', + ]; + }, + }, + }, ], rules: { // Subject после «:» на русском — не английский sentence-case. 'subject-case': [0], - 'scope-no-commas': [2, 'always'] - } + 'scope-no-commas': [2, 'always'], + 'subject-cyrillic': [2, 'always'], + 'type-enum': [ + 2, + 'always', + [ + 'feat', + 'fix', + 'perf', + 'ci', + 'refactor', + 'docs', + 'chore', + 'test', + 'style', + 'build', + 'revert', + ], + ], + }, }; diff --git a/docs/releasing.md b/docs/releasing.md index 3fc6f14..aab4f4a 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -63,7 +63,15 @@ Semver из git-тега пробрасывается в образ как `APP_ На push в `main` job **publish** запускает `scripts/commit/verify-release-commits.mjs` — в логе будут предупреждения о непарсящихся коммитах. -Если релиз «не создался», а CI зелёный: смотрите лог release — часто `No releasable commits`. Исправление: новый коммит с корректным заголовком (например `fix(web): …`). +Если релиз «не создался», а CI зелёный: смотрите лог **Detect new release** / **Verify releasable** — часто `No releasable commits`. + +Причины: + +1. Subject не Conventional Commits (`Update CDN Manager: …`, `Init Commit`) — parser не видит `type`. +2. Только `docs` / `chore` / `test` с прошлого тега — намеренный no-op. +3. Scope с запятыми: `refactor(a, b): …` — type не парсится. + +Исправление: новый коммит с корректным заголовком (например `fix(web): …` или `feat(fleet): …`) и push в `main`. Локально husky `commit-msg` + commitlint; шаблон [`.gitmessage`](../.gitmessage). ## Перезапуск упавшего job publish diff --git a/scripts/commit/verify-release-commits.mjs b/scripts/commit/verify-release-commits.mjs index d90dba2..a02b5d1 100644 --- a/scripts/commit/verify-release-commits.mjs +++ b/scripts/commit/verify-release-commits.mjs @@ -45,6 +45,11 @@ for (const { hash, subject } of commits) { unparseable.push({ hash: hash.slice(0, 7), subject }); continue; } + // semantic-release expects lowercase types (feat|fix|…); "Refactor: …" is not releasable. + if (parsed.type !== parsed.type.toLowerCase()) { + unparseable.push({ hash: hash.slice(0, 7), subject }); + continue; + } if (RELEASABLE.has(parsed.type)) { releasable.push({ hash: hash.slice(0, 7), subject, type: parsed.type }); } @@ -64,9 +69,20 @@ if (unparseable.length > 0) { } if (releasable.length > 0) { - console.log(`Releasable since ${tag}: ${releasable.length} commit(s).`); + console.log(`Releasable since ${tag || 'initial'}: ${releasable.length} commit(s).`); + for (const r of releasable) { + console.log(` ${r.hash} ${r.type} ${r.subject}`); + } } else { console.warn('::warning:: No releasable commits since last tag — release job will no-op.'); + console.warn( + 'Need at least one: feat|fix|perf|ci|refactor with Conventional Commits, e.g. feat(fleet): …', + ); + if (!tag) { + console.warn( + '::warning:: No git tags yet — first release (v1.0.0) also requires a releasable commit on HEAD history.', + ); + } } process.exit(0);