ci(lint): forbid literal skip-ci marker в commit messages

GitLab платформа skip'ает pipeline на любом commit'е чей subject или body
содержит pattern с square brackets вокруг 'skip ci'. Trap: semantic-release
включает release notes из всех commit'ов в release commit body — если хотя
бы один commit имеет этот pattern в subject/body, release tag pipeline
скипается → нет images → нет auto-PR.

Реальный случай: v2.16.1 — commit fix(release): 'remove ... из semantic-
release commit message' содержал паттерн дословно. Pipeline 6831 (v2.16.1
tag) skipped → пришлось trigger через API.

Новый CI job 'commit-msg-lint' (stage: test):
- Запускается на MR pipeline'ах и feature-branch push'ах
- Скипает chore(release) commit'ы (semantic-release auto-bot)
- Для каждого commit'а в range проверяет subject+body на square-bracket
  skip-ci pattern
- Fails CI с ясной подсказкой (как переформулировать) при match

RELEASE.md обновлён с разделом 'Skip-ci marker trap': примеры
правильных формулировок (skip-ci marker, the directive, etc) и
escape hatch (manual API trigger).
This commit is contained in:
Zimin A.N.
2026-05-13 00:54:19 +03:00
parent 1e5e6b59cb
commit 1da515276c
2 changed files with 144 additions and 5 deletions
+52 -5
View File
@@ -88,18 +88,65 @@ BREAKING CHANGE: /api/v1/* paths больше не поддерживаются.
## Skip release for specific commit
Если хочешь смержить `feat:` или `fix:` но **не** делать release сейчас —
добавь `[skip ci]` в commit message ИЛИ используй non-release type
(`chore:`, `docs:`, etc).
используй 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 [skip ci]`. `[skip ci]`
стандартный GitLab marker, новый pipeline не триггерится. Версия package.json
+ CHANGELOG.md обновляются в этом коммите.
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