# https://taskfile.dev version: '3' vars: PROJECT: "transcriber" # База диффа для шагов, которым нужна разница с основной веткой. BASE: '{{.BASE | default "origin/master"}}' # Пути скриптов проверки. Умолчания — канонические пути маркетплейса, чтобы # переустановка плагина не меняла Taskfile. Шаги независимы: каждый ловит # дрейф своего каталога, и выпадение одного не подменяется другим. # # Недостающий скрипт — отказ окружения у всех обёрток ниже, и код у него 3 по # общему словарю (CLAUDE.md, раздел «Гейт»). Прежний код 1 значил «дрейф» и # отправлял читателя искать разъехавшееся там, где просто неполно дерево. Сам # `task` отдаёт наружу свой 201 на любой отказ шага, поэтому словарь читается # по коду скрипта, а не по коду `task`. DOCS_PY: '{{.DOCS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/canon/scripts/docs.py"}}' TASKS_PY: '{{.TASKS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/task-track/scripts/tasks.py"}}' OPENSPEC_PY: '{{.OPENSPEC_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/code-openspec/scripts/openspec.py"}}' tasks: gate: desc: 'Все проверки разом. База диффа: task gate BASE=' cmds: # Приложение собирается первым: вшивание требует готового каталога, и # `go build` без него соберёт бинарник со вчерашней сборкой. - task: front - go build ./... - go vet ./... - | unformatted=$(gofmt -l .) if [ -n "$unformatted" ]; then echo "$unformatted" echo "gofmt: файлы выше не отформатированы" exit 1 fi - task: tests - golangci-lint run - task: shell - task: dockerfile - task: go-version - task: migrations - task: docs - task: tasks - task: openspec # Последним: единственный шаг, которому нужна сеть, и самый долгий. - 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=" exit 3 fi # Каталог шагов берётся из .av-dev.toml — там он уже записан ключом # `migrations` секции `[docs]` для сверки документов. Свой литерал завёл # бы факту второй дом: каталог переехал бы, а один из двух стражей молча # позеленел. До слияния плагинов файл звался docs/.docs.json. dir=$(python3 -c 'import tomllib; print(tomllib.load(open(".av-dev.toml","rb"))["docs"]["migrations"])' 2>/dev/null) || dir="" if [ -z "$dir" ] || [ ! -d "$dir" ]; then echo "каталог шагов схемы не найден: ключ [docs] migrations в .av-dev.toml → '$dir'" exit 3 fi # Страж предмета: правило, потерявшее файлы, стало бы вечно зелёным от # одного переименования — тот же приём, что у правил `internal/archrules`. if [ -z "$(ls "$dir" | grep -E '^[0-9]{12}_.*\.go$')" ]; then echo "в $dir нет ни одного файла шага: правило потеряло предмет" echo "поправь шаблон имени в этом шаге либо ключ [docs] migrations в .av-dev.toml" 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 front: desc: 'Приложение: зависимости, проверки, сборка' vars: # Образ берётся из Dockerfile: там он объявлен ступенью сборки. Второй дом # версии сборочного окружения разошёлся бы с первым молча, а своего шага # сверки у него, в отличие от версий Go, нет. NODE_IMAGE: sh: grep -oP '^FROM \K\S*node:\S*' Dockerfile | head -1 # Кэш установщика лежит вне дерева проекта: внутри контейнера он не пережил # бы прогон, и каждый набор проверок тянул бы зависимости заново. NPM_CACHE: '{{.NPM_CACHE | default "/tmp/transcriber-npm-cache"}}' cmds: # Node на машину не ставится — он зовётся контейнером, тем же образом, # каким собирается ступень образа. Требованием к машине остаётся docker. - | set -eu if ! command -v docker >/dev/null 2>&1; then echo "docker не найден в PATH" echo "приложение собирается контейнером: https://docs.docker.com/engine/install/" exit 3 fi if [ -z "{{.NODE_IMAGE}}" ]; then echo "в Dockerfile не нашлось ступени с образом node" exit 3 fi mkdir -p "{{.NPM_CACHE}}" run() { docker run --rm \ -u "$(id -u):$(id -g)" \ -e npm_config_cache=/npmcache \ -v "{{.NPM_CACHE}}:/npmcache" \ -v "$PWD/web:/web" \ -w /web \ "{{.NODE_IMAGE}}" \ sh -c "$1" } # Зависимости ставятся из файла замка командой, которая его не правит: # иначе набор проверок пачкал бы рабочее дерево, а собранное им # расходилось бы с собранным в образе. # # Сперва — установка из кэша, без единого обращения наружу: кэш лежит # вне дерева проекта и переживает прогоны, поэтому обычный случай сети # не требует вовсе. if ! run 'npm ci --offline' >/dev/null 2>&1; then # Кэша не хватило — значит нужна сеть, и её наличие проверяется одним # коротким обращением. Без этой проверки установщик уходит в повторы с # нарастающей паузой и **висит на каждом пакете**: гейт, который висит, # хуже красного — он не даёт ни исхода, ни причины. if ! run 'npm ping --fetch-timeout=15000 --fetch-retries=0' >/dev/null 2>&1; then echo "реестр пакетов недоступен" echo "шагу нужна сеть: он ставит зависимости приложения и тянет образ" exit 3 fi # Сеть здесь уже заведомо есть — проба реестра прошла. Значит всякий # отказ установки это отказ проекта: замок разошёлся с package.json, # пакет снят из реестра, сломался его postinstall. По словарю кодов # это дрейф, а не окружение: код 3 отправил бы человека чинить docker # и сеть вместо `git diff web/package-lock.json`. if ! run 'npm ci'; then echo "зависимости приложения не установились, а реестр доступен" echo "смотри расхождение web/package-lock.json с web/package.json" exit 1 fi fi run 'npm run check && npm run test && npm run build' shell: desc: 'shellcheck на скрипты оболочки' cmds: # Скриптов два и оба свои: шаг сверки версий и `docker/entrypoint.sh`. # Второй в образ копируется, но не исполняется — `ENTRYPOINT` в # `Dockerfile` закомментирован, — и проверяется он именно поэтому: код, # который никто не гоняет, портится незаметно. Ни один из двух не виден ни # `go vet`, ни `golangci-lint`. - | if ! command -v shellcheck >/dev/null 2>&1; then echo "shellcheck не найден в PATH" echo "поставь: apt install shellcheck (или https://github.com/koalaman/shellcheck)" exit 3 fi shellcheck scripts/check-go-version.sh docker/entrypoint.sh dockerfile: desc: 'hadolint на Dockerfile' cmds: # DL3007 (`alpine:latest` у рантайм-слоя) подавлен: это открытая задача # `pin-runtime-image-base`, и до её решения шаг краснел бы на известном. # DL3018 (закрепить версии пакетов `apk`) подавлен по существу: alpine не # держит старые версии в репозитории, и закрепление ломает сборку через # недели — то есть лечение хуже болезни. - | if ! command -v hadolint >/dev/null 2>&1; then echo "hadolint не найден в PATH" echo "поставь: https://github.com/hadolint/hadolint/releases" exit 3 fi hadolint --ignore DL3007 --ignore DL3018 Dockerfile go-version: desc: 'Одна версия Go в go.mod, Dockerfile, CLAUDE.md и README.md' cmds: # Скрипт лежит в самом репозитории, а не в плагине: его отсутствие значит # сломанное дерево, а не непоставленный плагин, и переопределять путь # нечем и незачем. Код отсутствия — 3, как у прочих обёрток. - | py=scripts/check-go-version.sh if [ ! -f "$py" ]; then echo "$py не найден: дерево репозитория неполно" exit 3 fi sh "$py" docs: desc: 'Раскладка docs/ против канона' cmds: # Шаг обязан краснеть внятно, если скрипта нет, а не пропускаться молча. - | py=$(eval echo {{.DOCS_PY}}) if [ ! -f "$py" ]; then echo "docs.py не найден: $py" echo "поставь плагин av-dev либо задай путь: task docs DOCS_PY=<путь>" exit 3 fi python3 "$py" check --base {{.BASE}} tasks: desc: 'Согласованность каталога задач' cmds: - | py=$(eval echo {{.TASKS_PY}}) if [ ! -f "$py" ]; then echo "tasks.py не найден: $py" echo "поставь плагин av-dev либо задай путь: task tasks TASKS_PY=<путь>" exit 3 fi python3 "$py" check --dir tasks openspec: desc: 'Форма openspec/config.yaml' cmds: - | py=$(eval echo {{.OPENSPEC_PY}}) if [ ! -f "$py" ]; then echo "openspec.py не найден: $py" echo "поставь плагин av-dev либо задай путь: task openspec OPENSPEC_PY=<путь>" exit 3 fi python3 "$py" check --dir . vulns: desc: 'Достижимые из кода уязвимости в зависимостях' cmds: # `govulncheck` — внешний инструмент, а не плагин и не файл репозитория: # ставится `go install golang.org/x/vuln/cmd/govulncheck@latest`. Его # отсутствие — отказ окружения, код 3, как у прочих обёрток. # # Свой код 3 у самого инструмента значит «уязвимость найдена» и с кодом # обёртки совпадает; различает их сообщение — обёртка называет недостающий # инструмент. Шагу нужна сеть: база уязвимостей живёт на vuln.go.dev, и без # сети шаг краснеет, а не пропускается молча. - | if ! command -v govulncheck >/dev/null 2>&1; then echo "govulncheck не найден в PATH" echo "поставь: go install golang.org/x/vuln/cmd/govulncheck@latest" exit 3 fi govulncheck ./... # Контракт роли app_image (pet-project-server): собрать полный образ и затегать # его $BUILD_ID. Деплой целиком: `inv pl -- transcriber` в pet-project-server. image: desc: 'Docker-образ. Тег из $BUILD_ID (по умолчанию dev)' cmds: - docker build --pull --file Dockerfile --tag "{{.PROJECT}}:${BUILD_ID:-dev}" .