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: "**/*" globs: "**/*"
alwaysApply: false alwaysApply: true
--- ---
# Commit messages (русский) # 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-фича для пользователя - `feat` — только новая UX-фича для пользователя
- `fix` — исправление бага - `fix` — исправление бага / регрессии / вёрстки
- `chore` — конфиг, зависимости, правила, CI - `chore` / `docs` / `build` — конфиг, правила, документация (**без** релиза)
- `refactor` — рефакторинг без изменения поведения - `ci` — CI/CD, hooks (patch-релиз)
- `docs` — документация - `refactor` — рефакторинг без смены UX (patch)
- `test` — тесты - `test` / `style` / `perf` — как обычно
- `perf` — производительность
## Правила ## Правила
- Префикс `type` и `scope` — **английский**; текст после `:` — **только русский** (кириллица)
- Subject в **императиве**, без точки в конце - Subject в **императиве**, без точки в конце
- Subject и body — **на русском** - Scope — **один** идентификатор **без запятых**: `web`, `api`, `ci`, `docker`, `fleet`
- Body (опционально) — что и зачем, не как - Локальный hook `.husky/commit-msg` гоняет `commitlint`
- Scope в скобках при необходимости: `feat(domains): добавить фильтр по статусу`
## Примеры ## Примеры
``` ```
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`). 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`. 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`. Без 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 ## 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 ## Pull requests
+33 -6
View File
@@ -8,17 +8,44 @@ module.exports = {
if (scope && scope.includes(',')) { if (scope && scope.includes(',')) {
return [ return [
false, 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]; 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: { rules: {
// Subject после «:» на русском — не английский sentence-case. // Subject после «:» на русском — не английский sentence-case.
'subject-case': [0], '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` — в логе будут предупреждения о непарсящихся коммитах. На 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 ## Перезапуск упавшего job publish
+17 -1
View File
@@ -45,6 +45,11 @@ for (const { hash, subject } of commits) {
unparseable.push({ hash: hash.slice(0, 7), subject }); unparseable.push({ hash: hash.slice(0, 7), subject });
continue; 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)) { if (RELEASABLE.has(parsed.type)) {
releasable.push({ hash: hash.slice(0, 7), subject, type: 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) { 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 { } else {
console.warn('::warning:: No releasable commits since last tag — release job will no-op.'); 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); process.exit(0);