Files
wiki/tasks/multi-agent/proposal_v1/07_git_workflow.md
2026-05-15 00:50:01 +03:00

20 KiB
Raw Blame History

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 — лишний слой для команды этого размера (515 человек).
  • 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-map
  • bugfix/PROJ-456-fix-empty-legend-rendering
  • hotfix/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 / 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:

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:

  1. Собирает образ из ветки.
  2. Поднимает stack через docker compose -f docker-compose.test.yml -p preview-<NN>.
  3. Подключает stack к доменной зоне *.preview.example.com через nginx-reverse-proxy (по wildcard).
  4. Сидирует тестовые данные (фикстуры из tests/fixtures/).
  5. Прогоняет healthcheck.
  6. Постит preview-URL в комментарий PR и в лейбл preview:url:<url>.

При merge / закрытии PR — окружение автоматически удаляется (docker compose down -v).

Для 20 проектов — один VPS (4 CPU / 16 GB RAM / 200 GB SSD) выдерживает 1015 одновременных preview-окружений лёгких приложений; для тяжёлых (Postgres + кэш + frontend) — 58.


Секреты и конфиги

  • Никогда не в репозитории.
  • В 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-<slug> от последнего 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/<date>.md.

Релизы и теги

  • Каждый merge в main → автоматический tag v<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 (что использовать)

Рекомендация по убыванию:

  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/<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 отличаются только данными, не образом.