(Анонс: Привет. Собираю статистику по инженерным процессам — от enterprise до инди‑команд (форма в конце статьи). Результаты агрегирую и опубликую на хабре в ближайшее время. Анонимно, 5–10 минут. Важен каждый.)

В сети очень много информации о том, как работать в большой и многорукой команде, с кучей выделенных ролей и настроенными процессами. Да я и сам отправляю к девопсам, когда на конференции подходят и спрашивают «а как правильно сделать репликацию базы данных?», потому что они (девопсы) сделают это быстрее и правильнее почти любого бэкендера. Но кроме компаний, в которых работают тысячи инженеров и есть отдел «на любой чих», — есть компании, в которых работают три джуна и один мидл (кстати, они-то и задают этот вопрос). Я и сам когда‑то с такой компании начинал… И эта статья для них.


Что мы хотим получить

Не «идеальный процесс» (да кому он нужен? не до жиру...), а спокойные, предсказуемые и повторяемые релизы на одном прод-сервере. Без превозмогания и героизма. За неделю — маленькими шагами.

Кому зайдёт

  • У тебя есть прод (одна штука). На проде есть SSH. Инфру менять нельзя.

  • Девопса нет, самая толстая лягушка на этом болоте — это ты (или не против ей стать). Все шишки сыпятся на тебя.

  • Хочется снизить долю «неудачных» релизов и время на латание. Пока непонятно как и насколько, но очень хочется...

У меня для тебя есть простой и действенный план. Минус только один: мало прочитать, придётся ещё и сделать.

Твой план на неделю (простой, но от этого не менее гениальный)

  • Релиз = тег. Привязываем выкладку к метке vX.Y.Z.

  • DoD релиза на одну страницу. Что именно должно быть готово.

  • Лёгкая дисциплина PR/MR. Ревью, порог по размеру + CODEOWNERS (если команда больше 2,5 человек).

  • Простой smoke-тест после выката. «CUJ, ты живой?»

  • Откат без боли. Если smoke не прошёл — вернулись на предыдущую сборку (и не ты вернул, а оно само как-то магическим образом «оп!»).

А теперь чуть подробнее.

Релизы — только из тегов

То, что у тебя вообще есть релизы, — уже здорово (не удивляйся: не то что релизы, скрамбан-то используют далеко не все). Теперь сделаем их предсказуемыми.

Ветки против тегов (на самом деле они совсем не против)

Ветки живые и изменяемые. Сегодня main указывает на один коммит, завтра — на другой. Через 10 минут туда может залететь force-push (хочешь ты этого или нет). В итоге можно зарелизить совсем не то, что планировалось и тестировалось. И «откатить ветку на вчера» нельзя, потому что у ветки нет «вчера».

Тег — это стоп-кадр. Метка, указывающая на конкретный коммит. Всегда на один и тот же — именно тот, который ты проверил и собираешься выкатить (иначе зачем помечать его тегом?). А откатиться на предыдущий тег — проще простого: git checkout v1.2.2

Теги победили. Но раз уж мы здесь, добавим ещё пару шагов.

Что стоит включить дополнительно

  • Запрет прямых пушей. Только через Pull Request / Merge Request. В GitHub/GitLab это решается галочкой в настройках. Иначе этот шаг будут обходить все кому не лень (а не лень будет всем). Я сам иногда забываю, что сижу на main, ещё не отбранчевался — и пушу туда.

  • Обязательный статус-чек. Тесты, линтер, сборка, статический анализ (если язык не компилируемый). Мёржим только зелёное. Задача со звёздочкой, но пользы — море.

  • Кросс-ревью. Если вас чуть больше, чем «совсем мало» (и особенно если в ревью не участвует кто-то сильнее тебя технически), лучше два ревьюера. Три — предлагать не буду, но иногда это тоже вполне ок, особенно если ревью у джуна.

Чуть CI/CD магии

Как завести CI в GitLab/GitHub — придётся разобраться самостоятельно (это несложно).

