#!/usr/bin/env bash # Детерминированный гейт ревью: прогоняет всё, у чего есть объективный оракул, # и печатает сводку. В отличие от `task test`/`task lint` не останавливается на # первом отказе — ревьюверу нужна полная картина, а не первая упавшая команда. # # Использование: scripts/gate.sh [] # 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() { # SUMMARY="${SUMMARY}$1\t$2\t$3\n" [ "$1" = "FAIL" ] && FAILED=1 return 0 } step() { # 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 см. в сводке — они идут в находки и в границы покрытия)"