Инструмент ставится вместе с остальными (версия закреплена), гейт его больше не пропускает. Уязвимость почти всегда унаследована — это состояние зависимостей и тулчейна, а не диффа, — поэтому шаг не краснит гейт, а получает статус WARN: виден в сводке, уезжает в находки и в границы покрытия. Иначе красный гейт на каждом прогоне перестают читать. Уязвимость, приехавшую с новой зависимостью change, агент отличает по трассам вызовов и выводит как major. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
6.7 KiB
Bash
Executable File
158 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Детерминированный гейт ревью: прогоняет всё, у чего есть объективный оракул,
|
||
# и печатает сводку. В отличие от `task test`/`task lint` не останавливается на
|
||
# первом отказе — ревьюверу нужна полная картина, а не первая упавшая команда.
|
||
#
|
||
# Использование: scripts/gate.sh [<base-rev>]
|
||
# base-rev — база для диффа (покрытие изменённых строк). По умолчанию
|
||
# определяется как merge-base с master; для самого master — HEAD~1.
|
||
#
|
||
# Коды возврата: 0 — все обязательные шаги зелёные, 1 — есть красные.
|
||
# Пропущенные шаги (нет инструмента) НЕ красят гейт, но попадают в сводку:
|
||
# молча пропущенная проверка — это ложное ощущение проверенности. То же с WARN:
|
||
# отказ, который почти всегда унаследован (уязвимость в зависимости или в
|
||
# стандартной библиотеке), не должен краснить каждый прогон ревью — иначе
|
||
# красный гейт перестают читать. Разбирает такой отказ агент.
|
||
|
||
set -u
|
||
|
||
OUT_DIR="tmp/gate"
|
||
mkdir -p "$OUT_DIR"
|
||
|
||
BASE="${1:-}"
|
||
if [ -z "$BASE" ]; then
|
||
if git rev-parse --verify -q master >/dev/null && \
|
||
[ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then
|
||
BASE="$(git merge-base HEAD master)"
|
||
else
|
||
BASE="HEAD~1"
|
||
fi
|
||
fi
|
||
|
||
FAILED=0
|
||
SUMMARY=""
|
||
|
||
record() { # <status> <name> <hint>
|
||
SUMMARY="${SUMMARY}$1\t$2\t$3\n"
|
||
[ "$1" = "FAIL" ] && FAILED=1
|
||
return 0
|
||
}
|
||
|
||
step() { # <name> <hint-on-fail> <cmd...>
|
||
local name="$1" hint="$2"
|
||
shift 2
|
||
if "$@" >"$OUT_DIR/$name.log" 2>&1; then
|
||
record OK "$name" ""
|
||
else
|
||
record FAIL "$name" "$hint → $OUT_DIR/$name.log"
|
||
fi
|
||
}
|
||
|
||
echo "== gate: база диффа $BASE =="
|
||
|
||
# --- Компиляция и статика ---
|
||
step build "не собирается" go build ./...
|
||
step vet "" go vet ./...
|
||
if command -v golangci-lint >/dev/null; then
|
||
step lint "" golangci-lint run
|
||
else
|
||
record SKIP lint "golangci-lint не установлен (task setup)"
|
||
fi
|
||
|
||
gofmt_out="$(gofmt -l . 2>/dev/null | grep -v '^tmp/' || true)"
|
||
if [ -n "$gofmt_out" ]; then
|
||
record FAIL gofmt "не отформатировано: $(echo "$gofmt_out" | tr '\n' ' ')"
|
||
else
|
||
record OK gofmt ""
|
||
fi
|
||
|
||
# --- Тесты ---
|
||
step test "" go test -count=1 ./...
|
||
|
||
# Флаки: повторный прогон тех же тестов. Расхождение результатов — находка не
|
||
# ниже major (тест, который иногда зелёный, не является оракулом ни для чего).
|
||
if [ -f "$OUT_DIR/test.log" ] && grep -q '^ok\|^---' "$OUT_DIR/test.log"; then
|
||
if go test -count=1 ./... >"$OUT_DIR/test-repeat.log" 2>&1; then
|
||
if grep -q "^FAIL" "$OUT_DIR/test.log"; then
|
||
record FAIL flaky "прогон 1 красный, прогон 2 зелёный — флаки-тест"
|
||
else
|
||
record OK flaky ""
|
||
fi
|
||
else
|
||
if grep -q "^FAIL" "$OUT_DIR/test.log"; then
|
||
record OK flaky "" # стабильно красный — это не флаки, отработает шаг test
|
||
else
|
||
record FAIL flaky "прогон 1 зелёный, прогон 2 красный — флаки-тест"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# --- Гонки ---
|
||
if command -v gcc >/dev/null; then
|
||
step race "детектор гонок сработал" env CGO_ENABLED=1 go test -race -count=1 ./...
|
||
else
|
||
record SKIP race "нет gcc: -race требует cgo. Гонки НЕ проверены — в границы покрытия"
|
||
fi
|
||
|
||
# --- Покрытие изменённых строк ---
|
||
if go test -count=1 -coverprofile="$OUT_DIR/cover.out" ./... >"$OUT_DIR/cover.log" 2>&1; then
|
||
if python3 scripts/diff-coverage.py "$OUT_DIR/cover.out" "$BASE" \
|
||
>"$OUT_DIR/diff-coverage.log" 2>&1; then
|
||
record OK diff-coverage "$(head -1 "$OUT_DIR/diff-coverage.log")"
|
||
else
|
||
record SKIP diff-coverage "не удалось посчитать → $OUT_DIR/diff-coverage.log"
|
||
fi
|
||
else
|
||
record SKIP diff-coverage "прогон с профилем не собрался → $OUT_DIR/cover.log"
|
||
fi
|
||
|
||
# --- Миграции на чистой схеме ---
|
||
step migrations "миграции не накатываются с нуля" \
|
||
go test -count=1 -run Migration ./internal/store/...
|
||
|
||
# --- ER-схема синхронна с миграциями ---
|
||
# docs/conventions/database.md: структуру меняем — обновляем ER-схему в том же
|
||
# change. Проверка по диффу, поэтому живёт здесь, а не в internal/archrules.
|
||
changed="$(git diff --name-only "$BASE"...HEAD 2>/dev/null || true)"
|
||
if echo "$changed" | grep -q '^internal/store/migrations/'; then
|
||
if echo "$changed" | grep -q '^docs/specs/database.md$'; then
|
||
record OK er-schema ""
|
||
else
|
||
record FAIL er-schema "миграция изменена, а docs/specs/database.md — нет"
|
||
fi
|
||
fi
|
||
|
||
# --- Секреты ---
|
||
if command -v gitleaks >/dev/null; then
|
||
step gitleaks "возможен секрет в истории/индексе" gitleaks git --no-banner
|
||
else
|
||
record SKIP gitleaks "gitleaks не установлен"
|
||
fi
|
||
|
||
# --- Уязвимости зависимостей ---
|
||
# Не блокирует: находка тут — состояние зависимостей и тулчейна, а не диффа.
|
||
# Исключение (уязвимость приехала с новой зависимостью в этом change) разбирает
|
||
# агент по списку вызовов.
|
||
if command -v govulncheck >/dev/null; then
|
||
if govulncheck ./... >"$OUT_DIR/govulncheck.log" 2>&1; then
|
||
record OK govulncheck ""
|
||
else
|
||
record WARN govulncheck "достижимо из кода уязвимостей: $(grep -c '^Vulnerability #' "$OUT_DIR/govulncheck.log") → $OUT_DIR/govulncheck.log"
|
||
fi
|
||
else
|
||
record SKIP govulncheck "govulncheck не установлен (task setup)"
|
||
fi
|
||
|
||
echo
|
||
echo "== сводка =="
|
||
printf "%b" "$SUMMARY" | while IFS=$'\t' read -r status name hint; do
|
||
printf "%-5s %-14s %s\n" "$status" "$name" "$hint"
|
||
done
|
||
|
||
echo
|
||
if [ "$FAILED" -eq 1 ]; then
|
||
echo "ГЕЙТ КРАСНЫЙ — опиниативные проходы не запускаются"
|
||
exit 1
|
||
fi
|
||
echo "гейт зелёный (шаги WARN и SKIP см. в сводке — они идут в находки и в границы покрытия)"
|