GitHub Actions
# .github/workflows/deploy.yml
name: Deploy on tag       # Название workflow, видно в интерфейсе GitHub Actions.
                          # Просто подпись, можно назвать "Продакшен деплой".

on:                      # Когда запускать этот workflow
  push:                  # … при событии push
    tags: ['v*.*.*']     # … но только если пушится тег, который выглядит как v1.2.3

jobs:                    # Набор задач, которые выполняет workflow
  deploy:                # Имя задачи — можно назвать "release" или как хотите
    runs-on: ubuntu-latest  # Запускать на виртуальной машине GitHub (Ubuntu)
    steps:                  # Последовательность шагов внутри задачи
      - uses: actions/checkout@v4
        # Первый шаг — "скачать" код репозитория
        # @v4 — версия официального экшена checkout
        # Без этого у runner-а не будет файлов проекта

      - run: ./scripts/deploy.sh "$GITHUB_REF_NAME"
        # Второй шаг — запускаем наш скрипт деплоя, передавая ему имя тега
        # $GITHUB_REF_NAME — встроенная переменная GitHub, здесь она равна имени тега
        # Скрипт получает этот тег и выкатывает ровно его
        # deploy.sh будет ниже
GitLab CI
# .gitlab-ci.yml
deploy:                   # Имя задачи — можно назвать как угодно, но обычно называют "deploy"
  stage: deploy           # Указываем стадию пайплайна. Обычно есть build/test/deploy
  rules:                  # Когда запускать эту задачу
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'  
      # Условие: если пайплайн запущен от тега, который совпадает с шаблоном vX.Y.Z
      # ^v\d+\.\d+\.\d+$  — регулярка: строка начинается с "v", потом три числа через точку (1.2.3)
      # Ветка сюда не подходит, только релизные теги.
  script:                 # Команды, которые нужно выполнить в раннере
    - ./scripts/deploy.sh "$CI_COMMIT_TAG"
      # Запускаем наш скрипт деплоя, передавая ему имя тега
      # $CI_COMMIT_TAG — встроенная переменная GitLab, здесь она равна имени тега
      #Скрипт получает этот тег и выкатывает ровно его
      # deploy.sh будет ниже

Осталось только запушить тег на коммит мержа — пайплайн подхватится сам (он слушает пуши) и доставит твой код.

Кстати в создании тегов нет ничего страшного
# аннотированный тег. Шаг 1
git tag -a v1.2.3 -m "Release 1.2.3: payments retry + admin filters"

# следующий уровень, если хотим побольше безовасности, подписываем
# альтернативный 1 шаг
git tag -s v1.2.3 -m "Release 1.2.3:..." # если настроен GPG
git tag -u ssh -s v1.2.3 -m "Release 1.2.3:..." # ssh ключом, git ≥ v2.34

# пушим. Шаг 2
git push origin v1.2.3
# альтенативный шаг 2. пушим все локальные теги, но можем запушить лишнее...
git push origin --tags

Поздравляю — теперь у тебя есть CI/CD, который выкатывает код по тегам. Было несложно, да?А если CI по каким-то причинам нет — всегда можно запустить вручную: ./deploy.sh v1.2.3 (deploy.sh будет ниже)

DoD релиза (определение готовности)

DoD (Definition of Done) — это список «минимального минимума», без которого релиз не считается готовым. По сути, это чек-лист безопасности: не прошёл по пунктам — не жми кнопку (в нашем случае пока что «не пуш тег»).

Звучит как очередная «бюрократическая ерунда для галочки». Частично это так, галочки там действительно есть, но на этом сходство заканчивается. Экономит DoD куда больше времени, чем тратит.

  • Меньше споров. Договорились один раз — и теперь правила одинаковые для всех (в том числе для тебя).

  • Меньше забытых мелочей. CI, changelog, smoke-тесты — всё на виду, а не только в чьей-то голове (а попробуй достань их оттуда, особенно когда человек занят и его отвлекли).

  • Меньше героизма. Релиз не держится на одном человеке, любой из команды может пройти по чек-листу и выкатить. (вот и bus-factor подрос... мелочь, а приятно.)

  • Легче откаты. Заранее записали, как именно откатываться. Ронял когда-нибудь прод? Если да, то знаешь, каково это — гуглить хотфиксы, пока тебе брызжут слюной в лицо.

