feat(spec): project template skill init and check flow (#237)
* feat(spec): add project-template skill init and check flow * refactor(spec): remove repo-init migrate references to project-template * test(spec): structural tests for project-template skill and command * fix(test): wrap long lines and use non_templated var in project template tests * fix(ci): reformat test_project_template_skill.py for ruff format --------- Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
parent
51a5300423
commit
b3aac737eb
7 changed files with 557 additions and 768 deletions
5
.opencode/commands/project-template.md
Normal file
5
.opencode/commands/project-template.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
description: Project template — init new project (cookiecutter + GitHub remote) or check existing (project-status)
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
Load the `project-template` skill via `skill({name: "project-template"})` and follow its ПРОТОКОЛ strictly. Два flow: init (cookiecutter по типу + git + gh repo create + branch protection + project-status) или check (project-status → отчёт → рекомендации через subagents). Главный агент — оркестратор: вопросы юзеру + `project-status` tool (read-only) + делегирование. Не делает cookiecutter/git/gh напрямую.
|
||||||
|
|
@ -49,7 +49,7 @@ description: <when to load this skill, in English. Example: Use when ... Also wh
|
||||||
├── memory/SKILL.md
|
├── memory/SKILL.md
|
||||||
├── python-development/SKILL.md
|
├── python-development/SKILL.md
|
||||||
├── release/SKILL.md
|
├── release/SKILL.md
|
||||||
├── repo-init/SKILL.md
|
├── project-template/SKILL.md
|
||||||
├── run-pipeline/SKILL.md
|
├── run-pipeline/SKILL.md
|
||||||
├── run-tests/SKILL.md
|
├── run-tests/SKILL.md
|
||||||
├── spec/SKILL.md
|
├── spec/SKILL.md
|
||||||
|
|
|
||||||
340
.opencode/skills/project-template/SKILL.md
Normal file
340
.opencode/skills/project-template/SKILL.md
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
---
|
||||||
|
name: project-template
|
||||||
|
description: Init new project from cookiecutter template by type + GitHub remote + branch protection, or audit existing project via project-status. Replaces repo-init. Also when user says "новый проект", "инициализируй репо", "проверь проект", "create project", "project template".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Template
|
||||||
|
|
||||||
|
Два flow: **init** (новый проект: cookiecutter по типу + GitHub remote) и
|
||||||
|
**check** (существующий проект: project-status аудит с рекомендациями).
|
||||||
|
|
||||||
|
Замена монолитному `repo-init`: Phase A (GitHub remote + branch protection)
|
||||||
|
мигрирована без изменений, Phase B (ручное scaffolding) заменена на cookiecutter
|
||||||
|
шаблоны из `.opencode/templates/<type>/` (issue #229 / PR #235). Проверка
|
||||||
|
архитектуры — через `project-status` tool (issue #228 / PR #233).
|
||||||
|
|
||||||
|
## ПРОТОКОЛ (ЖЁСТКО)
|
||||||
|
|
||||||
|
1. Определи flow (вопрос юзеру, см. ниже).
|
||||||
|
2. init flow → шаги 1-6 (см. ниже).
|
||||||
|
3. check flow → шаги 1-3 (см. ниже).
|
||||||
|
4. После каждого шага → 1 строка прогресса юзеру (формат: `✅ <step> — <done>`).
|
||||||
|
|
||||||
|
### ЗАПРЕЩЕНО
|
||||||
|
|
||||||
|
- Запускать cookiecutter / git / gh напрямую из main agent — делегируй subagent
|
||||||
|
(Template INIT, Template GITHUB). Main agent = оркестратор: вопросы юзеру +
|
||||||
|
`project-status` tool (read-only oracle) + делегирование.
|
||||||
|
- `gh repo create` БЕЗ предварительного git init + initial commit (bare-repo →
|
||||||
|
empty push → main branch не появляется → branch protection падает).
|
||||||
|
- Импровизировать тип проекта вне `VALID_TYPES` из `spec-status.py` (см. ниже).
|
||||||
|
- Запускать `/run-pipeline` автоматически — стоп после init/check, дальше юзер
|
||||||
|
сам.
|
||||||
|
|
||||||
|
### Остановы
|
||||||
|
|
||||||
|
- Subagent error → 1 retry, потом STOP + report пользователю.
|
||||||
|
- cookiecutter не установлен → WARN + инструкция, STOP init flow.
|
||||||
|
- Template для типа не найден → WARN, STOP init flow (предложи check flow или
|
||||||
|
тип из доступных).
|
||||||
|
- `project-status` tool вернул `⚠️ ...failed` → WARN, сообщи пользователю,
|
||||||
|
продолжай без отчёта (не блокирующий).
|
||||||
|
|
||||||
|
## Определение flow
|
||||||
|
|
||||||
|
Вопрос юзеру (один вопрос, multiple choice):
|
||||||
|
|
||||||
|
```
|
||||||
|
Это новый проект или проверка существующего?
|
||||||
|
[1] init — новый проект: cookiecutter по типу + GitHub remote + branch protection
|
||||||
|
[2] check — аудит существующего: project-status (структура, роуты, качество, README, infra)
|
||||||
|
```
|
||||||
|
|
||||||
|
Если юзер выбрал init, но cwd уже git-репо с коммитами и файлами → предложи
|
||||||
|
check flow (см. Граничные случаи). Если cwd пустой или юзер подтвердил init →
|
||||||
|
init flow.
|
||||||
|
|
||||||
|
## init flow
|
||||||
|
|
||||||
|
### Шаг 1: Вопрос — тип проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
Выбери тип проекта (из spec-status VALID_TYPES):
|
||||||
|
[1] backend — FastAPI + Tortoise, REST API (cookiecutter template)
|
||||||
|
[2] fullstack — backend + SvelteKit frontend (cookiecutter template)
|
||||||
|
[3] cli — Python CLI tool, Typer (cookiecutter template)
|
||||||
|
[4] mcp-server — MCP + REST сервер (нет cookiecutter template — ручная инициализация)
|
||||||
|
[5] bot — Telegram bot, aiogram 3 (нет cookiecutter template — ручная инициализация)
|
||||||
|
[6] worker — Prefect flows (нет cookiecutter template — ручная инициализация)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cookiecutter templates доступны для типов: `backend`, `fullstack`, `cli`
|
||||||
|
(директории в `.opencode/templates/`). Для `mcp-server`, `bot`, `worker`
|
||||||
|
шаблонов нет → WARN: "cookiecutter template not found for type `<type>`.
|
||||||
|
Доступные: backend, fullstack, cli. Для остальных типов используй check flow
|
||||||
|
или создай issue на добавление template." → STOP init flow.
|
||||||
|
|
||||||
|
### Шаг 2: Вопрос — имя, описание, опции
|
||||||
|
|
||||||
|
```
|
||||||
|
Имя проекта (kebab-case, станет package name и GitHub repo name): ___
|
||||||
|
Описание (1 строка): ___
|
||||||
|
GitHub owner: ___
|
||||||
|
use_auth: [1] no (default) / [2] yes
|
||||||
|
use_db: [1] yes (default) / [2] no
|
||||||
|
```
|
||||||
|
|
||||||
|
Опции `use_auth` / `use_db` — переменные cookiecutter (см. `cookiecutter.json`).
|
||||||
|
Значения: `"no"` или `"yes"` (строки, lowercase).
|
||||||
|
|
||||||
|
### Шаг 3: Делегирование — cookiecutter + git init + initial commit
|
||||||
|
|
||||||
|
Subagent (general) — Template INIT ниже. Cookiecutter рендерит проект в
|
||||||
|
`./<project_name>/`, post_gen_project hook удаляет файлы условные на
|
||||||
|
`use_auth`/`use_db`. Затем git init + initial commit внутри `./<project_name>/`.
|
||||||
|
|
||||||
|
### Шаг 4: Делегирование — GitHub remote + branch protection
|
||||||
|
|
||||||
|
Subagent (general) — Template GITHUB ниже. Мигрировано из repo-init Phase A
|
||||||
|
(steps 1-3) БЕЗ изменений: `gh repo create`, squash-only merge settings, branch
|
||||||
|
protection на main. Требует локальный git-репо с initial commit (из шага 3).
|
||||||
|
|
||||||
|
### Шаг 5: Оркестратор — project-status
|
||||||
|
|
||||||
|
Вызови `project-status` tool напрямую (read-only oracle, ALLOWED для
|
||||||
|
оркестратора — как `pipeline-status` / `spec-status`):
|
||||||
|
|
||||||
|
```
|
||||||
|
project-status({})
|
||||||
|
```
|
||||||
|
|
||||||
|
Tool вернёт отчёт: `Project: <type>`, 7 групп `[OK]/[WARN]/[FAIL]`, `Итог:`,
|
||||||
|
`Рекомендации:`. Покажи отчёт юзеру. Если tool вернул `⚠️ ...failed` → WARN,
|
||||||
|
продолжай без отчёта.
|
||||||
|
|
||||||
|
### Шаг 6: Финальный репорт
|
||||||
|
|
||||||
|
```
|
||||||
|
Project <project_name> created at ./<project_name>/.
|
||||||
|
GitHub: https://github.com/<owner>/<project_name>
|
||||||
|
Branch protection: main (PR + required_status_checks + linear history)
|
||||||
|
Project-status: <summary из шага 5>
|
||||||
|
Дальше: /run-pipeline для реализации фич, или /spec для генерации spec.
|
||||||
|
```
|
||||||
|
|
||||||
|
## check flow
|
||||||
|
|
||||||
|
### Шаг 1: Оркестратор — project-status
|
||||||
|
|
||||||
|
```
|
||||||
|
project-status({})
|
||||||
|
```
|
||||||
|
|
||||||
|
Для строгого режима (exit 1 на FAIL) — `project-status({ check: true })`.
|
||||||
|
Для пропуска медленных remote-проверок (branch protection via gh) —
|
||||||
|
`project-status({ fast: true })`. По умолчанию — non-blocking (exit 0).
|
||||||
|
|
||||||
|
### Шаг 2: Оркестратор — отчёт + рекомендации
|
||||||
|
|
||||||
|
Покажи полный отчёт юзеру. В разделе `Рекомендации:` — список FAIL-чеков с
|
||||||
|
путями. Сгруппируй по категориям (Структура / Качество кода / Тесты / README /
|
||||||
|
Infra / Coverage).
|
||||||
|
|
||||||
|
### Шаг 3: Вопрос — чинить?
|
||||||
|
|
||||||
|
```
|
||||||
|
Найдены проблемы: <N FAIL, M WARN>.
|
||||||
|
Запустить fix-subagents для рекомендаций?
|
||||||
|
[1] да — делегируй subagent(ов) для каждого FAIL
|
||||||
|
[2] нет — только отчёт, я починю сам
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `да` → для каждого FAIL из `Рекомендации:` создай subagent (general) с
|
||||||
|
Template FIX (ниже), передав путь и описание проблемы. Subagent чинит, коммитит
|
||||||
|
через `commit` tool, push. Один FAIL = один subagent (последовательно, не
|
||||||
|
параллельно — см. AGENTS.md Linear Execution). После всех фиксов → re-run
|
||||||
|
`project-status` для верификации.
|
||||||
|
|
||||||
|
Если `нет` → STOP, отчёт у юзера.
|
||||||
|
|
||||||
|
## Граничные случаи
|
||||||
|
|
||||||
|
- **Существующий репо (не пустой)** → init flow: предложи check flow. Если юзер
|
||||||
|
настаивает на init → cookiecutter создаст `./<project_name>/` рядом (не
|
||||||
|
перезапишет текущий репо). Уточни: "cwd уже git-репо с файлами. Init создаст
|
||||||
|
новый проект в подкаталоге `./<project_name>/`. Продолжить? [1] да / [2] нет,
|
||||||
|
лучше check flow".
|
||||||
|
- **GitHub repo уже существует** → skip `gh repo create`, только branch
|
||||||
|
protection (если ещё не настроена). Subagent проверяет: `gh repo view
|
||||||
|
<owner>/<name>` — если существует, пропускает create, переходит к settings +
|
||||||
|
branch protection.
|
||||||
|
- **cookiecutter не установлен** → WARN: "cookiecutter не найден. Установи:
|
||||||
|
`uv tool install cookiecutter` (или `pipx install cookiecutter`). После
|
||||||
|
установки повтори init." → STOP init flow.
|
||||||
|
- **project-status не найден** → WARN: "project-status tool не доступен (issue
|
||||||
|
#228 / PR #233 не завершён или tool не зарегистрирован). Пропускаю
|
||||||
|
project-status проверку." → продолжай без отчёта (не блокирующий).
|
||||||
|
- **Template для типа не найден** (mcp-server/bot/worker) → WARN (см. Шаг 1).
|
||||||
|
- **gh auth не настроен** → subagent упадёт на `gh repo create`. Сообщи юзеру:
|
||||||
|
"запусти `gh auth login` и повтори".
|
||||||
|
|
||||||
|
## Prompt templates
|
||||||
|
|
||||||
|
### Template INIT (cookiecutter + git init + initial commit)
|
||||||
|
|
||||||
|
```
|
||||||
|
Создай новый проект типа <type> с именем <project_name>.
|
||||||
|
Контекст: init flow project-template skill, cwd = <cwd>.
|
||||||
|
|
||||||
|
1. Проверь cookiecutter: `cookiecutter --version`. Если не установлен → STOP,
|
||||||
|
верни: "cookiecutter не установлен. Установи: `uv tool install cookiecutter`".
|
||||||
|
2. Запусти cookiecutter (no-input, переменные из ответов юзера):
|
||||||
|
`cookiecutter .opencode/templates/<type>/ --no-input \
|
||||||
|
project_name=<project_name> \
|
||||||
|
project_type=<type> \
|
||||||
|
description="<description>" \
|
||||||
|
use_auth=<auth> \
|
||||||
|
use_db=<db> \
|
||||||
|
python_version=3.13`
|
||||||
|
Cookiecutter создаст каталог `./<project_name>/` с рендеренным проектом.
|
||||||
|
post_gen_project hook удалит файлы условные на use_auth/use_db.
|
||||||
|
3. `cd <project_name>` (все дальнейшие команды — внутри этого каталога).
|
||||||
|
4. `git init`
|
||||||
|
5. `git add .` затем `git status` — проверь staged set (только файлы проекта,
|
||||||
|
без лишнего). Если лишнее — `git restore --staged <file>`.
|
||||||
|
6. `commit({ message: "chore: initial commit" })` tool (НЕ raw `git commit` —
|
||||||
|
заблокирован deny).
|
||||||
|
7. Верни: "done: project created at ./<project_name>/, git init + initial commit".
|
||||||
|
|
||||||
|
Если найдёшь баг вне scope — загрузи skill `bug-discovery` через
|
||||||
|
`skill("bug-discovery")` и следуй протоколу. НЕ чини баг сам.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template GITHUB (gh repo create + settings + branch protection)
|
||||||
|
|
||||||
|
> Мигрировано из repo-init Phase A (steps 1-3) БЕЗ изменений. Требует локальный
|
||||||
|
> git-репо с initial commit (из Template INIT).
|
||||||
|
|
||||||
|
```
|
||||||
|
Настрой GitHub remote для проекта <project_name> (cwd = <cwd>/<project_name>).
|
||||||
|
owner = <owner>, visibility = public (или private если internal).
|
||||||
|
|
||||||
|
0. Проверь: GitHub repo уже существует?
|
||||||
|
`gh repo view <owner>/<project_name>` — если exit 0, skip step 1 (create),
|
||||||
|
переходи к step 2 (settings) и step 3 (branch protection).
|
||||||
|
|
||||||
|
1. Создание репозитория:
|
||||||
|
`gh repo create <owner>/<project_name> --public --source=. --remote=origin --push`
|
||||||
|
(или --private если internal)
|
||||||
|
После создания: `gh auth setup-git`
|
||||||
|
|
||||||
|
2. Настройки репозитория (squash-only merge, auto-delete branch):
|
||||||
|
`gh api repos/<owner>/<project_name> \
|
||||||
|
--method PATCH \
|
||||||
|
-f allow_squash_merge=true \
|
||||||
|
-f allow_merge_commit=false \
|
||||||
|
-f allow_rebase_merge=false \
|
||||||
|
-f delete_branch_on_merge=true \
|
||||||
|
-f squash_merge_commit_title=COMMIT_OR_PR_TITLE \
|
||||||
|
-f squash_merge_commit_message=COMMIT_MESSAGES`
|
||||||
|
|
||||||
|
3. Защита ветки main (требовать PR, required_status_checks, linear history):
|
||||||
|
`gh api repos/<owner>/<project_name>/rules/branches/main \
|
||||||
|
--method POST \
|
||||||
|
-F target=branch \
|
||||||
|
-f enforcement=active \
|
||||||
|
--input - <<'EOF'
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"ref_name": {
|
||||||
|
"include": ["refs/heads/main"],
|
||||||
|
"exclude": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "pull_request",
|
||||||
|
"parameters": {
|
||||||
|
"required_approving_review_count": 0,
|
||||||
|
"dismiss_stale_reviews_on_push": false,
|
||||||
|
"require_code_owner_review": false,
|
||||||
|
"require_last_push_approval": false,
|
||||||
|
"required_review_thread_resolution": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "required_status_checks",
|
||||||
|
"parameters": {
|
||||||
|
"strict_required_status_checks": true,
|
||||||
|
"do_not_enforce_on_create": false,
|
||||||
|
"required_status_checks": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "deletion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "non_fast_forward"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF`
|
||||||
|
|
||||||
|
`required_status_checks` заполняется именами CI-джобов после первого пуша
|
||||||
|
(имена из cookiecutter CI: `lint`, `typecheck`, `test`, `complexity` для
|
||||||
|
Python; `check` для JS/TS). На этом этапе оставь пустой массив — обновится
|
||||||
|
после первого CI-прогона.
|
||||||
|
|
||||||
|
4. Верни: "done: GitHub remote created/verified, squash-only merge, branch
|
||||||
|
protection on main".
|
||||||
|
|
||||||
|
Если найдёшь баг вне scope — загрузи skill `bug-discovery` через
|
||||||
|
`skill("bug-discovery")` и следуй протоколу. НЕ чини баг сам.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template FIX (check flow — починить FAIL из project-status)
|
||||||
|
|
||||||
|
```
|
||||||
|
Почини проблему из project-status отчёта.
|
||||||
|
Категория: <category> (Структура / Качество кода / Тесты / README / Infra / Coverage)
|
||||||
|
Проблема: <name>: <detail>
|
||||||
|
Путь: <path из рекомендации>
|
||||||
|
|
||||||
|
1. Прочитай контекст проблемы (файл по пути из рекомендации).
|
||||||
|
2. Минимальный фикс: добавь/исправь только то, что указано в рекомендации.
|
||||||
|
Не рефактори unrelated код.
|
||||||
|
3. Перед коммитом — `git status` для проверки staged set (`commit` tool НЕ
|
||||||
|
делает `git add` — коммитит только staged; используй
|
||||||
|
`git add <конкретные-пути>`, НЕ `git add -A`).
|
||||||
|
4. `commit({ message: "fix(<scope>): <description>" })` tool (НЕ raw
|
||||||
|
`git commit`), push.
|
||||||
|
5. Верни: "done: fixed <name>, commit <hash>".
|
||||||
|
|
||||||
|
Если найдёшь баг вне scope — загрузи skill `bug-discovery` через
|
||||||
|
`skill("bug-discovery")` и следуй протоколу. НЕ чини баг сам.
|
||||||
|
```
|
||||||
|
|
||||||
|
## VALID_TYPES (из spec-status.py)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend, fullstack, mcp-server, cli, bot, worker
|
||||||
|
```
|
||||||
|
|
||||||
|
Cookiecutter templates доступны для: `backend`, `fullstack`, `cli`
|
||||||
|
(директории `.opencode/templates/<type>/`).
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Main agent = оркестратор: вопросы юзеру + `project-status` tool (read-only) +
|
||||||
|
делегирование subagent'ам (Template INIT / GITHUB / FIX). Не делает
|
||||||
|
cookiecutter/git/gh напрямую.
|
||||||
|
- `project-status` tool — read-only oracle, ALLOWED для оркестратора (как
|
||||||
|
`pipeline-status` / `spec-status`).
|
||||||
|
- init flow порядок: cookiecutter → git init/commit → gh repo create → branch
|
||||||
|
protection → project-status. Не меняй порядок.
|
||||||
|
- check flow: project-status → отчёт → рекомендации → subagents (последовательно).
|
||||||
|
- Subagent error → 1 retry, потом STOP + report.
|
||||||
|
- Совместим с spec-pipeline: Phase 8 EXECUTE (spec/SKILL.md Template I) может
|
||||||
|
вызывать project-template init для scaffolding issue.
|
||||||
|
- `commit` tool НЕ делает `git add` — коммитит только staged. Используй
|
||||||
|
`git add <конкретные-пути>`, НЕ `git add -A`.
|
||||||
|
|
@ -1,759 +0,0 @@
|
||||||
---
|
|
||||||
name: repo-init
|
|
||||||
description: Sequential checklist: create GitHub remote → configure settings/branch protection → scaffold project files (Python/JS). Use when starting a new repo. Also when user says "новый репо", "создай репозиторий", "настрой репо".
|
|
||||||
---
|
|
||||||
|
|
||||||
# Repo Init
|
|
||||||
|
|
||||||
Полный чек-лист инициализации нового репозитория. Все шаблоны — внутри, берутся из эталонных репозиториев (reference repos).
|
|
||||||
|
|
||||||
## Фазы
|
|
||||||
|
|
||||||
- **Phase A — GitHub remote** (шаги 1-3, выполняется один раз): создание репо, настройки merge, защита ветки. Требует локального git-репо с initial commit.
|
|
||||||
- **Phase B — Project scaffolding** (шаги 4-9, по шаблонам): Python/JS файлы, Dependabot, LICENSE, .editorconfig, pre-commit, верификация. Можно повторно использовать для существующего репо (skip Phase A).
|
|
||||||
|
|
||||||
## Содержание
|
|
||||||
|
|
||||||
1. [Создание репозитория](#1-создание-репозитория)
|
|
||||||
2. [Настройки репозитория](#2-настройки-репозитория)
|
|
||||||
3. [Защита ветки main](#3-защита-ветки-main)
|
|
||||||
4. [Python-проект](#4-python-проект)
|
|
||||||
5. [JS/TS-проект](#5-jsts-проект)
|
|
||||||
6. [Dependabot](#6-dependabot)
|
|
||||||
7. [Общие файлы](#7-общие-файлы)
|
|
||||||
8. [Установка pre-commit](#8-установка-pre-commit)
|
|
||||||
9. [Чек-лист верификации](#9-чек-лист-верификации)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase A — GitHub remote
|
|
||||||
|
|
||||||
> Шаги 1-3 выполняются один раз для нового репо. Требуют локального git-репо с initial commit.
|
|
||||||
|
|
||||||
## 0. Prerequisite: git init + initial commit
|
|
||||||
|
|
||||||
Перед `gh repo create --source=.` локальный каталог должен быть git-репо с хотя бы одним коммитом (`--source=.` пушит текущую ветку; без коммита — пустой репо).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git init
|
|
||||||
echo "# <repo-name>" > README.md
|
|
||||||
git add README.md
|
|
||||||
git status # проверь staged set — только README.md, без лишнего
|
|
||||||
```
|
|
||||||
|
|
||||||
> `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если
|
|
||||||
> в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`)
|
|
||||||
> — не коммить: сначала `git restore --staged <file>` или не stage'и его
|
|
||||||
> изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`.
|
|
||||||
|
|
||||||
Затем через `commit` tool (НЕ raw `git commit` — заблокирован deny):
|
|
||||||
|
|
||||||
```
|
|
||||||
commit({ message: "chore: initial commit" })
|
|
||||||
```
|
|
||||||
|
|
||||||
Если bare-repo без initial commit — `gh repo create --source=.` создаст remote, но push будет пустым, а main branch не появится → branch protection упадёт.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Создание репозитория
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh repo create <owner>/<repo-name> --public --source=. --remote=origin --push
|
|
||||||
```
|
|
||||||
|
|
||||||
Или приватный (если internal):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh repo create <owner>/<repo-name> --private --source=. --remote=origin --push
|
|
||||||
```
|
|
||||||
|
|
||||||
После создания — настроить git auth:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh auth setup-git
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Настройки репозитория
|
|
||||||
|
|
||||||
Squash-only merge, auto-delete branch после merge:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh api repos/<owner>/<repo-name> \
|
|
||||||
--method PATCH \
|
|
||||||
-f allow_squash_merge=true \
|
|
||||||
-f allow_merge_commit=false \
|
|
||||||
-f allow_rebase_merge=false \
|
|
||||||
-f delete_branch_on_merge=true \
|
|
||||||
-f squash_merge_commit_title=COMMIT_OR_PR_TITLE \
|
|
||||||
-f squash_merge_commit_message=COMMIT_MESSAGES
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Защита ветки main
|
|
||||||
|
|
||||||
Требовать PR, требовать status checks (CI), linear history:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh api repos/<owner>/<repo-name>/rules/branches/main \
|
|
||||||
--method POST \
|
|
||||||
-F target=branch \
|
|
||||||
-f enforcement=active \
|
|
||||||
--input - <<'EOF'
|
|
||||||
{
|
|
||||||
"conditions": {
|
|
||||||
"ref_name": {
|
|
||||||
"include": ["refs/heads/main"],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"type": "pull_request",
|
|
||||||
"parameters": {
|
|
||||||
"required_approving_review_count": 0,
|
|
||||||
"dismiss_stale_reviews_on_push": false,
|
|
||||||
"require_code_owner_review": false,
|
|
||||||
"require_last_push_approval": false,
|
|
||||||
"required_review_thread_resolution": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "required_status_checks",
|
|
||||||
"parameters": {
|
|
||||||
"strict_required_status_checks": true,
|
|
||||||
"do_not_enforce_on_create": false,
|
|
||||||
"required_status_checks": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "deletion"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "non_fast_forward"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
`required_status_checks` заполняется именами CI-джобов после первого пуша (см. шаблоны CI ниже). Имена джобов: `lint`, `typecheck`, `test`, `complexity` (Python) или `check` (JS/TS).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase B — Project scaffolding
|
|
||||||
|
|
||||||
> Шаги 4-9 — шаблоны файлов для Python или JS/TS проекта. Можно применять к существующему репо (skip Phase A). Не зависят от GitHub remote.
|
|
||||||
|
|
||||||
## 4. Python-проект
|
|
||||||
|
|
||||||
### Инструменты
|
|
||||||
|
|
||||||
| Инструмент | Назначение | Конфиг в |
|
|
||||||
|---|---|---|
|
|
||||||
| **uv** | Package manager, virtual env | `pyproject.toml` (build + deps) |
|
|
||||||
| **ruff** | Linter + formatter (замена flake8/isort/black) | `pyproject.toml` `[tool.ruff]` |
|
|
||||||
| **mypy** | Строгая типизация | `pyproject.toml` `[tool.mypy]` |
|
|
||||||
| **pytest** + **pytest-cov** | Тесты + покрытие | `pyproject.toml` `[tool.pytest]` |
|
|
||||||
| **xenon** | Анализ сложности кода | CI workflow |
|
|
||||||
| **pre-commit** | Git hooks (ruff + mypy перед коммитом) | `.pre-commit-config.yaml` |
|
|
||||||
| **hatchling** | Build backend (wheel) | `pyproject.toml` `[build-system]` |
|
|
||||||
|
|
||||||
### pyproject.toml
|
|
||||||
|
|
||||||
> Заменить `<package-name>`, `<description>`, `<owner>/<repo>` на реальные значения. `additional_dependencies` в pre-commit — список runtime-зависимостей (для mypy).
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[build-system]
|
|
||||||
requires = ["hatchling"]
|
|
||||||
build-backend = "hatchling.build"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "<package-name>"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "<description>"
|
|
||||||
readme = "README.md"
|
|
||||||
license = "MIT"
|
|
||||||
requires-python = ">=3.12"
|
|
||||||
authors = [{ name = "slaid098" }]
|
|
||||||
keywords = []
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 4 - Beta",
|
|
||||||
"Environment :: Console",
|
|
||||||
"Intended Audience :: End Users/Desktop",
|
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
"Operating System :: Microsoft :: Windows",
|
|
||||||
"Operating System :: POSIX :: Linux",
|
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Programming Language :: Python :: 3.14",
|
|
||||||
]
|
|
||||||
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
dev = [
|
|
||||||
"pytest>=8.0",
|
|
||||||
"pytest-cov>=5.0",
|
|
||||||
"pytest-timeout>=2.2",
|
|
||||||
"mypy>=1.10",
|
|
||||||
"ruff>=0.5",
|
|
||||||
"xenon>=0.9",
|
|
||||||
"pre-commit>=3.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
Homepage = "https://github.com/slaid098/<repo>"
|
|
||||||
Repository = "https://github.com/slaid098/<repo>"
|
|
||||||
Issues = "https://github.com/slaid098/<repo>/issues"
|
|
||||||
Changelog = "https://github.com/slaid098/<repo>/blob/main/CHANGELOG.md"
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
|
||||||
packages = ["src/<package_name>"]
|
|
||||||
|
|
||||||
# ── Ruff ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
target-version = "py312"
|
|
||||||
line-length = 100
|
|
||||||
src = ["src", "tests"]
|
|
||||||
|
|
||||||
[tool.ruff.lint]
|
|
||||||
select = [
|
|
||||||
"E", "W", # pycodestyle
|
|
||||||
"F", # pyflakes
|
|
||||||
"I", # isort
|
|
||||||
"B", # bugbear
|
|
||||||
"UP", # pyupgrade
|
|
||||||
"SIM", # simplify
|
|
||||||
"C90", # mccabe complexity
|
|
||||||
"PL", # pylint
|
|
||||||
"RUF", # ruff-specific
|
|
||||||
"S", # bandit (security)
|
|
||||||
"TRY", # tryceratops (exception handling)
|
|
||||||
"LOG", # flake8-logging
|
|
||||||
]
|
|
||||||
ignore = [
|
|
||||||
"S101", # assert in tests
|
|
||||||
"S311", # pseudo-random for non-crypto use
|
|
||||||
"RUF001", # ambiguous Cyrillic chars (we write in Russian)
|
|
||||||
"RUF002", # same for docstrings
|
|
||||||
"RUF003", # same for comments
|
|
||||||
"TRY003", # long messages outside exception class
|
|
||||||
"PLR2004", # magic values in tests
|
|
||||||
"S106", # hardcoded passwords in tests
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.ruff.lint.mccabe]
|
|
||||||
max-complexity = 10
|
|
||||||
|
|
||||||
[tool.ruff.lint.pylint]
|
|
||||||
max-args = 5
|
|
||||||
max-branches = 12
|
|
||||||
max-returns = 5
|
|
||||||
max-statements = 50
|
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
|
||||||
"tests/*" = ["S101", "PLR2004", "S106", "S603", "S607"]
|
|
||||||
|
|
||||||
# ── mypy ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
python_version = "3.12"
|
|
||||||
strict = true
|
|
||||||
warn_return_any = true
|
|
||||||
disallow_untyped_defs = true
|
|
||||||
disallow_incomplete_defs = true
|
|
||||||
check_untyped_defs = true
|
|
||||||
no_implicit_optional = true
|
|
||||||
warn_redundant_casts = true
|
|
||||||
warn_unused_ignores = true
|
|
||||||
|
|
||||||
# ── pytest ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
addopts = "--cov=<package_name> --cov-report=term-missing --cov-fail-under=90 --timeout=120"
|
|
||||||
testpaths = ["tests"]
|
|
||||||
|
|
||||||
# ── coverage ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[tool.coverage.run]
|
|
||||||
source = ["src/<package_name>"]
|
|
||||||
branch = true
|
|
||||||
|
|
||||||
[tool.coverage.report]
|
|
||||||
exclude_lines = [
|
|
||||||
"pragma: no cover",
|
|
||||||
"if __name__ == .__main__.:",
|
|
||||||
"if TYPE_CHECKING:",
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### .pre-commit-config.yaml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
rev: v0.15.0
|
|
||||||
hooks:
|
|
||||||
- id: ruff
|
|
||||||
args: [--fix]
|
|
||||||
- id: ruff-format
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
||||||
rev: v1.18.2
|
|
||||||
hooks:
|
|
||||||
- id: mypy
|
|
||||||
additional_dependencies: []
|
|
||||||
```
|
|
||||||
|
|
||||||
`additional_dependencies` — список runtime-зависимостей проекта (из `[project.dependencies]`), чтобы mypy мог резолвить типы.
|
|
||||||
|
|
||||||
### .gitignore (Python)
|
|
||||||
|
|
||||||
```gitignore
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.egg-info/
|
|
||||||
*.egg
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.eggs/
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
|
|
||||||
# Environment / secrets
|
|
||||||
.env
|
|
||||||
*.env
|
|
||||||
!.env.template
|
|
||||||
|
|
||||||
# Testing / quality caches
|
|
||||||
.pytest_cache/
|
|
||||||
.coverage
|
|
||||||
htmlcov/
|
|
||||||
.mypy_cache/
|
|
||||||
.ruff_cache/
|
|
||||||
```
|
|
||||||
|
|
||||||
### .github/workflows/ci.yml (Python)
|
|
||||||
|
|
||||||
4 job'а: lint → typecheck → test (matrix) → complexity.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v3
|
|
||||||
- run: uv sync --extra dev
|
|
||||||
- run: uv run ruff check src/ tests/
|
|
||||||
- run: uv run ruff format --check src/ tests/
|
|
||||||
|
|
||||||
typecheck:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v3
|
|
||||||
- run: uv sync --extra dev
|
|
||||||
- run: uv run mypy src/
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python: ["3.12", "3.13", "3.14"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v3
|
|
||||||
- run: uv sync --extra dev --python ${{ matrix.python }}
|
|
||||||
- run: uv run pytest
|
|
||||||
|
|
||||||
complexity:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v3
|
|
||||||
- run: uv sync --extra dev
|
|
||||||
- run: uv run xenon --max-absolute B --max-modules A --max-average A src/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Структура проекта (Python)
|
|
||||||
|
|
||||||
```
|
|
||||||
<repo>/
|
|
||||||
├── .github/
|
|
||||||
│ ├── workflows/
|
|
||||||
│ │ └── ci.yml
|
|
||||||
│ └── dependabot.yml
|
|
||||||
├── src/
|
|
||||||
│ └── <package_name>/
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ └── py.typed
|
|
||||||
├── tests/
|
|
||||||
│ └── __init__.py
|
|
||||||
├── .pre-commit-config.yaml
|
|
||||||
├── .gitignore
|
|
||||||
├── LICENSE
|
|
||||||
├── pyproject.toml
|
|
||||||
├── README.md
|
|
||||||
└── uv.lock
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. JS/TS-проект
|
|
||||||
|
|
||||||
### Инструменты
|
|
||||||
|
|
||||||
| Инструмент | Назначение | Конфиг в |
|
|
||||||
|---|---|---|
|
|
||||||
| **npm** | Package manager | `package.json` |
|
|
||||||
| **Biome** | Linter + formatter (замена ESLint/Prettier) | `biome.json` |
|
|
||||||
| **TypeScript** | Строгая типизация | `tsconfig.json` |
|
|
||||||
| **Vitest** + **@vitest/coverage-v8** | Тесты + покрытие | `vitest.config.ts` |
|
|
||||||
| **Knip** | Dead-code detection | `knip.json` |
|
|
||||||
|
|
||||||
### package.json
|
|
||||||
|
|
||||||
> Заменить `<name>`, `<description>` на реальные значения. `entry` в knip.json — точка входа (для tree-shaking анализа).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "<name>",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "<dev-command>",
|
|
||||||
"build": "<build-command>",
|
|
||||||
"lint": "biome check",
|
|
||||||
"format": "biome format --write",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"knip": "knip"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^1.9.4",
|
|
||||||
"@types/node": "^22.10.0",
|
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
|
||||||
"happy-dom": "^20.10.0",
|
|
||||||
"knip": "^6.24.0",
|
|
||||||
"typescript": "^5.7.0",
|
|
||||||
"vitest": "^3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### biome.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": true,
|
|
||||||
"ignore": ["node_modules", "dist", "coverage"]
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100,
|
|
||||||
"lineEnding": "lf"
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "double",
|
|
||||||
"semicolons": "always",
|
|
||||||
"trailingCommas": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### knip.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"entry": ["src/index.ts"],
|
|
||||||
"project": ["src/**/*.ts", "src/**/*.tsx"],
|
|
||||||
"ignore": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### tsconfig.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"strict": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"types": ["node"]
|
|
||||||
},
|
|
||||||
"include": ["src", "tests"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### vitest.config.ts
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
environment: "node",
|
|
||||||
include: ["tests/**/*.test.ts"],
|
|
||||||
coverage: {
|
|
||||||
provider: "v8",
|
|
||||||
reporter: ["text", "html"],
|
|
||||||
thresholds: {
|
|
||||||
lines: 60,
|
|
||||||
functions: 60,
|
|
||||||
branches: 60,
|
|
||||||
statements: 60,
|
|
||||||
},
|
|
||||||
exclude: [
|
|
||||||
"tests/**",
|
|
||||||
"dist/**",
|
|
||||||
"vitest.config.ts",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### .gitignore (JS/TS)
|
|
||||||
|
|
||||||
```gitignore
|
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
coverage/
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
```
|
|
||||||
|
|
||||||
### .github/workflows/ci.yml (JS/TS)
|
|
||||||
|
|
||||||
Single job: lint → typecheck → knip → test → build.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
cache: "npm"
|
|
||||||
- run: npm ci
|
|
||||||
- name: Lint (Biome)
|
|
||||||
run: npm run lint
|
|
||||||
- name: Typecheck
|
|
||||||
run: npm run typecheck
|
|
||||||
- name: Knip
|
|
||||||
run: npm run knip
|
|
||||||
- name: Test (Vitest + Coverage)
|
|
||||||
run: npm run test
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Структура проекта (JS/TS)
|
|
||||||
|
|
||||||
```
|
|
||||||
<repo>/
|
|
||||||
├── .github/
|
|
||||||
│ ├── workflows/
|
|
||||||
│ │ └── ci.yml
|
|
||||||
│ └── dependabot.yml
|
|
||||||
├── src/
|
|
||||||
│ └── index.ts
|
|
||||||
├── tests/
|
|
||||||
├── .gitignore
|
|
||||||
├── biome.json
|
|
||||||
├── knip.json
|
|
||||||
├── package.json
|
|
||||||
├── package-lock.json
|
|
||||||
├── tsconfig.json
|
|
||||||
├── vitest.config.ts
|
|
||||||
├── LICENSE
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Dependabot
|
|
||||||
|
|
||||||
Автоматическое обновление зависимостей. Еженедельно, 5 PR max.
|
|
||||||
|
|
||||||
### Python (uv/pip)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "pip"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
```
|
|
||||||
|
|
||||||
### JS/TS (npm)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Общие файлы
|
|
||||||
|
|
||||||
### LICENSE (MIT)
|
|
||||||
|
|
||||||
```
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 slaid098
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
```
|
|
||||||
|
|
||||||
### .editorconfig
|
|
||||||
|
|
||||||
```ini
|
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.{py,toml}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[*.{ts,tsx,js,jsx,json,yml,yaml,css,md}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Установка pre-commit
|
|
||||||
|
|
||||||
### Python
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv sync --extra dev
|
|
||||||
uv run pre-commit install
|
|
||||||
uv run pre-commit run --all-files
|
|
||||||
```
|
|
||||||
|
|
||||||
### JS/TS
|
|
||||||
|
|
||||||
Pre-commit hooks не используются. Все quality gates — в CI (lint → typecheck → knip → test → build).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Чек-лист верификации
|
|
||||||
|
|
||||||
- [ ] Репозиторий создан (`gh repo create`)
|
|
||||||
- [ ] `gh auth setup-git` выполнен (push/pull работает)
|
|
||||||
- [ ] Squash-only merge, delete branch on merge (Step 2)
|
|
||||||
- [ ] Branch protection на main (Step 3)
|
|
||||||
- [ ] CI пайплайн зелёный на первом PR
|
|
||||||
- [ ] Pre-commit hooks установлены (Python) / CI гоняет (JS/TS)
|
|
||||||
- [ ] Dependabot включён (Settings → Code security → Dependabot)
|
|
||||||
- [ ] LICENSE, .gitignore, .editorconfig в репозитории
|
|
||||||
- [ ] `uv.lock` / `package-lock.json` закоммичен
|
|
||||||
|
|
@ -196,14 +196,14 @@ switcher `[English]` / `[Русский]`, заголовок `## 🇷🇺 Ру
|
||||||
`quick_start_steps_*` не влияют на валидацию — они рендерятся вне delimiter-пар
|
`quick_start_steps_*` не влияют на валидацию — они рендерятся вне delimiter-пар
|
||||||
(summary/features).
|
(summary/features).
|
||||||
|
|
||||||
## 6. Независимость от repo-init
|
## 6. Независимость от project-template
|
||||||
|
|
||||||
- Скилл `repo-init` создаёт **пустой** `README.md` как часть инициализации
|
- Скилл `project-template` (init flow) создаёт проект через cookiecutter —
|
||||||
репо.
|
шаблон уже включает `README.md` с базовой структурой.
|
||||||
- `repo-readme` (через тулзу `create-readme`) **наполняет** его
|
- `repo-readme` (через тулзу `create-readme`) **наполняет** его
|
||||||
стандартизированным контентом.
|
стандартизированным контентом (delimiter tags, bilingual, cover).
|
||||||
- Может применяться к существующим репо без `repo-init` — тулза перезапишет
|
- Может применяться к существующим репо без `project-template` — тулза
|
||||||
`README.md` (локально) или обновит через GitHub API (с SHA).
|
перезапишет `README.md` (локально) или обновит через GitHub API (с SHA).
|
||||||
|
|
||||||
## 7. Параметры тулзы (кратко)
|
## 7. Параметры тулзы (кратко)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ Prompt template F (см. ниже).
|
||||||
|
|
||||||
```
|
```
|
||||||
Дефолтный roadmap (можешь править):
|
Дефолтный roadmap (можешь править):
|
||||||
1. scaffolding — repo structure, CI, .gitignore, LICENSE (через repo-init skill)
|
1. scaffolding — repo structure, CI, .gitignore, LICENSE (через project-template skill init flow)
|
||||||
2. core: <module 1> — ...
|
2. core: <module 1> — ...
|
||||||
3. core: <module 2> — ...
|
3. core: <module 2> — ...
|
||||||
4. auth (если выбран auth в Phase 2)
|
4. auth (если выбран auth в Phase 2)
|
||||||
|
|
@ -316,7 +316,7 @@ Default stack для типа (хардкод, добавить всегда):
|
||||||
## Критерии приемки (как проверяем)
|
## Критерии приемки (как проверяем)
|
||||||
## Связанные ресурсы (Part of spec, ref к docs/spec/roadmap.md)
|
## Связанные ресурсы (Part of spec, ref к docs/spec/roadmap.md)
|
||||||
- Issue #1 (scaffolding) body ДОЛЖЕН включать:
|
- Issue #1 (scaffolding) body ДОЛЖЕН включать:
|
||||||
"Используй repo-init skill для: pyproject.toml, CI, .gitignore, LICENSE, dependabot, pre-commit. Структура — из ## Структура в docs/spec/modules.md."
|
"Используй project-template skill init flow для: cookiecutter по типу проекта (pyproject.toml, CI, .gitignore, LICENSE, dependabot, pre-commit). Структура — из ## Структура в docs/spec/modules.md."
|
||||||
- `create-issue({ title: "type(scope): description", body: "<body>", labels: ["enhancement", "from-spec"] })` tool (НЕ raw `gh issue create` — заблокирован deny)
|
- `create-issue({ title: "type(scope): description", body: "<body>", labels: ["enhancement", "from-spec"] })` tool (НЕ raw `gh issue create` — заблокирован deny)
|
||||||
4. Собери реальные номера issues из вывода tool.
|
4. Собери реальные номера issues из вывода tool.
|
||||||
5. Update docs/spec/roadmap.md: добавь реальные #N номера. Update docs/spec/meta.md: executed=true, phase=8.
|
5. Update docs/spec/roadmap.md: добавь реальные #N номера. Update docs/spec/meta.md: executed=true, phase=8.
|
||||||
|
|
|
||||||
203
tests/test_project_template_skill.py
Normal file
203
tests/test_project_template_skill.py
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
"""Structural tests for the project-template skill and command.
|
||||||
|
|
||||||
|
Validates acceptance criteria from issue #230:
|
||||||
|
- SKILL.md exists with init + check flow
|
||||||
|
- command exists with agent: build
|
||||||
|
- Phase A (GitHub remote + branch protection) migrated from repo-init
|
||||||
|
- Phase B replaced by cookiecutter
|
||||||
|
- repo-init skill removed
|
||||||
|
- references to repo-init updated in spec/SKILL.md, add-skill/SKILL.md,
|
||||||
|
repo-readme/SKILL.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SKILLS_DIR = REPO_ROOT / ".opencode" / "skills"
|
||||||
|
COMMANDS_DIR = REPO_ROOT / ".opencode" / "commands"
|
||||||
|
|
||||||
|
SKILL_PATH = SKILLS_DIR / "project-template" / "SKILL.md"
|
||||||
|
COMMAND_PATH = COMMANDS_DIR / "project-template.md"
|
||||||
|
REPO_INIT_PATH = SKILLS_DIR / "repo-init" / "SKILL.md"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_file_exists():
|
||||||
|
"""SKILL.md must exist at the canonical path."""
|
||||||
|
assert SKILL_PATH.is_file(), f"missing: {SKILL_PATH}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_file_exists():
|
||||||
|
"""Command file must exist at the canonical path."""
|
||||||
|
assert COMMAND_PATH.is_file(), f"missing: {COMMAND_PATH}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_init_removed():
|
||||||
|
"""repo-init skill must be removed (acceptance criterion)."""
|
||||||
|
assert not REPO_INIT_PATH.exists(), f"repo-init still present: {REPO_INIT_PATH}"
|
||||||
|
assert not (SKILLS_DIR / "repo-init").exists(), "repo-init/ dir still present"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_frontmatter():
|
||||||
|
"""SKILL.md frontmatter has name + description."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert text.startswith("---\n"), "missing frontmatter opening"
|
||||||
|
end = text.find("\n---\n", 4)
|
||||||
|
assert end != -1, "missing frontmatter closing"
|
||||||
|
fm = text[4:end]
|
||||||
|
assert "name: project-template" in fm, "frontmatter name missing"
|
||||||
|
assert "description:" in fm, "frontmatter description missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_has_init_and_check_flows():
|
||||||
|
"""SKILL.md documents both init and check flows."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "## init flow" in text, "init flow section missing"
|
||||||
|
assert "## check flow" in text, "check flow section missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_init_flow_steps():
|
||||||
|
"""init flow references cookiecutter, git, gh repo create, branch protection, project-status."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "cookiecutter" in text, "cookiecutter not referenced"
|
||||||
|
assert "git init" in text, "git init not referenced"
|
||||||
|
assert "gh repo create" in text, "gh repo create not referenced"
|
||||||
|
assert "branch protection" in text.lower() or "Защита ветки" in text, (
|
||||||
|
"branch protection missing"
|
||||||
|
)
|
||||||
|
assert "project-status" in text, "project-status not referenced"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_check_flow_steps():
|
||||||
|
"""check flow references project-status tool + recommendations + subagents."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "project-status({})" in text, "project-status tool call missing"
|
||||||
|
assert "Рекомендации" in text or "Рекомендации:" in text, "recommendations section missing"
|
||||||
|
assert "subagent" in text.lower(), "subagent delegation missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_phase_a_migrated():
|
||||||
|
"""Phase A (squash-only merge, branch protection rules) migrated verbatim."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "allow_squash_merge=true" in text, "squash merge setting missing"
|
||||||
|
assert "allow_merge_commit=false" in text, "merge commit setting missing"
|
||||||
|
assert "allow_rebase_merge=false" in text, "rebase merge setting missing"
|
||||||
|
assert "delete_branch_on_merge=true" in text, "delete branch on merge missing"
|
||||||
|
assert "squash_merge_commit_title=COMMIT_OR_PR_TITLE" in text, "squash title missing"
|
||||||
|
assert "required_status_checks" in text, "required_status_checks rule missing"
|
||||||
|
assert "non_fast_forward" in text, "non_fast_forward rule missing"
|
||||||
|
assert "refs/heads/main" in text, "main branch ref missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_phase_b_cookiecutter():
|
||||||
|
"""Phase B replaced by cookiecutter (no manual pyproject/biome templates)."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert ".opencode/templates/" in text, "templates dir not referenced"
|
||||||
|
assert "cookiecutter.json" in text, "cookiecutter.json not referenced"
|
||||||
|
assert "use_auth" in text, "use_auth variable missing"
|
||||||
|
assert "use_db" in text, "use_db variable missing"
|
||||||
|
assert "post_gen_project" in text, "post_gen_project hook not referenced"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_boundary_cases():
|
||||||
|
"""Boundary cases documented.
|
||||||
|
|
||||||
|
existing repo, GitHub repo exists, cookiecutter missing, project-status missing.
|
||||||
|
"""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "Существующий репо" in text or "существующий репо" in text.lower(), (
|
||||||
|
"existing repo case missing"
|
||||||
|
)
|
||||||
|
assert "GitHub repo уже существует" in text or "gh repo view" in text, (
|
||||||
|
"existing GitHub repo case missing"
|
||||||
|
)
|
||||||
|
assert "cookiecutter не установлен" in text, "cookiecutter missing case missing"
|
||||||
|
assert "uv tool install cookiecutter" in text, "cookiecutter install instruction missing"
|
||||||
|
assert "project-status не найден" in text or "project-status tool не доступен" in text, (
|
||||||
|
"project-status missing case missing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_delegates_not_edits():
|
||||||
|
"""Skill delegates to subagents (Template INIT/GITHUB/FIX), main agent = orchestrator."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "Template INIT" in text, "Template INIT missing"
|
||||||
|
assert "Template GITHUB" in text, "Template GITHUB missing"
|
||||||
|
assert "Template FIX" in text, "Template FIX missing"
|
||||||
|
assert "оркестратор" in text, "orchestrator role not stated"
|
||||||
|
assert "subagent" in text.lower(), "subagent delegation not mentioned"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_valid_types_reference():
|
||||||
|
"""SKILL.md references VALID_TYPES from spec-status.py."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
for ptype in ("backend", "fullstack", "mcp-server", "cli", "bot", "worker"):
|
||||||
|
assert ptype in text, f"project type {ptype} not mentioned"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_cookiecutter_types_documented():
|
||||||
|
"""SKILL.md documents which types have cookiecutter templates (backend, fullstack, cli)."""
|
||||||
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
||||||
|
templated = ["backend", "fullstack", "cli"]
|
||||||
|
non_templated = ["mcp-server", "bot", "worker"]
|
||||||
|
for ptype in templated:
|
||||||
|
assert ptype in text, f"templated type {ptype} missing"
|
||||||
|
for ptype in non_templated:
|
||||||
|
assert ptype in text, f"non-templated type {ptype} missing"
|
||||||
|
assert "cookiecutter template" in text.lower() or "cookiecutter templates" in text.lower(), (
|
||||||
|
"cookiecutter template availability not documented"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_agent_build():
|
||||||
|
"""Command file has agent: build frontmatter."""
|
||||||
|
text = COMMAND_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "agent: build" in text, "agent: build missing in command"
|
||||||
|
assert 'skill({name: "project-template"})' in text, "skill load instruction missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_no_repo_init_reference():
|
||||||
|
"""Command file must not reference repo-init."""
|
||||||
|
text = COMMAND_PATH.read_text(encoding="utf-8")
|
||||||
|
assert "repo-init" not in text, "command still references repo-init"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"rel_path",
|
||||||
|
[
|
||||||
|
".opencode/skills/spec/SKILL.md",
|
||||||
|
".opencode/skills/add-skill/SKILL.md",
|
||||||
|
".opencode/skills/repo-readme/SKILL.md",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_no_repo_init_references_in_skills(rel_path):
|
||||||
|
"""No stale repo-init references in dependent skills (handoff/decisions docs excluded)."""
|
||||||
|
path = REPO_ROOT / rel_path
|
||||||
|
assert path.is_file(), f"missing: {rel_path}"
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
assert "repo-init" not in text, f"stale repo-init reference in {rel_path}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_spec_skill_references_project_template():
|
||||||
|
"""spec/SKILL.md should reference project-template (not repo-init) for scaffolding."""
|
||||||
|
text = (REPO_ROOT / ".opencode" / "skills" / "spec" / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
assert "project-template" in text, "spec/SKILL.md does not reference project-template"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_skill_tree_lists_project_template():
|
||||||
|
"""add-skill/SKILL.md skill tree should list project-template (not repo-init)."""
|
||||||
|
text = (REPO_ROOT / ".opencode" / "skills" / "add-skill" / "SKILL.md").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "project-template/SKILL.md" in text, "add-skill tree missing project-template"
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_readme_references_project_template():
|
||||||
|
"""repo-readme/SKILL.md section 6 should reference project-template (not repo-init)."""
|
||||||
|
text = (REPO_ROOT / ".opencode" / "skills" / "repo-readme" / "SKILL.md").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "project-template" in text, "repo-readme missing project-template reference"
|
||||||
Loading…
Add table
Reference in a new issue