Мини‑проект: командный workflow на GitLab

Мини-проект: командный workflow на GitLab

В предыдущих уроках ты изучил отдельные инструменты Git: ветки, мержи и rebase, теги, stash, bisect. Теперь соберём их вместе в рабочий процесс, который используют реальные команды. Этот урок - пошаговое руководство: от создания проекта на GitLab до релиза через CI/CD pipeline.

Workflow, описанный ниже, подходит для команд от 2 до 15 человек. Он проверен на практике и решает основные проблемы: конфликты в main, сломанный прод, «а кто это замержил без ревью?» и «как откатить последний деплой».

Шаг 1: Настройка проекта на GitLab

Создание репозитория

# Создаём Go-проект локально
mkdir myservice && cd myservice
go mod init github.com/team/myservice

# Инициализируем Git
git init
git add .
git commit -m "init: project scaffold"

# Подключаем remote (GitLab)
git remote add origin git@gitlab.example.com:team/myservice.git
git push -u origin main

Структура проекта

Минимальная структура для Go-сервиса:

myservice/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handler/
│   ├── service/
│   ├── repository/
│   └── domain/
├── migrations/
├── .gitlab-ci.yml
├── .gitignore
├── Makefile
├── Dockerfile
├── go.mod
└── go.sum

Что из этого попадёт в репозиторий, а что останется локально, решает .gitignore - как собрать его для Go-проекта, разбирали отдельно.

Защита веток

В GitLab: Settings → Repository → Protected Branches.

Branch: main
Allowed to merge: Maintainers
Allowed to push: No one
Require approval: 1 approval

Branch: develop
Allowed to merge: Developers + Maintainers
Allowed to push: No one

Это значит: никто не может пушить напрямую в main или develop. Все изменения - только через MR. Даже ты.

Настройка MR

Settings → Merge Requests:

Merge method: Merge commit (или Squash + Merge commit)
Squash commits: Encourage
Merge checks:
  ✓ Pipelines must succeed
  ✓ All discussions must be resolved
Merge request approvals:
  Required approvals: 1

Шаг 2: Соглашения команды

Именование веток

Договорённость об именах веток убирает двусмысленность:

Тип         │ Формат                        │ Пример
────────────┼───────────────────────────────┼──────────────────────
Фича        │ feature/FL-XXX-short-desc     │ feature/FL-142-rate-limit
Багфикс     │ fix/FL-XXX-short-desc         │ fix/FL-195-login-error
Хотфикс     │ hotfix/FL-XXX-short-desc      │ hotfix/FL-201-prod-crash
Рефакторинг │ refactor/short-desc           │ refactor/extract-auth-service
CI/Infra    │ chore/short-desc              │ chore/upgrade-go-1.22
Релиз       │ release/vX.Y.Z                │ release/v1.2.0

Префикс FL-XXX - номер задачи в трекере (Jira, GitLab Issues, Linear). Без номера задачи ветку не создаём - каждое изменение должно быть отслеживаемым.

Формат коммитов

Conventional Commits - стандарт, который позволяет автоматизировать changelog:

<type>: <description>

[optional body]

[optional footer]

Типы:

feat: - новая функциональность
fix: - исправление бага
refactor: - рефакторинг без изменения поведения
test: - добавление/изменение тестов
docs: - документация
chore: - CI, зависимости, инфраструктура
perf: - улучшение производительности

Примеры:

FL-142 feat: add rate limiting to auth endpoints
FL-195 fix: correct token expiration check in refresh handler
FL-200 refactor: extract email validation to domain package
chore: upgrade Go to 1.22, update golangci-lint

Правила:

  • Начинай с номера задачи (если есть)
  • Тип строчными буквами
  • Описание на английском (или на языке команды, главное - единообразно)
  • Первая строка до 72 символов
  • Тело коммита - для объяснения «почему», а не «что» (diff показывает «что»)

Шаг 3: CI/CD Pipeline

.gitlab-ci.yml для Go-проекта

Полноценный pipeline с четырьмя стадиями:

stages:
 - lint
 - test
 - build
 - deploy

variables:
  GOPATH: /go
  GO_VERSION: "1.22"
  GOLANGCI_LINT_VERSION: "v1.57.2"

