Гейт ищет гонки, переписанные шаги схемы и девять новых классов дефектов
- включены noctx, contextcheck и bodyclose (отмена доходит до внешнего вызова, контекст приезжает сверху, тело ответа закрывается), nilerr, rowserrcheck и sqlclosecheck (отказ не теряется молча), testifylint и nolintlint (форма утверждения и форма подавления), плюс errcheck check-type-assertions: непроверенное приведение типа паникует, и check-blank его не видит - заведён шаг tests: go test -race, потому что «результат пишет только держатель захвата» — утверждение об одновременности. Без компилятора C шаг гоняет тесты без детектора и краснеет кодом 3 после них: гонки не повод отнимать у гейта сами тесты - заведён шаг migrations: у файла шага схемы допустим один статус — A. Баз диффа две, BASE и HEAD: первая отвечает на «уже уехал» настолько, насколько свежа origin/master, вторая ловит правку закоммиченного шага независимо от неё. Каталог берётся из docs/.docs.json, пустой каталог роняет шаг - единственное подавление — noctx на httptest.NewRequest в проверках: за фикстурой запроса внешнего собеседника нет. Граница проверена мутацией — http.Get и exec.Command из проверки правилу по-прежнему подсудны
This commit is contained in:
@@ -20,6 +20,34 @@ linters:
|
|||||||
# Запреты по месту: чем судят ответ в проверках, чем читают время, откуда
|
# Запреты по месту: чем судят ответ в проверках, чем читают время, откуда
|
||||||
# берут конфигурацию, куда пишут вывод. Подробности у каждого правила ниже.
|
# берут конфигурацию, куда пишут вывод. Подробности у каждого правила ниже.
|
||||||
- forbidigo
|
- forbidigo
|
||||||
|
# Отмена доходит до внешнего вызова: запрос и внешний процесс заводятся с
|
||||||
|
# контекстом. Инвариант «принятая запись не теряется молча» держится
|
||||||
|
# остановкой на середине, а не только записью в лог: `ffmpeg`, заведённый
|
||||||
|
# без контекста, переживает остановку воркера и дожёвывает чужую запись.
|
||||||
|
- noctx
|
||||||
|
# Контекст приезжает сверху, а не заводится по месту. `context.Background()`
|
||||||
|
# внутри адаптера обрывает цепочку отмены ровно на границе с платным
|
||||||
|
# внешним сервисом — там, где отмена и нужна.
|
||||||
|
- contextcheck
|
||||||
|
# Тело ответа закрывается. `errcheck` его не видит: `(io.ReadCloser).Close`
|
||||||
|
# объявлен в `exclude-functions` ниже, и незакрытое тело от невыясненного
|
||||||
|
# `Close` этим списком не отличается.
|
||||||
|
- bodyclose
|
||||||
|
# `return nil` после проверенной ошибки — это молчаливая потеря отказа,
|
||||||
|
# прямо запрещённая инвариантом об очереди (CLAUDE.md, major).
|
||||||
|
- nilerr
|
||||||
|
# Отказ выборки не теряется: неспрошенный `rows.Err()` превращает оборванное
|
||||||
|
# чтение в пустой результат.
|
||||||
|
- rowserrcheck
|
||||||
|
# `Rows` и `Stmt` закрываются: незакрытая выборка держит соединение.
|
||||||
|
- sqlclosecheck
|
||||||
|
# Форма утверждений в проверках: перепутанные местами «ожидалось/получено»,
|
||||||
|
# `assert` там, где после провала продолжать нельзя, `require` из горутины.
|
||||||
|
- testifylint
|
||||||
|
# Подавление — это решение: строчное `//nolint` обязано называть линтер и
|
||||||
|
# причину, а протухшее подавление обязано краснеть. Тот же порядок, что у
|
||||||
|
# подавлений в этом файле, но применённый к комментариям в коде.
|
||||||
|
- nolintlint
|
||||||
settings:
|
settings:
|
||||||
forbidigo:
|
forbidigo:
|
||||||
# `analyze-types` включает суждение по типу приёмника, а не по печатному
|
# `analyze-types` включает суждение по типу приёмника, а не по печатному
|
||||||
@@ -101,11 +129,24 @@ linters:
|
|||||||
- pkg: github.com/cockroachdb/errors
|
- pkg: github.com/cockroachdb/errors
|
||||||
desc: 'стек-трейс избыточен, контекст несёт цепочка %w (docs/conventions/errors.md)'
|
desc: 'стек-трейс избыточен, контекст несёт цепочка %w (docs/conventions/errors.md)'
|
||||||
|
|
||||||
|
nolintlint:
|
||||||
|
# Подавление без причины снимают при первом же неудобстве: снимающий не
|
||||||
|
# знает, что оно ловило. Те же два требования, что у подавлений в этом
|
||||||
|
# файле, — имя линтера и причина строкой.
|
||||||
|
require-explanation: true
|
||||||
|
require-specific: true
|
||||||
|
# Подавление, которому нечего подавлять, — след починенного места, и
|
||||||
|
# краснеть оно обязано: иначе перечень подавлений врёт.
|
||||||
|
allow-unused: false
|
||||||
|
|
||||||
errcheck:
|
errcheck:
|
||||||
# Без этого `_ = x.Close()` снимает замечание, и критерий «отказ не
|
# Без этого `_ = x.Close()` снимает замечание, и критерий «отказ не
|
||||||
# теряется молча» принимается реализацией, которая его теряет. Отказ,
|
# теряется молча» принимается реализацией, которая его теряет. Отказ,
|
||||||
# который решено не проверять, теперь объявляют ниже поимённо — заметно.
|
# который решено не проверять, теперь объявляют ниже поимённо — заметно.
|
||||||
check-blank: true
|
check-blank: true
|
||||||
|
# Непроверенное приведение типа паникует, а не отдаёт ошибку, поэтому
|
||||||
|
# `check-blank` его не ловит: `v := x.(T)` вовсе не про присваивание в `_`.
|
||||||
|
check-type-assertions: true
|
||||||
exclude-functions:
|
exclude-functions:
|
||||||
# Закрытие через defer и лучшая-попытка уборки файла — осознанно без проверки
|
# Закрытие через defer и лучшая-попытка уборки файла — осознанно без проверки
|
||||||
- (io.Closer).Close
|
- (io.Closer).Close
|
||||||
@@ -145,6 +186,16 @@ linters:
|
|||||||
- forbidigo
|
- forbidigo
|
||||||
path: '_test\.go$'
|
path: '_test\.go$'
|
||||||
text: 'time.Now'
|
text: 'time.Now'
|
||||||
|
# `httptest.NewRequest` строит фикстуру для обработчика в том же процессе:
|
||||||
|
# внешнего собеседника за ней нет, и отменять у неё нечего — правило здесь
|
||||||
|
# говорит не о том, что мы имели в виду. Изъятие названо по имени этой
|
||||||
|
# функции, а не выключением `noctx` на проверках целиком: настоящий внешний
|
||||||
|
# вызов из проверки — `http.Get`, `exec.Command` — правилу по-прежнему
|
||||||
|
# подсуден.
|
||||||
|
- linters:
|
||||||
|
- noctx
|
||||||
|
path: '_test\.go$'
|
||||||
|
text: 'httptest\.NewRequest'
|
||||||
|
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
|
|||||||
+78
-1
@@ -33,17 +33,94 @@ tasks:
|
|||||||
echo "gofmt: файлы выше не отформатированы"
|
echo "gofmt: файлы выше не отформатированы"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- go test ./...
|
- task: tests
|
||||||
- golangci-lint run
|
- golangci-lint run
|
||||||
- task: shell
|
- task: shell
|
||||||
- task: dockerfile
|
- task: dockerfile
|
||||||
- task: go-version
|
- task: go-version
|
||||||
|
- task: migrations
|
||||||
- task: docs
|
- task: docs
|
||||||
- task: tasks
|
- task: tasks
|
||||||
- task: openspec
|
- task: openspec
|
||||||
# Последним: единственный шаг, которому нужна сеть, и самый долгий.
|
# Последним: единственный шаг, которому нужна сеть, и самый долгий.
|
||||||
- task: vulns
|
- task: vulns
|
||||||
|
|
||||||
|
tests:
|
||||||
|
desc: 'Тесты с детектором гонок'
|
||||||
|
cmds:
|
||||||
|
# Гонки ищет детектор, а не чтение кода: у сервиса три воркера ходят в одну
|
||||||
|
# очередь, и «результат пишет только держатель захвата» — утверждение о
|
||||||
|
# одновременном доступе. Детектору нужен CGO и компилятор C; сборка
|
||||||
|
# приложения по-прежнему обходится без них (CLAUDE.md, «Стек»), поэтому их
|
||||||
|
# отсутствие — отказ окружения, код 3, а не отказ проверки.
|
||||||
|
# Окружение проверяется **после** обычного прогона, а не вместо него:
|
||||||
|
# отсутствие компилятора отнимает у гейта поиск гонок, но не должно
|
||||||
|
# отнимать сами тесты. Порядок проверок — сперва компилятор: без него
|
||||||
|
# совет «включи CGO_ENABLED=1» бесполезен.
|
||||||
|
- |
|
||||||
|
if ! command -v gcc >/dev/null 2>&1 && ! command -v clang >/dev/null 2>&1; then
|
||||||
|
go test ./... || exit 1
|
||||||
|
echo "тесты прошли, но гонки не искали: детектору нужен компилятор C"
|
||||||
|
echo "ни gcc, ни clang не найдены в PATH; поставь: apt install gcc"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
if [ "$(go env CGO_ENABLED)" != "1" ]; then
|
||||||
|
go test ./... || exit 1
|
||||||
|
echo "тесты прошли, но гонки не искали: детектору нужен CGO"
|
||||||
|
echo "CGO_ENABLED=$(go env CGO_ENABLED); включи: CGO_ENABLED=1 task gate"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
go test -race ./...
|
||||||
|
|
||||||
|
migrations:
|
||||||
|
desc: 'Применённый шаг схемы не переписывается'
|
||||||
|
cmds:
|
||||||
|
# Инвариант CLAUDE.md (critical): хранилище считает применённое по имени
|
||||||
|
# файла шага, поэтому изменить уехавший шаг нельзя — только добавить новый.
|
||||||
|
# Компилятор этого не держит, и до этого шага не держало ничто.
|
||||||
|
#
|
||||||
|
# Судится каталог шагов против базы диффа: у файла шага допустим один
|
||||||
|
# статус — `A`. Правка (`M`), удаление (`D`) и переименование (`R`) красят.
|
||||||
|
# `migrations.go` под правило не подпадает: строка `Register` у нового шага
|
||||||
|
# прибавляется именно там, и запрет на него запретил бы заведение шага.
|
||||||
|
- |
|
||||||
|
if ! git rev-parse --verify --quiet "{{.BASE}}" >/dev/null 2>&1; then
|
||||||
|
echo "база диффа не найдена: {{.BASE}}"
|
||||||
|
echo "задай свою: task migrations BASE=<rev>"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
# Каталог шагов берётся из docs/.docs.json — там он уже записан ключом
|
||||||
|
# `migrations` для сверки документов. Свой литерал завёл бы факту второй
|
||||||
|
# дом: каталог переехал бы, а один из двух стражей молча позеленел.
|
||||||
|
dir=$(python3 -c 'import json,sys; print(json.load(open("docs/.docs.json"))["migrations"])' 2>/dev/null) || dir=""
|
||||||
|
if [ -z "$dir" ] || [ ! -d "$dir" ]; then
|
||||||
|
echo "каталог шагов схемы не найден: ключ migrations в docs/.docs.json → '$dir'"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
# Страж предмета: правило, потерявшее файлы, стало бы вечно зелёным от
|
||||||
|
# одного переименования — тот же приём, что у правил `internal/archrules`.
|
||||||
|
if [ -z "$(ls "$dir" | grep -E '^[0-9]{12}_.*\.go$')" ]; then
|
||||||
|
echo "в $dir нет ни одного файла шага: правило потеряло предмет"
|
||||||
|
echo "поправь шаблон имени в этом шаге либо ключ migrations в docs/.docs.json"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
# Баз две, и вторая обязательна. `{{.BASE}}` отвечает на «шаг уже уехал»
|
||||||
|
# ровно настолько, насколько свеж `origin/master`: отставшая ссылка
|
||||||
|
# читает весь каталог как добавленный, и правило молчит. `HEAD` ловит
|
||||||
|
# правку закоммиченного шага в рабочем дереве независимо от ссылки.
|
||||||
|
for base in {{.BASE}} HEAD; do
|
||||||
|
touched=$(git diff --name-status "$base" -- "$dir" \
|
||||||
|
| grep -E '[0-9]{12}_[^/]*\.go$' \
|
||||||
|
| grep -vE '^A[[:space:]]' || true)
|
||||||
|
if [ -n "$touched" ]; then
|
||||||
|
echo "база $base:"
|
||||||
|
echo "$touched"
|
||||||
|
echo "применённый шаг схемы переписан: изменение схемы — только новым файлом шага"
|
||||||
|
echo "(CLAUDE.md, «Инварианты», critical: хранилище считает применённое по имени файла)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
shell:
|
shell:
|
||||||
desc: 'shellcheck на скрипты оболочки'
|
desc: 'shellcheck на скрипты оболочки'
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
@@ -276,7 +276,9 @@ func treeWithScript(t *testing.T) string {
|
|||||||
// `env` — добавка к окружению прогона, `args` — аргументы скрипта.
|
// `env` — добавка к окружению прогона, `args` — аргументы скрипта.
|
||||||
func runScript(t *testing.T, root, dir string, args []string, env ...string) (int, string) {
|
func runScript(t *testing.T, root, dir string, args []string, env ...string) (int, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cmd := exec.Command("sh", append([]string{filepath.Join(root, "scripts", "check-go-version.sh")}, args...)...)
|
// Контекст проверки: зависший скрипт умирает вместе с ней, а не переживает
|
||||||
|
// прогон осиротевшим процессом.
|
||||||
|
cmd := exec.CommandContext(t.Context(), "sh", append([]string{filepath.Join(root, "scripts", "check-go-version.sh")}, args...)...)
|
||||||
cmd.Dir = filepath.Join(root, dir)
|
cmd.Dir = filepath.Join(root, dir)
|
||||||
if len(env) > 0 {
|
if len(env) > 0 {
|
||||||
cmd.Env = append(os.Environ(), env...)
|
cmd.Env = append(os.Environ(), env...)
|
||||||
|
|||||||
Reference in New Issue
Block a user