Проблема со сложностью управления TODO/FIXME комментариями в коде очень стара. Ей больше 50 лет. Дошло до того, что в некоторых проектах начали запрещать их со словами: “или исправь сразу или не создавай мусор”. Вот о том как можно автоматизировать управление ими и превратить из мусора в полезный инструмент мы и поговорим.


TL;DR;

  1. Берём инструмент, который будет регистрировать новые TODO/FIXME в ишью/баг трекере. Например, в Jira. 

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

  3. Применяем их к различным сценариям использования этих инструментов. 

Теперь подробности. 

Введение / проблематика

Каждый более-менее опытный разработчик либо встречал в коде, либо сам писал пометки вида TODO/FIXME и подобные (далее для кратко будем называть их просто TODO). Порой приходишь на проект, а там 100500 таких пометок. Некоторым уже по 5+ лет и никто ничего не делает с ними. О большинстве из них уже забыли.

За десятилетия с момента возникновения проблемы появилось множество инструментов автоматизации. Теперь у нас есть CI и различные инструменты контроля качества кода. Грех этим не воспользоваться. Можно настроить передачу новых TODO из кода в баг-трэкинговую систему, например, JIRA. Затем настроить контроль за удалением из кода этих пометок, когда задачи завершённы. 

Кто-то может возразить: “оно же будет фонить и получите много мусорных тикетов”. На что хочу пояснить. Это ожидаемое поведение. Более того — желаемое.

Представим ситуацию. Разработчик сдаёт мердж-реквест в котором 10+ новых TODO. Мердж-реквест проходит ревью и все соглашаются с тем, что обозначенные проблемы в коде есть, их нужно исправлять, но не сейчас… Кто должен помнить об этом? И ладно если есть на проекте тимлид и он будет помнить об этих проблемах какое-то время и, возможно, поставит задачу. А если процесс принятия мердж-реквестов построен так, что они могут приниматься без тимлида? Или на проекте нет тимлид, но есть проджект-менеджер, который не смотрит в код? В любом случае, кто бы не управлял проектом, нужно выделить ресурсы для выполнения работ, запланировать их, и увязать с выполнением бизнес-задач. Значит, всё равно нужно заводить тикет. Так пускай они автоматически залетают тикет-трэкинговую систему в бэклог. С настроенными лейбами, компонентами, приоритетом, заасайненными на того, кто должен разбирать их, и прочими плюшками. И далее с ними будет происходить магия менеджмента.

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

Внедрив этот подход, мы можем начать стимулировать разработчиков создавать TODO в коде. Для чего? Всё просто. Например, бизнес просит временно не тратить ресурсы на написание тестов. Либо реализовать самое простое и быстрое решение, хоть и кривое. И девелопер в процессе реализации видит блок кода, требующий рефакторинга. Или намеренно реализуется только обработка позитивного флоу. Он предупреждает бизнес, что вырастет тех. долг и смело расставляет TODO в коде. Затем, когда приходит время заняться тех. долгом, мы разговариваем с бизнесом не об абстрактном тех. долге, размер которого реально никто не знает, а о конкретных тикетах с описанием, линками на родительские тикеты, эстимейшенами и приоритетами. И это, согласитесь, совсем другой разговор.

Регистрация TODO в баг-трекере

К своему удивлению, я не нашёл готового инструмента для переноса TODO из кода в баг-трекер. Пришлось писать. И делал это с оглядкой на выбранный инструмент по контролю за завершёнными TODO (о нём ниже). 

С готовым инструментом делается это просто:

  1. Установил.

  2. Сконфигурировал.

  3. Добавил джобу на CI.

Далее, чуть подробнее по каждому пункту. 

Установка

Установка простая. Добавляете пакет “aeliot/todo-registrar” в dev-зависимости компоузера. Позже планируется упаковать в PHAR, чтобы в проектах иметь меньше проблем с зависимостями. Чуть позже планируется статья об этом. 

Конфигурация

Создаёте файл “.todo-registrar.dist.php” в корне проекта. Можно и в любом другом месте, но тогда при вызове скрипта нужно будет передать путь к конфигу. Этот файл должен возвращать экземпляр класса \Aeliot\TodoRegistrar\Config. 

Пример можно посмотреть в корне пакета.

Базовый конфиг