# Кэширование Go-модулей между запусками
.go-cache: &go-cache
  cache:
    key: go-modules-${CI_COMMIT_REF_SLUG}
    paths:
 - .go/pkg/mod/
    policy: pull-push

# ── Lint ──────────────────────────────────────────────

lint:
  stage: lint
  image: golangci/golangci-lint:${GOLANGCI_LINT_VERSION}
  <<: *go-cache
  script:
 - cd backend
 - golangci-lint run ./... --timeout=5m
  rules:
 - if: $CI_MERGE_REQUEST_ID
 - if: $CI_COMMIT_BRANCH == "develop"
 - if: $CI_COMMIT_BRANCH == "main"

# ── Test ──────────────────────────────────────────────

test:unit:
  stage: test
  image: golang:${GO_VERSION}
  <<: *go-cache
  script:
 - cd backend
 - go test ./... -count=1 -race -coverprofile=coverage.out
 - go tool cover -func=coverage.out
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: backend/coverage.out
  coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'
  rules:
 - if: $CI_MERGE_REQUEST_ID
 - if: $CI_COMMIT_BRANCH == "develop"
 - if: $CI_COMMIT_BRANCH == "main"

test:e2e:
  stage: test
  image: docker:24
  services:
 - docker:24-dind
  script:
 - docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
  rules:
 - if: $CI_COMMIT_BRANCH == "main"
 - if: $CI_COMMIT_TAG

# ── Build ─────────────────────────────────────────────

build:
  stage: build
  image: docker:24
  services:
 - docker:24-dind
  script:
 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
 - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -f backend/Dockerfile .
 - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    # Тегируем latest для main
 - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest
        docker push $CI_REGISTRY_IMAGE:latest
      fi
    # Тегируем версией для тегов
 - |
      if [ -n "$CI_COMMIT_TAG" ]; then
        docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
        docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
      fi
  rules:
 - if: $CI_COMMIT_BRANCH == "main"
 - if: $CI_COMMIT_TAG

# ── Deploy ────────────────────────────────────────────

deploy:staging:
  stage: deploy
  image: alpine:3.19
  before_script:
 - apk update && apk add openssh-client
 - eval $(ssh-agent -s)
 - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
 - ssh -o StrictHostKeyChecking=no deploy@$STAGING_HOST "
        cd /opt/myservice &&
        docker compose pull &&
        docker compose up -d --remove-orphans
      "
  environment:
    name: staging
    url: https://staging.example.com
  rules:
 - if: $CI_COMMIT_BRANCH == "main"

deploy:production:
  stage: deploy
  image: alpine:3.19
  before_script:
 - apk update && apk add openssh-client
 - eval $(ssh-agent -s)
 - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
 - ssh -o StrictHostKeyChecking=no deploy@$PROD_HOST "
        cd /opt/myservice &&
        export IMAGE_TAG=$CI_COMMIT_TAG &&
        docker compose pull &&
        docker compose up -d --remove-orphans
      "
  environment:
    name: production
    url: https://example.com
  rules:
 - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  when: manual

Что здесь происходит:

  • lint - запускается на каждый MR и пуш в develop/main
  • test:unit - юнит-тесты с подсчётом покрытия
  • test:e2e - интеграционные тесты только на main и тегах
  • build - собирает Docker-образ, пушит в registry
  • deploy:staging - автоматический деплой в staging при пуше в main
  • deploy:production - ручной деплой при создании тега (кнопка в GitLab UI)

Подробный разбор такого pipeline - от сборки образа до пуша в registry - в уроке GitLab CI для Docker-проекта.

`golangci/golangci-lint:v1.57.2`, а не `golangci/golangci-lint:latest`. `golang:1.22`, а не `golang:latest`. Иначе pipeline может внезапно сломаться из-за обновления инструмента, а не из-за твоего кода.

Makefile

CI/CD и локальная разработка используют одни и те же команды через Makefile:

.PHONY: lint test build run

GO_MODULE := github.com/team/myservice
BINARY := bin/server

## Линтинг
lint:
	cd backend && golangci-lint run ./... --timeout=5m

## Тесты
test:
	cd backend && go test ./... -count=1 -race

