ci(hooks): добавить commit-msg и шаблон Conventional Commits
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 4s
quality / docker-check (push) Skipped
quality / web (push) Successful in 57s
quality / api (push) Successful in 41s
CD / quality (push) Successful in 1m46s
CD / publish (push) Successful in 11m48s

Без type/scope semantic-release не ставил тег и пропускал bake.
This commit is contained in:
Denozordec
2026-09-04 13:43:48 +07:00
parent dd3c378b5f
commit 64e6df8703
8 changed files with 120 additions and 25 deletions
+29 -15
View File
@@ -1,36 +1,50 @@
---
description: Conventional commits на русском языке
description: Conventional commits на русском — обязательно для semantic-release
globs: "**/*"
alwaysApply: false
alwaysApply: true
---
# Commit messages (русский)
Формат: `<type>[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
```
+12
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
# Enforce Conventional Commits (semantic-release + commitlint).
pnpm exec commitlint --edit "$1"
+2
View File
@@ -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).
+15 -2
View File
@@ -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
+33 -6
View File
@@ -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',
],
],
},
};
+9 -1
View File
@@ -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
+17 -1
View File
@@ -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);