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:
@@ -67,6 +67,98 @@ default:
|
|||||||
.docs-changes: &docs-changes
|
.docs-changes: &docs-changes
|
||||||
- "docs/**/*"
|
- "docs/**/*"
|
||||||
|
|
||||||
|
# ─── LINT ──────────────────────────────────────
|
||||||
|
# Commit-message lint: ловит literal `[skip ci]` в subject/body.
|
||||||
|
#
|
||||||
|
# Контекст trap'ы:
|
||||||
|
# 1. semantic-release auto-генерирует release notes из commit'ов с момента
|
||||||
|
# последнего tag'а — включает subject + body всех commit'ов.
|
||||||
|
# 2. Если хотя бы один из этих commit'ов содержит литеральное `[skip ci]`
|
||||||
|
# (даже описывая что фиксят его, e.g. 'fix: remove [skip ci] из...'),
|
||||||
|
# эта строка попадает в release commit body.
|
||||||
|
# 3. Release commit body содержит `[skip ci]` → GitLab матчит → пропускает
|
||||||
|
# tag pipeline (semantic-release/gitlab plugin создаёт tag, но pipeline
|
||||||
|
# на этот tag-ref скипается).
|
||||||
|
# 4. Без tag pipeline нет images → нет propose-prod-image-bump → нет
|
||||||
|
# auto-PR в infra → manual escape потребуется.
|
||||||
|
#
|
||||||
|
# Реальный случай: v2.16.1 (2026-05-12) — fix(release): 'remove [skip ci] из
|
||||||
|
# semantic-release commit message'. Auto-changelog содержал эту строку →
|
||||||
|
# v2.16.1 tag pipeline skipped → пришлось trigger вручную через API.
|
||||||
|
#
|
||||||
|
# Защита: fail CI на любом commit'е (subject ИЛИ body) который содержит
|
||||||
|
# substring `[skip ci]`. Когда нужно реально skip — использовать иные
|
||||||
|
# формулировки: `skip-ci marker`, `the skip-ci directive`, etc.
|
||||||
|
#
|
||||||
|
# Не блокирует chore(release) commit'ы semantic-release'а потому что
|
||||||
|
# .releaserc.json message template уже не содержит `[skip ci]` (MR !154).
|
||||||
|
#
|
||||||
|
# Skipping этот job сам: rules ниже исключают chore(release) commit'ы
|
||||||
|
# (subject matches 'chore(release):' regex), потому что body этих commit'ов
|
||||||
|
# содержит весь changelog со всеми историческими commit'ами — false positive.
|
||||||
|
commit-msg-lint:
|
||||||
|
stage: test
|
||||||
|
image: repo.nstart.cloud/library/alpine:3.20
|
||||||
|
script:
|
||||||
|
- apk add --no-cache git grep
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
# Сравниваем с last common ancestor relative к main для MR pipeline'ов,
|
||||||
|
# с last 10 commit'ами для branch pushes.
|
||||||
|
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
|
||||||
|
RANGE="$CI_MERGE_REQUEST_DIFF_BASE_SHA..HEAD"
|
||||||
|
elif [ -n "$CI_COMMIT_BEFORE_SHA" ] && [ "$CI_COMMIT_BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
|
||||||
|
RANGE="$CI_COMMIT_BEFORE_SHA..HEAD"
|
||||||
|
else
|
||||||
|
RANGE="HEAD~10..HEAD"
|
||||||
|
fi
|
||||||
|
echo "Checking commit messages in range: $RANGE"
|
||||||
|
|
||||||
|
BAD=0
|
||||||
|
while IFS= read -r SHA; do
|
||||||
|
SUBJECT=$(git log -1 --format=%s "$SHA")
|
||||||
|
# Скипаем release-bot commit'ы — semantic-release пишет changelog с
|
||||||
|
# историей commit'ов и может legitimately содержать литеральное
|
||||||
|
# `[skip ci]` если pre-MR-!154 commits в истории.
|
||||||
|
case "$SUBJECT" in
|
||||||
|
chore\(release\):*) echo " $SHA SKIP (release commit): $SUBJECT"; continue ;;
|
||||||
|
esac
|
||||||
|
MSG=$(git log -1 --format=%B "$SHA")
|
||||||
|
if echo "$MSG" | grep -q '\[skip ci\]'; then
|
||||||
|
echo " $SHA FAIL: содержит литеральное [skip ci]"
|
||||||
|
echo " Subject: $SUBJECT"
|
||||||
|
BAD=$((BAD+1))
|
||||||
|
else
|
||||||
|
echo " $SHA OK: $SUBJECT"
|
||||||
|
fi
|
||||||
|
done < <(git rev-list "$RANGE")
|
||||||
|
|
||||||
|
if [ "$BAD" -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════════════════════════"
|
||||||
|
echo "ERROR: $BAD commit(s) содержат литеральное \`[skip ci]\`."
|
||||||
|
echo ""
|
||||||
|
echo "Trap: semantic-release включит эту строку в release notes →"
|
||||||
|
echo "тег pipeline на release commit'е будет skip'нут GitLab'ом."
|
||||||
|
echo ""
|
||||||
|
echo "Если нужно описать skip-ci marker, используй:"
|
||||||
|
echo " - 'skip-ci marker' (с дефисом)"
|
||||||
|
echo " - 'the skip-ci directive'"
|
||||||
|
echo " - 'GitLab pipeline skip syntax'"
|
||||||
|
echo ""
|
||||||
|
echo "См. RELEASE.md → раздел 'Skip-ci marker trap'."
|
||||||
|
echo "════════════════════════════════════════════════════════════"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "All commits clean."
|
||||||
|
rules:
|
||||||
|
# Запускается на MR pipeline'ах и на push в feature branch'ах. На main +
|
||||||
|
# tag — не нужно (если что-то прошло в main, поздно валидировать; на tag
|
||||||
|
# хотим minimal pipeline overhead).
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH != "main" && $CI_COMMIT_TAG == null'
|
||||||
|
|
||||||
# ─── TEST ──────────────────────────────────────
|
# ─── TEST ──────────────────────────────────────
|
||||||
maven-test:
|
maven-test:
|
||||||
stage: test
|
stage: test
|
||||||
|
|||||||
+52
-5
@@ -88,18 +88,65 @@ BREAKING CHANGE: /api/v1/* paths больше не поддерживаются.
|
|||||||
## Skip release for specific commit
|
## Skip release for specific commit
|
||||||
|
|
||||||
Если хочешь смержить `feat:` или `fix:` но **не** делать release сейчас —
|
Если хочешь смержить `feat:` или `fix:` но **не** делать release сейчас —
|
||||||
добавь `[skip ci]` в commit message ИЛИ используй non-release type
|
используй non-release type (`chore:`, `docs:`, etc).
|
||||||
(`chore:`, `docs:`, etc).
|
|
||||||
|
|
||||||
```
|
```
|
||||||
chore(deps): bump axios 1.7 -> 1.8
|
chore(deps): bump axios 1.7 -> 1.8
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> ⚠ **НЕ используй `[skip ci]`** в commit message — см. раздел
|
||||||
|
> «Skip-ci marker trap» ниже.
|
||||||
|
|
||||||
## Loop prevention
|
## Loop prevention
|
||||||
|
|
||||||
Release job создаёт commit `chore(release): vX.Y.Z [skip ci]`. `[skip ci]` —
|
Release job создаёт commit `chore(release): vX.Y.Z`. Loop prevention работает
|
||||||
стандартный GitLab marker, новый pipeline не триггерится. Версия package.json
|
через releaseRules в `.releaserc.json` — chore-тип имеет `release: false` →
|
||||||
+ CHANGELOG.md обновляются в этом коммите.
|
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
|
## Branch policy
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user