test-coverage:
	cd backend && go test ./... -count=1 -coverprofile=coverage.out
	cd backend && go tool cover -html=coverage.out -o coverage.html

## Сборка
build:
	cd backend && go build -o $(BINARY) ./cmd/server

## Запуск
run: build
	cd backend && ./$(BINARY)

## Docker
docker-up:
	docker compose up -d --build

docker-down:
	docker compose down

## E2E
test-e2e:
	docker compose -f docker-compose.test.yml up --build --abort-on-container-exit

Шаг 4: Ежедневный workflow разработчика

Начало работы над задачей

# 1. Убедись, что develop актуален
git checkout develop
git pull origin develop

# 2. Создай ветку от develop
git checkout -b feature/FL-142-rate-limit

# 3. Работай: пиши код, коммить часто
# Первый коммит - тесты (TDD)
vim backend/internal/service/auth_test.go
git add .
git commit -m "FL-142 test: add rate limiting tests (red)"

# Второй коммит - реализация
vim backend/internal/middleware/rate_limit.go
vim backend/internal/handler/auth.go
git add .
git commit -m "FL-142 feat: add rate limiting middleware"

# Третий коммит - интеграция
vim backend/cmd/server/wire.go
git add .
git commit -m "FL-142 chore: wire rate limiting into DI"

# 4. Перед пушем - проверь локально
make lint
make test

# 5. Ребейзни на свежий develop (могли быть изменения)
git fetch origin
git rebase origin/develop

# 6. Запуши
git push origin feature/FL-142-rate-limit

Создание MR

После пуша GitLab покажет ссылку на создание MR. Или создай вручную:

Source branch: feature/FL-142-rate-limit
Target branch: develop
Title: FL-142 feat: add rate limiting to auth endpoints
Description: (по шаблону из урока 07)
Assignee: ты
Reviewer: коллега
Labels: backend, security

Реакция на ревью

# Коллега оставил замечания. Исправляем:
git checkout feature/FL-142-rate-limit
vim backend/internal/middleware/rate_limit.go
git add .
git commit -m "FL-142 fix: address review comments - extract config"

git push origin feature/FL-142-rate-limit
# MR обновляется автоматически

Мерж

После одобрения ревьюером:

  1. Убедись, что pipeline зелёный
  2. Убедись, что нет конфликтов (если есть - rebase на develop)
  3. Нажми Merge (с включённым squash, если нужна чистая история)
# Если нужно разрешить конфликты
git checkout feature/FL-142-rate-limit
git fetch origin
git rebase origin/develop
# Разреши конфликты
git add .
git rebase --continue
git push --force-with-lease origin feature/FL-142-rate-limit
`--force-with-lease` безопаснее: он откажет в пуше, если кто-то успел запушить в ту же ветку. `--force` перезаписывает безусловно. Всегда используй `--force-with-lease`.

Шаг 5: Релизный процесс

Подготовка релиза

Когда develop набрал достаточно фич для релиза:

# 1. Создай release-ветку
git checkout develop
git pull origin develop
git checkout -b release/v1.2.0

# 2. Обнови версию в коде (если есть)
# Например, в main.go или version.go
echo 'package main

const Version = "1.2.0"' > backend/cmd/server/version.go
git add .
git commit -m "chore: bump version to 1.2.0"

# 3. Финальное тестирование на release-ветке
make test
make test-e2e

# 4. Исправь баги, если нашлись (коммиты в release-ветку)
git commit -m "fix: correct error message in login handler"

# 5. Мерж в main через MR
# Source: release/v1.2.0 → Target: main

Создание тега и деплой

# После мержа release → main
git checkout main
git pull origin main

# Создай annotated тег
git tag -a v1.2.0 -m "Release v1.2.0

Features:
- FL-142: Rate limiting on auth endpoints
- FL-150: OAuth Google provider
- FL-167: Progress sync API

Fixes:
- FL-195: Login validation error
- FL-198: Token refresh race condition"

# Запуши тег
git push origin v1.2.0

# CI/CD pipeline запустится автоматически (build + deploy:production manual)
# В GitLab UI нажми кнопку deploy для production

# Не забудь мержнуть main обратно в develop
git checkout develop
git merge main
git push origin develop

Шаг 6: Hotfix workflow

Баг на проде. Нужно починить быстро, не дожидаясь следующего релиза.

