{ "index": 290, "slug": "editorial-2019-12-mechanism-code-ownership", "title": "Кому принадлежит изменение: Git, CODEOWNERS и границы ответственности", "excerpt": "Последний автор строки не всегда знает смысл контракта, а CODEOWNERS не заменяет review. Разбираем, как разделить историю, маршрут проверки, решение и контроль после merge.", "contentHtml": "
Ошибка на стыке модулей часто начинается с простой задержки. В задаче уже указан файл, в git blame видно имя, но исправление ждёт ответа. Разработчик считает владельцем последнего автора строки. Автор помнит рефакторинг и не знает, кто принимает решение по контракту. Pull request получает формальный approval, а после merge никто не проверяет спорный ответ. Цена ошибки — не только потерянные часы. Неизвестный статус может попасть в успешный сценарий, изменение контракта — пройти без нужного специалиста, а тот же дефект вернуться в соседнем файле.
Тезис простой: «владелец кода» — не одна роль. История Git отвечает на вопрос «кто менял». CODEOWNERS отвечает на вопрос «кого запросить по пути». Review отвечает на вопрос «какое решение принято для diff». Отдельное назначение нужно для проверки после merge. Если смешать эти ответы, команда получает имя вместо ответственности. Если разделить их, каждый риск получает проверяемый маршрут.
\nФормулировка «у модуля нет владельца» слишком общая. Начните с наблюдаемого поведения: gateway вернул unknown, checkout показал успешную оплату; retry записал старый результат поверх нового; изменение файла прошло без человека, который знает допустимые статусы. В такой записи есть вход, неверный результат и цена. Она помогает найти границу решения, а не назначить виноватого.
Пример ниже искусственный. Он не описывает production-инцидент, настоящих людей или измеренный эффект. В нём обработчик получает ответ платёжного gateway. Статусы paid и declined известны, остальные значения функция возвращает как отдельное unknown. Следующий переход не должен трактовать его как success, а исходный ответ остаётся доступным для диагностики.
function nextState(response) {\n if (response.status === 'paid') return 'success';\n if (response.status === 'declined') return 'failure';\n return 'unknown';\n}\n\n// Искусственный пример: unknown не считается success.\nconst state = nextState({ status: 'pending_review' });\nconsole.log(state); // unknown\nФункция возвращает unknown, но сама по себе не знает, что делать дальше. Владелец контракта решает, допустим ли отдельный unknown, можно ли повторить запрос и какие данные сохранить. Владелец пути кода проверяет обработчик, переходы интерфейса и тест. Эти люди могут совпасть, но совпадение надо подтвердить, а не предположить по истории строки.
git blame показывает revision и автора, которые последними изменили строку. Это полезный вход в расследование. Строка могла прийти из рефакторинга, переноса файла или форматирования. Поэтому blame не доказывает, что автор принимает сегодняшнее бизнес-решение. git log добавляет историю пути и связанных изменений, но также не назначает текущую ответственность.
Ниже CODEOWNERS означает именно механизм GitHub. Он сопоставляет путь с пользователями или командами и автоматически запрашивает их review для обычного открытого pull request; draft pull request не вызывает такой запрос, пока его не переведут в режим ready for review. GitHub ищет файл в .github/, корне или docs/; если файлов несколько, использует первый по порядку поиска. Для запроса review GitHub использует CODEOWNERS из base branch — ветки, в которую попадёт pull request. Правило в feature branch само по себе не гарантирует нужный маршрут.
Review тоже имеет узкую границу. Comment оставляет замечание, Approve сообщает, что изменение готово к merge, Request changes блокирует принятие до исправления. Ни один статус не записывает, кто проверит поведение после выпуска. Поэтому follow-up надо назначить отдельно: это может быть проверка лога, сценария или отдельная техническая задача.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Задача уходит к автору строки | Историю приняли за текущего владельца решения | git blame -L, затем diff и связанный контракт | Сохранить автора как факт и отдельно назначить decision owner |
| Нужный reviewer не получил запрос | Не совпал путь, выбран не тот base branch или owner не имеет доступа | Проверить расположение файла, регистр пути, порядок правил и права | Исправить точное правило CODEOWNERS и открыть новый review-маршрут |
| Есть approval, но спорный статус не разобран | Review проверил форму diff, а не риск контракта | Найти в обсуждении ответ на вопрос о unknown | Запросить конкретное подтверждение или отправить Request changes |
| После merge никто не знает результат | Follow-up не был частью записи изменения | Проверить назначенного исполнителя и ожидаемый сигнал | Создать отдельное действие; не закрывать задачу по одному факту merge |
В примере общий маршрут задаёт запасного reviewer, а более узкие пути уточняют область. Правила вымышлены и приведены только для объяснения синтаксиса. В GitHub последнее совпавшее правило имеет больший приоритет. Несколько owners для одного пути записывают в одной строке. Отрицание через ! и диапазоны в квадратных скобках нельзя переносить из `.gitignore` без проверки: в CODEOWNERS они не работают как в gitignore.
# Искусственный пример, не конфигурация рабочего репозитория.\n* @example/platform\n/web/checkout/ @example/checkout\n/web/checkout/gateway/ @example/payments\n/docs/checkout-contract.md @example/payments @example/checkout\n/.github/CODEOWNERS @example/repository-admins\nДля изменения /web/checkout/gateway/status.js сначала проверяют, какое правило совпало и кто имеет право получить review. Для документа контракта можно явно запросить обе команды, если именно они обладают знаниями о семантике и потребителе. Но сама строка с двумя owners не делает approval обоих обязательными: при требовании review from Code Owners GitHub считает достаточным approval любого из них. Если риск требует двух разных решений, это надо записать в описании изменения и запросить обе команды явно.
Короткая запись в issue или pull request должна связывать риск с ролью. Для искусственного сценария достаточно такого контракта:
\nsymptom: unknown status enters success flow\ndecision_owner: payments-contract\ncode_path_owner: checkout\nreview_questions:\n - Is unknown a separate state?\n - Can retry create a second transition?\nfollow_up_owner: checkout\nready_when:\n - unknown is visible in the test\n - success transition rejects unknown\n - review answers both questions\n - follow-up has a recorded result\nЭта запись не создаёт новую должность и не обещает реальный production-сигнал. Она задаёт границу проверки. Если проект не использует CODEOWNERS, те же поля можно хранить в задаче и запросить reviewer вручную. Механизм маршрутизации изменится, но вопросы останутся теми же.
\ngit blame -L, затем git log --follow -- path и связанные документы. Не переносите имя из истории в поле decision owner.Отрицательный путь должен ломать удобную гипотезу. Удалите владельца из учебной записи: критерий готовности должен перестать выполняться. Подставьте неизвестный статус: он не должен перейти в success. Перенесите файл в соседний каталог: маршрут CODEOWNERS должен измениться или проверка должна сообщить о несоответствии. Если любой из этих тестов проходит без назначенного решения, критерий слишком слабый.
Есть и ограничения механизма. CODEOWNERS знает путь, но не знает, кто владеет внешним API или продуктовым смыслом. Git знает историю, но не знает будущую ответственность. Review фиксирует решение по конкретному diff и может устареть после существенной правки. Автоматический запрос не равен прочитанному review. Эти границы нельзя закрыть ещё одним wildcard-правилом.
\nМинимальная рабочая карта выглядит так: один симптом, один владелец решения, владелец пути, reviewer с вопросом по риску, отрицательный тест и отдельный follow-up. Один человек может занимать все роли. Готовность проверяется не количеством имён, а свидетельствами: неизвестный вход остаётся отдельным состоянием, review отвечает на заявленные вопросы, а последующее действие имеет записанный результат или явную новую задачу.
\n