Шаблон DoD. Копипасти в docs/release_dod.md, вноси правки под себя
[ ] PR/MR слиты, ревью 2+ профильных (CODEOWNERS)  
[ ] CI зелёный / сборка проверена локально  
[ ] Релизный тег создан (vX.Y.Z), CHANGELOG обновлён  
[ ] Фичи за флагами, если есть возможность (или явный kill-switch)  
[ ] Post-deploy smoke (3–5 проверок) готов  
[ ] Назначен владелец релиза (DRI)  
[ ] Понимаем, как откатываться, если smoke провалится  

PR/MR-дисциплина (минимальная)

Самый большой PR/MR, который я когда-либо делал, был порядка 40 тысяч изменений. Благо он был механический (спасибо Rector за это), но руки всё равно дрожали, когда нажимал кнопку merge. (Кстати, не зря дрожали, четверть системы таки отвалилась, пришлось экстренно откатываться.)

Думаю, все вы сталкивались с PR/MR «конского размера». Ревьюить такое — мягко говоря, неприятно. Во-первых, ничего не понятно. Во-вторых, вообще ничего не понятно. В-третьих, вы точно что-то пропустили. И чем крупнее PR/MR, тем выше вероятность, что там затаился баг (И скорее всего далеко не один).

Что делаем

  • Ставим ориентир по размеру. ≤ 400 строк (и добавления, и удаления). Вышли за рамки — режем. Как именно резать и что делать с гигантскими PR/MR — тема для отдельной статьи (оставляйте заявки в комментариях, может, и Patreon заведу). Пока договоримся на словах.

  • Назначаем владельцев областей через CODEOWNERS. Каждая папка с кодом должна иметь «владельца». Это может быть конкретный @vasilii или целая команда @team-backend. Если PR/MR задевает папку — обязательное ревью и апрув от владельца. Подробнее — в доках GitHub и GitLab.

  • SLA на ревью. Договоритесь между собой: «не больше двух рабочих дней на ревью, ок?». С первого раза может не получиться (точно будут срывать сроки), но хотя бы попробуете. А если ответственность в команде высокая, PR/MR режете на <400 LOC, а задачи декомпозированы хотя бы по 8 часов — то может и с первого раза выйдет.

CI или руками?

Можно прикрутить чекеры в CI, чтобы не пропускать PR/MR больше 400 строк. Но на старте я настоятельно не рекомендую. С непривычки набьёте шишек, скажете «фигня правило» и отключите. Лучше поработать с договорённостями на словах хотя бы 2–3 месяца, привыкнуть, а потом уже автоматизировать.

Простой post-deploy smoke (жив ли critical user journey)

Есть два стула.

  1. Ты выкатил релиз, увидел, что всё упало, и сразу откатил обратно.

  2. Ты выкатил релиз и спокойно пошёл домой (естественно, в пятницу вечером).

Какой вариант выбираешь? Подсказка: во втором случае о том, что всё упало, ты всё равно узнаешь — но позже. Когда клиенты достучатся до CEO, а он достучится до тебя лично, чтобы передать всю гамму чувств, которые он испытывает. Я бы выбрал первый вариант.

В одной из компаний, где я работал, пришлось писать health-checker на 400+ путей (если память не изменяет), ещё и за сессионной авторизацией с разными ролями. Но оно того стоило: мы выкатывали релиз, запускали чекер — и точно знали, что ничего не уронили.

/scripts/smoke.sh максимально простой вариант смоука
#!/usr/bin/env bash
# падаем на любой ошибке (ну а как вы хотели)
set -euo pipefail

# задаем переменные
BASE_URL="${BASE_URL:-https://your-prod.example.com}"
TIMEOUT="${TIMEOUT:-5}"
MAX_MS="${MAX_MS:-800}"
TOKEN="${TOKEN:-}"   # если нужен