# 1. Создай hotfix-ветку от main (не от develop!)
git checkout main
git pull origin main
git checkout -b hotfix/FL-201-prod-crash

# 2. Исправь баг
vim backend/internal/service/user.go
git add .
git commit -m "FL-201 fix: handle nil user in GetProfile"

# 3. Добавь тест
vim backend/internal/service/user_test.go
git add .
git commit -m "FL-201 test: cover nil user case in GetProfile"

# 4. Проверь
make test

# 5. Открой MR в main
git push origin hotfix/FL-201-prod-crash
# MR: hotfix/FL-201-prod-crash → main

# 6. После мержа - тег
git checkout main
git pull origin main
git tag -a v1.2.1 -m "Hotfix v1.2.1: fix nil user crash in GetProfile"
git push origin v1.2.1

# 7. Мержни main обратно в develop (чтобы фикс был и там)
git checkout develop
git merge main
git push origin develop

Поток релиза с тегами SemVer: main с тегами v1.2.0 и v1.2.1, release готовит релиз и вливается в main, develop продолжает разработку, hotfix исправляет прод и вливается обратно в main и develop

Шаг 7: Approval Rules в GitLab

Для серьёзных проектов настрой правила одобрения:

Settings → Merge Requests → Approval Rules

Rule 1: "Backend Review"
  Approvers: @backend-team
  Required: 1
  Target: backend/**

Rule 2: "Security Review"  
  Approvers: @security-team
  Required: 1
  Target: **/auth/**, **/middleware/**, .gitlab-ci.yml

Rule 3: "Any Developer"
  Approvers: @developers
  Required: 1
  Target: All files

Это значит: если MR затрагивает auth - нужен ревью от security team. Любой MR - хотя бы один ревью от любого разработчика.

Шаг 8: GitLab CI/CD переменные

Секреты для CI/CD хранятся в настройках проекта, а не в коде:

Settings → CI/CD → Variables

SSH_PRIVATE_KEY     (masked, protected) - ключ для деплоя
STAGING_HOST        (protected) - IP staging-сервера
PROD_HOST           (protected) - IP prod-сервера
DB_PASSWORD         (masked, protected) - пароль БД

Флаги:

  • Protected - переменная доступна только в protected ветках (main) и тегах
  • Masked - значение не отображается в логах pipeline
Файл `.gitlab-ci.yml` - часть репозитория. Все, у кого есть доступ к коду, увидят его содержимое. Используй CI/CD Variables для паролей, токенов и ключей.

Чеклист для нового проекта

Сводка всего, что нужно настроить:

Репозиторий:
  [ ] .gitignore для Go (бинарники, .env, IDE)
  [ ] README.md с описанием и инструкцией по запуску
  [ ] Makefile с основными командами (lint, test, build, run)
  [ ] .gitlab-ci.yml с lint → test → build → deploy стадиями

Ветки:
  [ ] main - protected (no direct push, require MR, require pipeline)
  [ ] develop - protected (no direct push, require MR)

MR:
  [ ] Шаблон MR в .gitlab/merge_request_templates/Default.md
  [ ] Squash on merge: Encourage
  [ ] Pipeline must succeed: Enabled
  [ ] All discussions must be resolved: Enabled
  [ ] Minimum 1 approval

CI/CD:
  [ ] Variables: SSH_PRIVATE_KEY, *_HOST, DB_PASSWORD (protected + masked)
  [ ] Кэширование Go-модулей
  [ ] Версии инструментов запинены (не @latest)

Соглашения команды:
  [ ] Формат имён веток: feature/FL-XXX-desc
  [ ] Формат коммитов: FL-XXX type: description
  [ ] Релизный процесс: develop → release → main → tag
  [ ] Hotfix процесс: main → hotfix → main + tag → merge back to develop

Мини-задание

  • Создай проект на GitLab с защищённой веткой main и веткой develop
  • Настрой .gitlab-ci.yml хотя бы с двумя стадиями: lint и test
  • Создай ветку feature/test-workflow, сделай коммит, открой MR в develop
  • Добавь шаблон MR в .gitlab/merge_request_templates/Default.md
  • После мержа MR создай annotated тег v0.1.0 и запуши его

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.