Базовый конфиг достаточно простой:

  1. Нужно указать массив тегов, по которым будет осуществляться поиск TODO, если он отличается от базового набора. По умолчанию, настроена поддержка TODO и FIXME. Вместе с тем, вы можете настроить отслеживание любых собственных тегов. Поиск тегов регистронезависимый.

  2. Передать сконфигурированный Finder, отвечающий за поиск файлов. Если когда-либо настраивали PHP CS Fixer — здесь идентично. 

  3. Указать тип баг-трекера и конфиги к нему. Вместо энума с выбранным типом регистратора можно передать свою фабрику регистратора. Интерфейс там максимально простой. Это значительно упрощает использование кастомных решений.

Настройки специфичные для ишью-трекера

Чуть более сложен конфиг непосредственно регистратора.

На текущий момент из популярных баг/ишью трекеров реализована поддержка только JIRA. Остальные в планах на разработку. Если есть желание, то можно помочь с этим.

Здесь необходимо указать:

  1. параметры подключения  API JIRA. Авторизация возможна как по токену (рекомендуется), так и с использованием логина и пароля. 

  2. код проекта в JIRA

  3. общие настройки тикетов. Такие как:

    • Тип создаваемого тикета 

    • Лейбы, общие для всех тикетов

    • Компоненты, общие для всех тикетов

    • Приоритеты и пр. 

Тонкая настройка создаваемых тикетов

Здесь есть несколько инструментов для настройки разных аспектов. 

Во-первых, использование многострочных комментариев позволяет выделить заголовок и тело тикетах. 

Во-вторых, трекер-специфичные настройки. Например, JIRA-регистратор позволяют указать лейбы, компоненты, приоритет и тип создаваемых тикетов. 

