20 KiB
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/<plane-id>-<slug>— короткоживущие фичевые ветки (≤5 дней жизни в норме). Отmain, вmain.bugfix/<plane-id>-<slug>— то же, что фича, но для багов.hotfix/<plane-id>-<slug>— срочные фиксы для prom. От тега prom-релиза, мерджатся вmainи cherry-pick'ятся как новый prom-tag.phase/<phase-id>-<slug>— для крупных фаз, объединяющих несколько фич (опционально, не для всех проектов).chore/<slug>— обслуживание без Plane-задачи (обновление зависимостей, инфра-tweaks). Допускается в редких случаях.
Не используются:
develop— лишний слой для команды этого размера (5–15 человек).- Long-lived release-ветки — релизы делаются через теги, не ветки.
gh-pagesили другие магические ветки — кроме случая, когда хостится статика.
Конвенция имён
feature/<plane-id>-<kebab-slug>
bugfix/<plane-id>-<kebab-slug>
hotfix/<plane-id>-<kebab-slug>
phase/<phase-id>-<kebab-slug>
chore/<kebab-slug>
<plane-id> — точный ID Work Item, как в Plane (PROJ-123).
<kebab-slug> — короткий описательный slug (≤50 символов).
Примеры:
feature/PROJ-123-add-noise-zones-on-mapbugfix/PROJ-456-fix-empty-legend-renderinghotfix/PROJ-789-revert-broken-rate-limit
Conventional Commits
Каждый commit:
<type>(<scope>): <subject>
<body>
Refs: <plane-id> # или Closes: <plane-id> для финального
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 или в <type>!: префиксе. Триггерит 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 / lintci / type-checkci / test-unitci / test-integrationci / buildci / coverageci / security-scanqg / spec-lintqg / adr-lintqg / req-coverageqg / e2eqg / visual-regressionqg / 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:
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
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-окружение
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
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
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
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 — ночной регресс
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-<NN>.preview.example.com |
| test | Постоянное тестовое; на нём ночной регресс | CI на merge в main |
https://test.<project>.example.com |
| prom | Production | CI после approve | https://<project>.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:
- Собирает образ из ветки.
- Поднимает stack через
docker compose -f docker-compose.test.yml -p preview-<NN>. - Подключает stack к доменной зоне
*.preview.example.comчерез nginx-reverse-proxy (по wildcard). - Сидирует тестовые данные (фикстуры из
tests/fixtures/). - Прогоняет healthcheck.
- Постит preview-URL в комментарий PR и в лейбл
preview:url:<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 — отдельный лёгкий путь для прод-инцидентов:
- Заводится Work Item типа
IncidentилиBugс лейбломpriority:p0иincident:prom. - Branch:
hotfix/PROJ-NNN-<slug>от последнего prom-tag (не от main, чтобы не подтянуть test-only изменения). - Process: тот же 7-этапный, но с урезанными SLA (Анализ — 30 мин, Архитектура — 30 мин, Дизайн — n/a, Разработка — 1ч, Review — 15 мин, Тест — 30 мин, Deploy — 15 мин). Override QG возможен через
:break-glass:от Owner. - После merge в
main— деплой в test → prom как обычно, плюс backport: коммиты hotfix'а уже вmain, не нужно мержить отдельно. - После инцидента — обязательная ретроспектива и postmortem в
docs/operations/incidents/<date>.md.
Релизы и теги
- Каждый merge в
main→ автоматический tagv<X.Y.Z>(semver). <X.Y.Z>определяется по типам 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 (что использовать)
Рекомендация по убыванию:
- GitHub — если уже используется. Самый зрелый CI (Actions), отличная поддержка MCP, лучший UX. Платно при private + большом количестве минут CI.
- Gitea Actions (self-hosted) — open-source, совместим с GitHub Actions YAML, дёшево, контроль над хранением. Минус — экосистема Actions беднее, некоторые сторонние actions придётся пере-писать.
- 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/<id>/локальна для проекта. 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:<role>в author. - ❌ Закрывать PR, не создавая release-tag (даже для маленькой правки — все деплои через теги).
- ❌ «Тестируем на test, прод заполним позже». Test и prom отличаются только данными, не образом.