- заведён каталог web/ — Vue 3, роутер пятой версии, сборка Vite; собранное вшивается через go:embed и раздаётся корневым маршрутом: разметка на неизвестном пути вне корней сервиса, отказ контракта внутри корня - перечень корней сервиса стал единой точкой и порождает регистрацию маршрутов, а не описывает её; журнал раздачи пишет исход и длину пути, но не сам путь - шаг front зовёт Node контейнером docker — Biome, юнит-тесты Vue и сборка входят в гейт, а в Dockerfile появилась ступень приложения
309 lines
18 KiB
YAML
309 lines
18 KiB
YAML
# 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=<rev>'
|
||
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=<rev>"
|
||
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}" .
|