В третьих, можно определить assignee, как в основном конфиге регистратора, так и через символ @ сразу за тегом (// TODO@my.assignee: ... ).

В четвёртых, можно иметь несколько конфигов для разных частей кода или для разных тегов. 

В пятых, инлайн-конфиг. Можно прямо в тексте комментария дополнить или переопределить разные параметры тикета, применительно к конкретной TODO.

Инлайн-конфиг

Интересной фичей компонента является инлайн-конфиг. Он позволяет прямо в тексте комментария переопределить базовые настройки тикетов и/или дополнить их. Например лейбами или линками на другие тикеты. Указать компоненты. Переопределить тип создаваемого тикета и его приоритет. Почти всё, что душе угодно. Очень гибкий инструмент. 

Сейчас реализованы “EXTRAS”. Это упрощённый формат похожий, на js-объект или JSON только без кавычек, содержащий корневое поле “EXTRAS”. Закрывает большинство потребностей в настройке.

Система ожидает, не более чем один инлайн-конфиг на одну TODO. 

Примеры настроек, поддерживаемых реализованным JIRA-регистратором.

Нужно указать релейтед-тикет? Легко. 

{EXTRAS: {linkedIssues: XX-123}}

Нужно привязать несколько тикетов? Так:

{EXTRAS: {linkedIssues: [XX-123, XX-234]}}

Нужно привязать тикеты с разным типом привязки? Легко. 

{EXTRAS: {linkedIssues: {child_of: XX-123, is_blocked_by: [XX-234, YY-567]}}}

Аналогично указываются лейбы и компоненты. 

{EXTRAS: {labels: [label-a, label-b], components: [component-a, component-b]}}

Как видно из примеров, в поля предполагающие наличие нескольких значений можно передавать как массив значений, так и одну строку. 

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

Порядок применения конфигов

  1. @assignee в привязки к тегу. 

  2. EXTRAS 

  3. Затем общий конфиг JIRA-регистратора. 

Настраиваем джобу на CI

Это очень важный пункт. От правильности его настройки зависит будут ли сохранены в коде ИД, созданных тикетов. И не будет ли дублирующихся тикетов. 

Схема

Во первых, выбираем одну бранчу в которой будем контролировать появление в коде новых TODO. Это должна быть достаточно стабильная бранча в которой не ведётся разработка. При этом она должна быть как можно ближе к разработке, чтобы тех. долг попадал в учёт как можно раньше. Как правило, это “develop”. Не стоит использовать релизные бранчи. Параллельно может существовать несколько релизных бранчей и тикеты могут мигрировать из релиза в релиз по требованию бизнеса. Что может привести к повторному созданию тикетов под разными номерами и конфликтам мерджа, связанным с этим. 

Во вторых, после запуска скрипта при наличии изменений в коде проекта следует создать новую бранчу по шаблону. Например, “todo-registrar” или “todo-registrar-<random_suffix>”. И закомитить изменения туда. Это важно при повторном запуске. См. ниже. 

В третьих, создать мердж-реквест из новой бранчи в контролируемую. 

Повторный запуск

Предыдущие пункты описывают создание коммитов и мердж-реквестов, но нужно сделать кое-что ещё для того, что избежать создания дублей тикетов. Перед запуском основного скрипта важно проверить наличие хотя бы одного открытого мердж-реквеста в контролируемую ветку из ветки, созданной по выбранному шаблону. При его наличии следует пропускать этап поиска новых TODO и их регистрации. 

Не следует переживать, что что-то “не долетело” из-за открытого мердж-реквеста. Его мердж вызовет повторную проверку бранчи. 

Пример для GitLab

Система обкатывалась на Gitlab. Для Github делается аналогично. Главное, следовать схеме приведённой выше. 

Настраиваем джобу в .gitlab-ci.yml
'TODO registrar':
    #...
    allow_failure: true
    script:
        - |
            export TR_NEW_BRANCH=todo-registrar-$(echo "$RANDOM$RANDOM$RANDOM" | base64 | head -c 8; echo)
            export TR_MR_EXISTS=$(bash scripts/gitlab/mr_check_existing.sh $MR_TARGET_BRANCH "todo-registrar-[[:alnum:]]{8}")
            if [[ "0" == "$TR_MR_EXISTS" ]]; then
                echo "NO opened MR exists! Looking for new TODOs..."
                vendor/bin/todo-registrar
                export TR_TITLE="TODO-REGISTRAR: automated registering of new TODOs "
                export TR_PUSHED=$(bash scripts/gitlab/commit_and_push.sh $TR_NEW_BRANCH "$TR_TITLE")
                if [[ "1" == "$TR_PUSHED" ]]; then
                    bash scripts/gitlab/mr_create.sh $MR_TARGET_BRANCH $TR_NEW_BRANCH "$TR_TITLE"
                else
                    echo "No changes, nothing to commit & push!"
                fi
            else
                echo "Registering of TODOs is skip case opened MR exists!"
            fi
    only:
        - develop

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

Скрипт проверки существования открытого мерджреквеста

Скрипт рассчитан на то, что исходная бранча может иметь разные названия и поддерживает проверку имени бранчи по паттерну.

Кладём его в файл scripts/gitlab/mr_check_existing.sh

#!/bin/bash

if [ $# -ne 3 ]; then
  echo "Usage: $0 <MR_PRIVATE_TOKEN> <MR_TARGET_BRANCH> <MR_SOURCE_BRANCH>"
  exit 1
fi

PRIVATE_TOKEN=$1
PROJECT_ID=${CI_PROJECT_ID}
SOURCE_BRANCH=$3
TARGET_BRANCH=$2
GITLAB_API_URL="$CI_SERVER_URL/api/v4"
MR_EXIST=0
REGEX="\"source_branch\":\s*\"$SOURCE_BRANCH\""

# shellcheck disable=SC2034
for page in {1..15}
do
  MR_LIST=$(curl -sS --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \
    "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests?target_branch=$TARGET_BRANCH&state=opened&page=$page")

  if [ "$MR_LIST" == "[]" ]; then
    break
  fi
  if [[ "$MR_LIST" =~ $REGEX ]]; then
    MR_EXIST=1
    break
  fi
done

echo $MR_EXIST

Скрип ответственный за коммит и пуш

Клядём его в файл scripts/gitlab/commit_and_push.sh

#!/bin/bash

if [ $# -ne 2 ]; then
  echo "Usage: $0 <MR_NEW_BRANCH> <MR_TITLE>"
  exit 1
fi

NEW_BRANCH=$1
TITLE=$2

git config --global user.email "cibot@example.com"
git config --global user.name "CI Bot"
git config remote.origin_ci.url >&- || git remote add origin_ci https://oauth2:${MR_PRIVATE_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git
git checkout -b $NEW_BRANCH 1> /dev/null

git add .
TR_NOTHING_TO_COMMIT=$(git commit -m "$TITLE" | grep "nothing to commit")
if [[ -z  $TR_NOTHING_TO_COMMIT ]]; then
    git push origin_ci $NEW_BRANCH 1> /dev/null
    echo 1
else
    git checkout -b $CI_COMMIT_REF_SLUG 1> /dev/null
    git branch -D $NEW_BRANCH 1> /dev/null
    echo 0
fi

Скрипт создания мерджреквеста

Кладём его в файл scripts/gitlab/mr_create.sh

#!/bin/bash

if [ $# -ne 4 ]; then
  echo "Usage: $0 <MR_PRIVATE_TOKEN> <MR_TARGET_BRANCH> <MR_SOURCE_BRANCH> <MR_TITLE>"
  exit 1
fi

GITLAB_API_URL="$CI_SERVER_URL/api/v4"
PRIVATE_TOKEN=$1
PROJECT_ID=${CI_PROJECT_ID}
SOURCE_BRANCH=$3
TARGET_BRANCH=$2
TITLE=$4

curl -sS --request POST --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \
  "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests" \
  --form "source_branch=$SOURCE_BRANCH" \
  --form "target_branch=$TARGET_BRANCH" \
  --form "title=$TITLE" \
  --form "description=Automated merge request from CI/CD pipeline"

Контроль за удалением неактуальных TODO

План таков:

  1. Ставим пакет staabm/phpstan-todo-by

  2. Пакет предоставляет правило для PHPStan. Если не установлен, то устанавливаем. 

  3. Добавляем правило из этого пакета в конфиг PHPStan.

  4. Конфигурируем подключение к ишью-трекеру (JIRA) в конфиге PHPStan. 

  5. Настраиваем джобу на Gitlab CI. 

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

Эта джоба может выполняться на любой бранче. И ей следует позволить завершаться с ошибкой. Как правило, тикет, по которому написан код, завершается после прохождения ревью и тестирования, и на этом этапе открытого мердж-реквеста скрипт не может проконтролировать закрытие тикета, т.к. он ещё как правило открыт. 

Ограничение выбранного пакета:

В текущей реализации этот пакет контролирует только 3 тега: TODO, FIXME, XXX. Поэтому, в качестве кастомного тега следует использовать тег XXX. Либо после регистрации тикетов менять кастомные теги на один из поддерживаемых этим компонентом. Возможно, скриптом. Либо законтрибьютить возможность конфигурации отслеживаемых тегов. Либо использовать другое решение. К счастью, здесь есть альтернативы. 

Сценарии использования

Внедрение в старый проект

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

Оцените сколько TODO/FIXME у вас в проекте сейчас. На сколько больно будет стартовать на дефолтных настройках. Возможно, в процессе этого анализа вы удалите часть устаревших комментариев. Если с тем, что осталось всё равно больно стартовать, то есть несколько сценариев:

  1. Добавление суффикса к существующим тегам. Чтобы превратить TODO в TODO-OLD, или что-то подобное. И сразу настроить отслеживание распространённых тегов. Позже можно удалить этот суффикс у части комментариев, чтобы они были обработаны скриптом. Возможно, в несколько подходов. 

  2. Настроить отслеживание только одного тега. 

  3. Настроить отслеживание кастомного тега. Например, TODO-JIRA. И объяснить всем разработчикам какой тег использовать. В любой IDE тоже можно настроить подсветку кастомного тега. Ограничения: Могут быть проблемы с doctrine/annotation если перед тегом стоит знак @. Может потребоваться настройка игнорирование кастомных тегов доктриной. 

  4. Настроить мониторинг только части проекта. 

Я рекомендую первый и второй сценарии. 

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

Стало гораздо веселее проводить ревью проектов.

  • Поставил пакет. 

  • Настроил. 

  • Сидишь читаешь код и бомбишь тудушки, используя любой удобный тег. 

Не нужно переключаться между окнами и заполнять в ишью-трекере 100500 полей по каждому замечанию. Всё само залетит в JIRA. Дальше по номеру тикета будет легко найти место, требующие внимание. 

Упростилась работа разработчиков

Теперь, столкнувшись с необходимостью изменить что-то, разработчик не мучается с вопросом “можно ли исправить это в рамках моей задачи? И как?” У некоторых на это уходит колоссальное количество времени. Теперь, поставил TODO и пошёл дальше. И можно быть уверенным, что об этой тудушке никто не забудет. Т.е. с девелопера не снимается ответственность за качество кода полностью, но снижается давление. Потому тикеты реализуются быстрее. 

Отслеживание тех. долга

Теперь не важно по какой причине разработчик оставляет тех. долг. Будь то бизнес попросил ускориться или по собственному желанию. Просто оставляешь комментарии в коде с указанием ссылки на текущую задачу в инлайн-конфиге и в джиру залетают связанные тикеты по каждому пункту. 

Пост скриптум

Очень интересуют подобные решения для отслеживания и регистрации TODO в файлах: yaml, twig, html, xml, javascript, css, less, flatter. Буду благодарен за ссылки на них.

Свежие идеи, замечания и контрибьюторы в проект TODO Registrar также приветствуются.

UPD:

Обновил примеры скриптов и включил подсветку кода

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