remove commitlint configuration and update release documentation to enforce single scope in commit messages
CI / openapi (push) Has been cancelled
CI / web (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / go (push) Has been cancelled
CI / bird2 (push) Has been cancelled
CI / commitlint (push) Has been cancelled
CI / release (push) Has been cancelled

This commit is contained in:
Denozordec
2026-05-21 17:54:41 +07:00
parent fb108ec5ab
commit 4db6438245
6 changed files with 107 additions and 3 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["@commitlint/config-conventional"]
}
+2
View File
@@ -142,6 +142,8 @@ feat(web): add module create dialog on /modules
| `.cursor/` | `chore` |
| прочее в корне | `chore` |
**Запрещено:** несколько scope через запятую (`refactor(web, httpapi): …`) — semantic-release не распознает `type`, релиз не будет (см. [docs/releasing.md](../../docs/releasing.md)).
`type` определять по **содержимому diff**, не только по пути.
## Multi-change
+2
View File
@@ -303,6 +303,8 @@ jobs:
cache-dependency-path: package-lock.json
- name: Install release tooling
run: npm ci
- name: Verify releasable commit messages
run: node scripts/commit/verify-release-commits.mjs
- name: Semantic release
run: npx semantic-release
env:
+22
View File
@@ -0,0 +1,22 @@
/** @type {import('@commitlint/types').UserConfig} */
module.exports = {
extends: ['@commitlint/config-conventional'],
plugins: [
{
rules: {
'scope-no-commas': ({ scope }) => {
if (scope && scope.includes(',')) {
return [
false,
'scope must not contain commas (semantic-release will not parse the commit type)'
];
}
return [true];
}
}
}
],
rules: {
'scope-no-commas': [2, 'always']
}
};
+17
View File
@@ -11,6 +11,8 @@ EvoBGP использует [Conventional Commits](https://www.conventionalcommi
| `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) |
| `docs`, `chore`, `test` | без релиза |
**Scope:** один идентификатор **без запятых** (`web`, `httpapi`, `api`). Заголовок `refactor(a, b): …` **не парсится** semantic-release → релиз не создаётся (commitlint на PR это тоже отклонит). Подробнее — раздел «Scope и semantic-release» ниже.
`refactor` — patch без новых функций: перестройка кода/UI при том же поведении для пользователя. По semver на одном уровне с `fix`, но семантически «мельче» `feat` (не minor).
Отдельного суффикса `1.x.y.fix` в semver нет: «fix» в Conventional Commits означает **patch** (третья цифра). Для починки пайплайна без смены продукта — `fix(ci):` или `ci:` (оба дают patch после настройки `.releaserc.json`).
@@ -62,6 +64,21 @@ API: `GET /version`, `GET /v1/version` — поля `version`, `git_sha`, `build
Web UI показывает версию из API (footer sidebar, страница «Мониторинг»).
## Scope и semantic-release
Парсер [conventional-commits-parser](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser) (его использует semantic-release) **не понимает запятые в scope**:
| Заголовок | Парсится | Релиз |
|-----------|----------|-------|
| `refactor(web): fix layout` | да, `refactor` | patch |
| `refactor(NetworkOverviewTab, NetworkSpeakersCard): fix layout` | **нет**, `type: null` | **нет** |
Правило: **один scope** из таблицы в [.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc) (`web`, `httpapi`, `api`, …).
На push в `main` job **release** запускает `scripts/commit/verify-release-commits.mjs` — при непарсящихся коммитах CI упадёт до semantic-release.
Если релиз «не создался», а CI зелёный: смотрите лог release — часто `No releasable commits`. Исправление: новый коммит с корректным заголовком (например `refactor(web): …`).
## CHANGELOG
Release notes — в Gitea Release; файл `CHANGELOG.md` генерируется в CI и прикрепляется как asset, **не** попадает в git history.
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* Warns about commits since the last tag that semantic-release cannot parse.
* Exit 0 always — semantic-release still decides release/no-op.
*/
import { execSync } from 'node:child_process';
import parser from 'conventional-commits-parser';
const RELEASABLE = new Set(['feat', 'fix', 'perf', 'ci', 'refactor']);
function lastTag() {
try {
return execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim();
} catch {
return '';
}
}
function commitsSince(ref) {
const range = ref ? `${ref}..HEAD` : 'HEAD';
const out = execSync(`git log ${range} --format=%H%x09%s`, { encoding: 'utf8' }).trim();
if (!out) return [];
return out.split('\n').map((line) => {
const [hash, subject] = line.split('\t');
return { hash: hash.trim(), subject: subject.trim() };
});
}
const tag = lastTag();
const commits = commitsSince(tag);
const unparseable = [];
const releasable = [];
for (const { hash, subject } of commits) {
const parsed = parser.sync(subject);
if (!parsed.type) {
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 });
}
}
if (commits.length === 0) {
console.log(`No new commits since ${tag || 'initial'}.`);
process.exit(0);
}
if (unparseable.length > 0) {
console.warn('::warning:: Commits not parseable by semantic-release (no version bump):');
for (const b of unparseable) {
console.warn(` ${b.hash} ${b.subject}`);
}
console.warn('Fix: single scope without commas, e.g. refactor(web): summary');
}
if (releasable.length > 0) {
console.log(`Releasable since ${tag}: ${releasable.length} commit(s).`);
} else {
console.warn('::warning:: No releasable commits since last tag — release job will no-op.');
}
process.exit(0);