Мини‑проект: командный 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-проекта.
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 обновляется автоматически
Мерж
После одобрения ревьюером:
- Убедись, что pipeline зелёный
- Убедись, что нет конфликтов (если есть - rebase на develop)
- Нажми 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
Шаг 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
Шаг 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
Чеклист для нового проекта
Сводка всего, что нужно настроить:
Репозиторий:
[ ] .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и запуши его