# подкинем в хэдеры авторизацию если указан токен
headers=()
[ -n "$TOKEN" ] && headers+=(-H "Authorization: Bearer $TOKEN")

# список роутов для проверки.
checks=(
  "GET /health"
  "GET /api/ping"
  "GET /"
)

fail=0
# для каждого роута отправяем запрос, проверяем что не 500 и меньше таймаута
for c in "${checks[@]}"; do
  method=${c%% *}
  path=${c#* }
  read -r code time_total <<<"$(curl -sS -o /dev/null -m "$TIMEOUT" \
    -w "%{http_code} %{time_total}" "${headers[@]}" \
    -X "$method" "$BASE_URL$path")"
  ms=$(awk -v s="$time_total" 'BEGIN{printf "%.0f", s*1000}')
  echo "[$method $path] $code ${ms}ms"
  if [[ "$code" -ge 500 ]] || [[ "$ms" -gt "$MAX_MS" ]]; then
    fail=1
  fi
done

# или зафейлились или нет
if [[ $fail -eq 1 ]]; then
  echo "SMOKE FAIL"; exit 1
else
  echo "SMOKE OK"; exit 0
fi

Для начала просто скопируй себе скрипт, заменив роуты. Потом докрутишь «как нужно». Хоть какой-то смоук, который у тебя есть, всегда лучше, чем идеальный, которого у тебя нет. И не забывай докидывать новые критичные роуты по мере появления.

Деплой и откат на одном сервере

Вот мы и добрались. Ещё буквально пять минут — и ты станешь повелителем прода.

Наша цель — выкатывать новую сборку так, чтобы:

  • ты просто запушил тег;

  • оно самопроверилось (ещё на этапе мержа);

  • самовыкатилось на прод;

  • самопросмокалось (вот это я придумал конечно... нужно запомнить слово);

  • если что-то пошло не так — самооткатилось, как будто ничего и не было.

Для этого используем всё, что уже разобрали в статье, плюс подход с симлинком.

/srv/
  app/
    releases/
      v1.2.2/
      v1.2.3/
    current -> /srv/app/releases/v1.2.3
scripts/deploy.sh
#!/usr/bin/env bash
# падаем на любой ошибке
set -euo pipefail

# задаем переменные
TAG="${1:?TAG required e.g. v1.2.3}" #берется из аргументов запуска
APP=/srv/app # пишите свою базовую директорию
REL=$APP/releases
CUR=$(readlink -f "$APP/current" || true)
NEW="$REL/$TAG"

echo "Deploying $TAG..."
mkdir -p "$NEW"

# раскладываем сборку в $NEW (tar/rsync/docker pull). для любителей гита — смотри следующую кодовую вставку
# пример:
# tar -xzf "build/$TAG.tar.gz" -C "$NEW"

ln -sfn "$NEW" "$APP/current"
systemctl restart myapp.service   # или ваш способ рестарта

export BASE_URL="https://your-prod.example.com"
if ! ./scripts/smoke.sh; then
  echo "Smoke failed. Rolling back..."
  [ -n "$CUR" ] && ln -sfn "$CUR" "$APP/current" && systemctl restart myapp.service
  exit 1
fi

echo "Deployed $TAG OK"
модификация для любителей git (и не компилируемых языков). не рекомендуется, но всякое бывает

с worktree

# --- Git checkout по тегу в новую директорию релиза ---
REPO="$APP/repo"           # держим репозиторий отдельно от releases
mkdir -p "$REPO"

# инициализируем репо один раз
if [ ! -d "$REPO/.git" ]; then
  git clone --origin origin git@your.git.host:org/project.git "$REPO"
fi

cd "$REPO"
# подтягиваем теги и новую историю
git fetch --tags --force

# (опционально) проверяем подпись тега
# git tag -v "$TAG" || { echo "Tag signature invalid"; exit 1; }

# разрешаем тег в точный SHA и работаем уже с ним (на случай переписанного тега)
SHA=$(git rev-list -n1 "$TAG") || { echo "Tag not found: $TAG"; exit 1; }
echo "Resolved $TAG -> $SHA"

# создаём worktree на этот SHA прямо в $NEW
git worktree add --force --detach "$NEW" "$SHA"

# если есть сабмодули
if [ -f "$NEW/.gitmodules" ]; then
  git -C "$NEW" submodule update --init --recursive
fi

чистая копия (дольше, больше нагрузка на I/O)

# --- Git checkout по тегу в чистую директорию ---
TMP="$REL/.tmp-$TAG"
rm -rf "$TMP" && mkdir -p "$TMP"

git clone --no-checkout git@your.git.host:org/project.git "$TMP"
git -C "$TMP" fetch --tags --force
git -C "$TMP" checkout --force "tags/$TAG"

# сабмодули при необходимости
if [ -f "$TMP/.gitmodules" ]; then
  git -C "$TMP" submodule update --init --recursive
fi

# перемещаем в $NEW атомарно
rm -rf "$NEW"
mv "$TMP" "$NEW"

Итого

  • Создаёшь MR/PR.

  • На MR/PR отрабатывают чекеры, линтеры и прочие проверки — всё зелёное.

  • Ревью пройдено, владельцы кода поставили свой королевский апрув. Смержил.

  • Пушишь релизный тег (в статье показано, как пушить; но ещё лучше — запускать по кнопке, чтобы не было случайностей).

  • Запускается CI, который вызывает scripts/deploy.sh.

  • deploy.sh подтягивает новую версию приложения.

  • deploy.sh запускает scripts/smoke.sh.

  • smoke.sh проверяет указанные роуты.

  • Если всё ок — релиз завершён. Если зафейлилось — откат на предыдущий стабильный тег.

Пара часов со слезами и матом — и ты уже повелитель прода, который больше не парится по поводу релизов.

Потерпи ещё чуть-чуть — нам нужен DRI релиза.

Зачастую всё рассыпается не из-за техники или скриптов, а из-за человеческого фактора и дискоммуникации. Поэтому вводим роль DRI (Directly Responsible Individual — «непосредственно ответственный человек»)релиза. Это не герой, который будет всё тащить, а координатор: он отвечает за релиз, знает, что делать, и имеет полномочия принимать решения.

Что делает DRI релиза

  • Объявляет план: когда, что именно и сколько займёт.

  • Держит канал в курсе статуса: «идём по плану / стоп / откат».

  • Принимает решение об откате (по smoke/метрикам) — без согласований «по кругу».

  • Фиксирует итог и делает постмортем, если что-то пошло не так.

Вот теперь все.

У тебя в руках рабочий набор, который можно включить в маленькой команде буквально за один день. Или за неделю — если внедрять по одному правилу ежедневно. Начни уже сегодня: введи хотя бы одно правило, завтра — второе, и через неделю твой процесс заиграет новыми красками (а может, ещё и премию дадут или похвалят).

Кстати, сбрось статью товарищу, пусть знает твою доброту. А если эту статю сбросили тебе — с тебя пиво. Или кофе, но я бы выбрал пиво.


Про исследование

Собираю анонимные ответы по инженерной культуре — от enterprise до инди‑команд. Когда наберём материал, опубликую агрегированный отчёт: что чаще всего страдает, что в норме и что с этим делать. Ссылка на форму для заполнения (5-10 минут)

Комментарии (0)


  1. savostin
    26.09.2025 07:31

    Вот спасибо огромный. Наконец-то начну теперь.


    1. T2ig Автор
      26.09.2025 07:31

      супер, успешных успехов)


  1. olegl84
    26.09.2025 07:31

    проблема в том что мы тестим на проде, в идеале вообще тестить на стейдж. Я бы все реализовал бы на докер compose.


    1. T2ig Автор
      26.09.2025 07:31

      Отличное замечание. Полностью охватить тему в одной статье действительно невозможно. Даже с минимальным набором материала текст получился немаленьким. Я постарался дать каркас — «как сделать быстро, но полезно», а дальше его можно дорабатывать под свои задачи.