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
+92
View File
@@ -67,6 +67,98 @@ default:
.docs-changes: &docs-changes
- "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 ──────────────────────────────────────
maven-test:
stage: test