# 07. Git workflow и CI **Назначение:** описать конкретный Git-flow, branch protection, конвенцию коммитов, состав CI-пайплайнов, окружения, ephemeral preview — всё, что превращает Git в одновременно «движок процесса» и «единственный источник правды». --- ## Простым языком Каждая фича получает свою отдельную ветку в Git. Все правки идут через PR (Pull Request). PR — это контейнер всех артефактов фичи: код, тесты, ТЗ, ADR, дизайн, отчёт о тестировании, лог деплоя. Между этапами стоит CI: пока он не зелёный — двинуться вперёд нельзя. Главная ветка `main` всегда боеспособна — её содержимое отражает то, что в test-окружении (через минуту после merge). Когда test проверен — ставится тег, по которому идёт деплой в prom. Всё, что происходит, видно в PR (для инженера) и в Plane (для человека-наблюдателя). Никаких локальных папок, никаких «у меня работало». Никто не правит код напрямую на сервере. --- ## Модель веток Используется **trunk-based development** — упрощённый GitHub Flow: - **`main`** — единственная долгоживущая ветка. Всегда зелёная (CI), всегда деплоится в test через минуту после merge. - **`feature/-`** — короткоживущие фичевые ветки (≤5 дней жизни в норме). От `main`, в `main`. - **`bugfix/-`** — то же, что фича, но для багов. - **`hotfix/-`** — срочные фиксы для prom. От тега prom-релиза, мерджатся в `main` и cherry-pick'ятся как новый prom-tag. - **`phase/-`** — для крупных фаз, объединяющих несколько фич (опционально, не для всех проектов). - **`chore/`** — обслуживание без Plane-задачи (обновление зависимостей, инфра-tweaks). Допускается в редких случаях. **Не используются:** - `develop` — лишний слой для команды этого размера (5–15 человек). - Long-lived release-ветки — релизы делаются через теги, не ветки. - `gh-pages` или другие магические ветки — кроме случая, когда хостится статика. --- ## Конвенция имён ``` feature/- bugfix/- hotfix/- phase/- chore/ ``` `` — точный ID Work Item, как в Plane (`PROJ-123`). `` — короткий описательный slug (≤50 символов). Примеры: - `feature/PROJ-123-add-noise-zones-on-map` - `bugfix/PROJ-456-fix-empty-legend-rendering` - `hotfix/PROJ-789-revert-broken-rate-limit` --- ## Conventional Commits Каждый commit: ``` (): Refs: # или Closes: для финального ``` **Types:** - `feat` — новая фича (minor bump в semver). - `fix` — багфикс (patch bump). - `perf` — улучшение производительности (patch bump). - `refactor` — рефакторинг без изменения поведения. - `test` — добавление/правка тестов. - `docs` — изменения документации. - `build` — сборка / зависимости. - `ci` — CI/CD конфигурация. - `chore` — обслуживание. - `arch` — изменения, связанные с новым ADR (если важно подсветить отдельно). - `style` — форматирование. **BREAKING CHANGE** — указывается в body или в `!:` префиксе. Триггерит major bump. **Scope** — модуль, в котором изменение: `feat(api):`, `fix(map):`, `docs(adr):`. Список разрешённых scope в `commitlint.config.js` (если включён). Примеры: ``` feat(map): add noise zones layer Implement REQ-F-1 from PROJ-123. Use Mapbox tile-set for vector layer; cache 1h on CDN edge. Refs: PROJ-123 ``` ``` fix(api)!: rename /v1/zones to /v1/noise-zones BREAKING CHANGE: clients must update endpoint URL. Migration guide: docs/migrations/2026-05-rename-zones.md. Refs: PROJ-456 ``` --- ## Branch protection rules (на `main`) В forge (GitHub / Gitea / GitLab) настройки: - **Require pull request reviews before merging** — да, минимум 1 (от reviewer-агента). - **Dismiss stale reviews when new commits are pushed** — да. - **Require status checks to pass before merging** — да, обязательные: - `ci / lint` - `ci / type-check` - `ci / test-unit` - `ci / test-integration` - `ci / build` - `ci / coverage` - `ci / security-scan` - `qg / spec-lint` - `qg / adr-lint` - `qg / req-coverage` - `qg / e2e` - `qg / visual-regression` - `qg / a11y` - **Require branches to be up to date before merging** — да (rebase автоматически). - **Require linear history** — да (запрет merge-commit'ов; squash или rebase). - **Require signed commits** — рекомендуется (если возможна автоматическая подпись агентских коммитов). - **Restrict who can push to matching branches** — да, только сервисный аккаунт CI и `Owner`. - **Allow force pushes** — нет. - **Allow deletions** — нет. Те же правила (мягче) — для `feature/*`, `bugfix/*`, `hotfix/*`, чтобы запретить force-push и удаление веток. --- ## Pre-commit hooks (`.pre-commit-config.yaml`) Запускаются автоматически на `git commit` локально и в CI: ```yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-json - id: check-merge-conflict - id: check-added-large-files args: ['--maxkb=1024'] - repo: https://github.com/gitleaks/gitleaks rev: v8.18.0 hooks: - id: gitleaks - repo: https://github.com/conventional-changelog/commitlint rev: v19.0.0 hooks: - id: commitlint stages: [commit-msg] - repo: local hooks: - id: spec-lint name: spec-lint entry: ./scripts/lint-spec.sh language: script pass_filenames: false - id: adr-lint name: adr-lint entry: ./scripts/lint-adr.sh language: script pass_filenames: false - id: naming-check name: naming-check entry: ./scripts/check-naming.sh language: script pass_filenames: false - id: no-new-todos name: no-new-todos entry: ./scripts/no-new-todos.sh language: script pass_filenames: false ``` > Агентам **запрещено** обходить hooks через `--no-verify` без явного одобрения от Owner. --- ## CI/CD pipeline Файлы в `.github/workflows/` (или `.gitea/workflows/`). ### `ci.yml` — на каждый push в feature-ветку и PR ```yaml name: CI on: push: branches: ['feature/**', 'bugfix/**', 'hotfix/**', 'chore/**'] pull_request: branches: [main] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - run: make lint type-check: runs-on: ubuntu-latest steps: [..., make type-check] test-unit: runs-on: ubuntu-latest steps: [..., make test-unit] test-integration: runs-on: ubuntu-latest services: postgres: { image: postgres:16, ... } redis: { image: redis:7 } steps: [..., make test-integration] build: runs-on: ubuntu-latest steps: - uses: docker/setup-buildx-action@v3 - run: docker build -t app:${{ github.sha }} . coverage: runs-on: ubuntu-latest needs: [test-unit, test-integration] steps: [..., make coverage, ./scripts/coverage-delta.sh] security-scan: runs-on: ubuntu-latest steps: - uses: aquasecurity/trivy-action@master - run: bandit -r src/ || npm audit --production spec-lint: runs-on: ubuntu-latest steps: [..., ./scripts/lint-spec.sh] adr-lint: runs-on: ubuntu-latest steps: [..., ./scripts/lint-adr.sh] req-coverage: runs-on: ubuntu-latest steps: [..., python scripts/req-coverage.py] ``` ### `preview.yml` — ephemeral preview-окружение ```yaml name: Preview on: pull_request: types: [opened, synchronize, reopened] branches: [main] jobs: deploy-preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: docker compose -f docker-compose.test.yml -p preview-${{ github.event.number }} up -d --build - run: ./scripts/wait-healthy.sh preview-${{ github.event.number }} - run: | echo "preview_url=https://pr-${{ github.event.number }}.preview.example.com" >> $GITHUB_OUTPUT - uses: actions/github-script@v7 with: script: | github.rest.issues.addLabels({ ..., labels: ['preview:url:https://pr-${{ github.event.number }}.preview.example.com'] }) ``` ### `qg-test.yml` — полный тест-регресс на preview ```yaml name: QG-6 Test on: pull_request: types: [labeled] jobs: test: if: github.event.label.name == 'stage:test' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/run-test-plan.sh ${{ github.event.pull_request.number }} # запускает все TC из 04-test-plan.yaml - run: ./scripts/visual-regression.sh - run: ./scripts/a11y-check.sh - run: ./scripts/perf-check.sh - run: ./scripts/security-baseline.sh - run: ./scripts/generate-test-report.py > docs/work-items/${PLANE_ID}/13-test-report.md - uses: stefanzweifel/git-auto-commit-action@v5 ``` ### `deploy-test.yml` — деплой в test на merge в main ```yaml name: Deploy Test on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest environment: test steps: - uses: actions/checkout@v4 - name: Determine version bump run: ./scripts/semver-from-commits.sh > VERSION - name: Tag run: | VERSION=$(cat VERSION) git tag $VERSION git push origin $VERSION - name: Deploy run: ansible-playbook -i infra/ansible/inventory.test infra/ansible/deploy.yml - name: Wait healthy run: ./scripts/wait-healthy.sh test - name: Smoke test run: ./scripts/smoke.sh test - name: Update Plane run: ./scripts/plane-update.sh "$PLANE_ID" awaiting-prom-approval ``` ### `deploy-prom.yml` — деплой в prom по approve ```yaml name: Deploy Prom on: workflow_dispatch: inputs: version: required: true repository_dispatch: types: [plane-prom-approved] jobs: deploy: runs-on: ubuntu-latest environment: prom steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.version || github.event.client_payload.version }} - run: ansible-playbook -i infra/ansible/inventory.prom infra/ansible/deploy.yml - run: ./scripts/wait-healthy.sh prom 600 - run: ./scripts/smoke.sh prom - run: ./scripts/check-metrics.sh prom 600 # error rate / p95 в окне 10 минут - run: ./scripts/plane-update.sh "$PLANE_ID" awaiting-final-approval ``` ### `nightly.yml` — ночной регресс ```yaml name: Nightly Regression on: schedule: - cron: '0 2 * * *' # 02:00 UTC jobs: regression: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/run-full-regression.sh test - run: ./scripts/visual-regression.sh - if: failure() run: ./scripts/plane-create-issue.sh "Nightly regression failed" "incident:nightly" ``` --- ## Окружения Три **полностью идентичных по образу** окружения, отличающиеся только данными и секретами: | Среда | Назначение | Кто туда деплоит | URL | |-------|-----------|------------------|-----| | **dev** | Локальная разработка агента/инженера | разработчик через `make dev` | `http://localhost:*` | | **preview** | Эфемерное окружение на PR | CI автоматически на open/sync PR | `https://pr-.preview.example.com` | | **test** | Постоянное тестовое; на нём ночной регресс | CI на merge в `main` | `https://test..example.com` | | **prom** | Production | CI после approve | `https://.example.com` | **Принцип 12-Factor:** один Docker-образ, разница только в `.env` и секретах (управляются через CI secrets / Ansible vault). **Принцип «no SSH в prom»:** инженер и тем более агент не имеют SSH-доступа к серверам prom. Все изменения — через CI-pipeline. SSH разрешён только в read-only режиме для troubleshooting (через bastion + audit-лог). --- ## Ephemeral preview — детали При открытии PR CI: 1. Собирает образ из ветки. 2. Поднимает stack через `docker compose -f docker-compose.test.yml -p preview-`. 3. Подключает stack к доменной зоне `*.preview.example.com` через nginx-reverse-proxy (по wildcard). 4. Сидирует тестовые данные (фикстуры из `tests/fixtures/`). 5. Прогоняет healthcheck. 6. Постит preview-URL в комментарий PR и в лейбл `preview:url:`. При merge / закрытии PR — окружение автоматически удаляется (`docker compose down -v`). **Для 20 проектов** — один VPS (4 CPU / 16 GB RAM / 200 GB SSD) выдерживает 10–15 одновременных preview-окружений лёгких приложений; для тяжёлых (Postgres + кэш + frontend) — 5–8. --- ## Секреты и конфиги - **Никогда** не в репозитории. - В CI — через `${{ secrets.SECRET_NAME }}` (GitHub) / `${{ secrets.SECRET_NAME }}` (Gitea). - В test/prom — через **Ansible Vault** (`infra/ansible/secrets.yml.vault`) или через **HashiCorp Vault** (если уже есть). - В `.env.example` — только структура (имена переменных) с пустыми значениями. - `gitleaks` в pre-commit отлавливает попытки коммита секрета. --- ## Семантика hotfix Hotfix — отдельный лёгкий путь для прод-инцидентов: 1. Заводится Work Item типа `Incident` или `Bug` с лейблом `priority:p0` и `incident:prom`. 2. Branch: `hotfix/PROJ-NNN-` от **последнего prom-tag** (не от main, чтобы не подтянуть test-only изменения). 3. Process: тот же 7-этапный, но с урезанными SLA (Анализ — 30 мин, Архитектура — 30 мин, Дизайн — n/a, Разработка — 1ч, Review — 15 мин, Тест — 30 мин, Deploy — 15 мин). Override QG возможен через `:break-glass:` от Owner. 4. После merge в `main` — деплой в test → prom как обычно, плюс backport: коммиты hotfix'а уже в `main`, не нужно мержить отдельно. 5. После инцидента — обязательная ретроспектива и postmortem в `docs/operations/incidents/.md`. --- ## Релизы и теги - Каждый merge в `main` → автоматический tag `v` (semver). - `` определяется по типам commit'ов в PR (`feat` → minor, `fix`/`perf` → patch, `BREAKING CHANGE` → major). - Tag пушится в forge → CI запускает `deploy-test.yml`. - Release notes — автоматически из CHANGELOG.md (или из commit-сообщений). - В Plane создаётся комментарий на каждом Work Item, релизнутом в этом теге: «релиз vX.Y.Z, изменения: …». --- ## Forge (что использовать) Рекомендация по убыванию: 1. **GitHub** — если уже используется. Самый зрелый CI (Actions), отличная поддержка MCP, лучший UX. Платно при private + большом количестве минут CI. 2. **Gitea Actions** (self-hosted) — open-source, совместим с GitHub Actions YAML, дёшево, контроль над хранением. Минус — экосистема Actions беднее, некоторые сторонние actions придётся пере-писать. 3. **GitLab CE** (self-hosted) — мощно, но тяжелее в эксплуатации. Для ~20 проектов — **Gitea + Drone/Gitea Actions** даёт оптимальное соотношение цены и контроля. Если бюджет позволяет и команда привычна к GitHub — оставить GitHub. --- ## Service account для агентов Каждый агент коммитит от имени **сервисного git-аккаунта** (например, `claude-bot@example.com`): - Свой SSH/PAT-токен. - Подписывает коммиты GPG-ключом, хранящимся в CI secrets. - В `git config user.name` и `user.email` — фиксированные `claude-bot` / `claude-bot@example.com`. - Не имеет доступа на push в `main` — только в feature-ветки. Merge в main делается через PR от reviewer-аппрува. Зачем: даёт чёткую возможность отличать commits от агента и от человека (для метрик и аудита). --- ## Что хранится в монорепо vs полирепо Решение: **полирепо** (один репозиторий = один проект). Аргументы: - Plane Project — уже один репо. - Структура `docs/work-items//` локальна для проекта. - `CLAUDE.md` — на проект. - CI pipelines — независимы. Если возникает «общая дизайн-система» / «общая библиотека утилит» — отдельный репо с публикацией пакета (npm/pip), а не монорепо. --- ## Антипаттерны Git-flow - ❌ Долгоживущие feature-ветки (>5 дней). Если задача длинная — декомпозиция. - ❌ Несколько фич в одной ветке. Одна ветка = один Work Item. - ❌ Push в `main` напрямую. Только через PR + branch protection. - ❌ Merge-commit'ы (`Merge branch ...`). Только squash или rebase. - ❌ `--no-verify` без объяснения. - ❌ `--force-push` в main или общие ветки. - ❌ Коммиты от имени человека-разработчика, когда работал агент. Указывать `agent:` в author. - ❌ Закрывать PR, не создавая release-tag (даже для маленькой правки — все деплои через теги). - ❌ «Тестируем на test, прод заполним позже». Test и prom отличаются только данными, не образом.