Files
jellybit/scripts/gate.sh
T
avandClaude Opus 4.8 6792f7082a ревью: детерминированный гейт task gate и карта проекта для архитектурного прохода
scripts/gate.sh гонит всё, у чего есть объективный оракул (build, vet, lint,
gofmt, тесты, повтор на флаки, -race, покрытие изменённых строк, миграции,
ER-схема по диффу, gitleaks, govulncheck), не останавливаясь на первом отказе:
ревью нужна полная картина. Пропущенный шаг попадает в сводку — молча
пропущенная проверка даёт ложное ощущение проверенности.

scripts/diff-coverage.py считает покрытие именно изменённых строк: общий
процент по пакету для ревью бесполезен.

scripts/review-context.sh собирает вход, которого нет в диффе — пакеты с
назначением, граф внутренних зависимостей, публичную поверхность и инвентарь
концепций. Агент, видящий только дифф, не знает словаря проекта и потому не
может судить об архитектуре.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:17:28 +03:00

148 lines
5.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Детерминированный гейт ревью: прогоняет всё, у чего есть объективный оракул,
# и печатает сводку. В отличие от `task test`/`task lint` не останавливается на
# первом отказе — ревьюверу нужна полная картина, а не первая упавшая команда.
#
# Использование: scripts/gate.sh [<base-rev>]
# base-rev — база для диффа (покрытие изменённых строк). По умолчанию
# определяется как merge-base с master; для самого master — HEAD~1.
#
# Коды возврата: 0 — все обязательные шаги зелёные, 1 — есть красные.
# Пропущенные шаги (нет инструмента) НЕ красят гейт, но попадают в сводку:
# молча пропущенная проверка — это ложное ощущение проверенности.
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
# --- Уязвимости зависимостей ---
if command -v govulncheck >/dev/null; then
step govulncheck "" govulncheck ./...
else
record SKIP govulncheck "govulncheck не установлен (go install golang.org/x/vuln/cmd/govulncheck@latest)"
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 "гейт зелёный (пропущенные шаги см. в сводке — они идут в границы покрытия)"