Где вести задачи по 1С-проекту? Вопрос, на который у каждой команды свой ответ -- и обычно этот ответ звучит как "в Excel" или "в чате". Jira слишком тяжела для малых команд. Redmine требует отдельного сервера и внимания. Trello хорош для канбана, но не для кода. А что если трекер уже есть -- там же, где лежат исходники?
Я расскажу, как настроил двустороннюю синхронизацию между GitHub Issues и документами "Задача" в 1С. Задача создаётся в GitHub -- автоматически появляется в 1С. Статус меняется в 1С -- label обновляется в GitHub. Без ручного копирования, без "а ты завёл задачу в систему?".
Зачем 1С-нику GitHub
Справедливый вопрос. Мир 1С исторически живёт в своей экосистеме: хранилище конфигураций, планы обмена, регламентные задания. Git появился в этом мире относительно недавно -- вместе с EDT и форматом выгрузки конфигурации в XML. И до сих пор воспринимается многими как чужеродный элемент.
Но если исходники уже в Git (а в моих проектах это стандарт), то GitHub Issues -- бесплатный и органичный трекер. Он привязан к репозиторию. Задачу можно упомянуть в коммите через #номер, и она автоматически свяжется с изменениями. Pull request закрывает issue. История задачи и история кода -- в одном месте.
Jira умеет то же самое, но стоит денег и требует настройки интеграции с Git. Redmine -- бесплатный, но это ещё один сервер, ещё одна система, ещё один пароль. YouTrack -- отличный, но для команды из 3-5 человек избыточен.
GitHub Issues -- это "уже здесь". Не нужно ничего устанавливать, не нужно ничего настраивать. Создал issue -- описал задачу. Назначил label -- обозначил статус. Привязал к milestone -- определил спринт. Для 1С-проекта этого хватает.
Но есть проблема: 1С ничего не знает о GitHub. Менеджер проекта заводит задачу в GitHub, а разработчик 1С работает в Конфигураторе (или EDT) и видит задачи только в 1С. Руководитель хочет видеть статусы в GitHub, а исполнитель обновляет их в 1С. Два мира -- между ними пропасть. Мост нужно строить.
Webhook -- HTTP-сервис -- документ "Задача"
Архитектура интеграции состоит из трёх звеньев. GitHub отправляет webhook при событиях с issue (создание, изменение, комментарий). 1С принимает webhook через HTTP-сервис. Обработчик HTTP-сервиса создаёт или обновляет документ "Задача".
Webhook настраивается в Settings репозитория. Payload URL -- адрес HTTP-сервиса 1С, Content type -- application/json, события -- Issues. GitHub будет отправлять POST-запрос при каждом действии с issue: opened, edited, labeled, unlabeled, closed, reopened.
// HTTP-сервис: обработка входящего webhook
Функция ОбработатьWebhook(Запрос)
Тело = Запрос.ПолучитьТелоКакСтроку();
Данные = ПарсерJSON.Прочитать(Тело);
Действие = Данные["action"];
Issue = Данные["issue"];
Если Действие = "opened" Тогда
СоздатьЗадачу(Issue);
ИначеЕсли Действие = "edited" Тогда
ОбновитьЗадачу(Issue);
ИначеЕсли Действие = "labeled"
ИЛИ Действие = "unlabeled" Тогда
СинхронизироватьСтатус(Issue);
КонецЕсли;
Ответ = Новый HTTPСервисОтвет(200);
Возврат Ответ;
КонецФункции
При создании issue (action: opened) HTTP-сервис разбирает JSON и заполняет реквизиты документа "Задача": заголовок из issue.title, описание из issue.body, номер issue, URL, метки. Назначает исполнителя по маппингу (об этом ниже). Записывает документ.
Важный момент -- аутентификация. GitHub позволяет подписывать webhook секретным ключом. В заголовке X-Hub-Signature-256 приходит HMAC-SHA256 от тела запроса. HTTP-сервис на стороне 1С должен проверить подпись, прежде чем обрабатывать данные. Без этого любой, кто узнает URL, может отправить поддельный запрос и создать мусорные задачи в базе.
// Проверка подписи webhook
ПодписьЗаголовок = Запрос.Заголовки["X-Hub-Signature-256"];
ТелоДвоичные = Запрос.ПолучитьТелоКакДвоичныеДанные();
HMAC = Новый ХешированиеДанных(ХешФункция.SHA256, СекретныйКлюч);
HMAC.Добавить(ТелоДвоичные);
ОжидаемаяПодпись = "sha256=" + НРег(HMAC.ХешСумма);
Если ПодписьЗаголовок <> ОжидаемаяПодпись Тогда
Возврат Новый HTTPСервисОтвет(403);
КонецЕсли;
Ещё один нюанс: idempotency. GitHub может отправить один и тот же webhook дважды (при таймауте, при ретрае). Обработчик должен быть идемпотентным: если документ с таким номером issue уже существует -- обновить, а не создавать дубль. Поиск по реквизиту НомерIssue перед записью решает проблему.
Это, кстати, тот же принцип, что и с транзакциями в 1С: операция должна быть атомарной и безопасной при повторном выполнении. Если webhook пришёл дважды -- результат должен быть тот же, что и при однократной обработке.
Маппинг пользователей: когда GitHubLogin пустой
В идеальном мире у каждого пользователя 1С заполнен реквизит GitHubLogin. Webhook приходит с полем issue.assignee.login, мы ищем пользователя по логину -- и готово. В реальном мире этот реквизит заполнен у двух человек из восьми.
Причина прозаическая: реквизит добавили позже, чем завели пользователей. Кто-то заполнил при случае, кто-то -- нет. Заставлять всех заполнять прямо сейчас -- потеря времени и гарантированное сопротивление. Нужен fallback.
Решение: трёхуровневый маппинг.
Функция НайтиПользователяПоGitHub(GitHubLogin)
// Уровень 1: точное совпадение по реквизиту GitHubLogin
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Пользователи
|ГДЕ GitHubLogin = &Login";
Запрос.УстановитьПараметр("Login", GitHubLogin);
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
Возврат Результат.Выгрузить()[0].Ссылка;
КонецЕсли;
// Уровень 2: поиск по ФИО из профиля GitHub
ПрофильGitHub = ПолучитьПрофильGitHub(GitHubLogin);
ФИОИзGitHub = ПрофильGitHub["name"]; // "Иванов Пётр"
Если ЗначениеЗаполнено(ФИОИзGitHub) Тогда
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Пользователи
|ГДЕ Наименование ПОДОБНО &ФИО";
Запрос.УстановитьПараметр("ФИО", "%" + ФИОИзGitHub + "%");
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
// Нашли -- заодно запишем GitHubLogin, чтобы
// следующий раз сработал уровень 1
Пользователь = Результат.Выгрузить()[0].Ссылка;
ОбъектПользователя = Пользователь.ПолучитьОбъект();
ОбъектПользователя.GitHubLogin = GitHubLogin;
ОбъектПользователя.Записать();
Возврат Пользователь;
КонецЕсли;
КонецЕсли;
// Уровень 3: таблица явного маппинга
Возврат СоответствиеМаппинга[GitHubLogin];
КонецФункции
Первый уровень -- прямое совпадение по GitHubLogin. Быстро, надёжно, однозначно.
Второй уровень -- fallback по ФИО. GitHub API позволяет получить профиль пользователя, в котором есть поле name. Если Пётр Иванов на GitHub подписан как "Иванов Пётр", а в 1С есть пользователь "Иванов Пётр Сергеевич" -- поиск через ПОДОБНО найдёт совпадение. И -- ключевой момент -- запишет GitHubLogin в карточку пользователя. Самозаполняющийся справочник. Через неделю fallback становится не нужен -- все логины заполнены автоматически.
Третий уровень -- жёстко прописанная таблица соответствий. Для случаев, когда ФИО в GitHub не совпадает с ФИО в 1С (ник вместо имени, латиница, сокращения). Этих случаев обычно один-два на проект.
Элегантно? Не особо. Работает? Безотказно. Через две недели после запуска все восемь пользователей были смаплены автоматически. Третий уровень понадобился ровно одному человеку, который на GitHub подписан как "DarkLord42".
Синхронизация статусов: labels и статусы
Статусы задач в 1С -- это перечисление: "Новая", "В работе", "На внутреннем тестировании", "На тестировании у клиента", "Выполнена", "Отменена". В GitHub статусов как таковых нет -- есть labels (метки). Каждый label -- произвольная строка с цветом.
Создал в репозитории набор labels, соответствующих статусам в 1С:
GitHub Label → Статус в 1С
─────────────────────────────────────────
To Do → Новая
In Progress → В работе
To analyst → На внутреннем тестировании
To customer → На тестировании у клиента
Done → Выполнена
Won't fix → Отменена
Когда менеджер навешивает label "In Progress" в GitHub, webhook приходит с action: labeled. Обработчик находит документ "Задача" по номеру issue, определяет соответствующий статус по таблице маппинга и обновляет реквизит.
Обратная синхронизация -- из 1С в GitHub -- работает через GitHub API. При смене статуса документа "Задача" подписка на событие вызывает процедуру, которая:
- Определяет новый label по обратной таблице маппинга
- Снимает все статусные labels с issue через
DELETE /repos/:owner/:repo/issues/:number/labels/:name - Навешивает новый label через
POST /repos/:owner/:repo/issues/:number/labels
// Подписка на событие ПриЗаписи документа "Задача"
Процедура ОбработкаЗаписиЗадачи(Источник, Отказ)
Если Источник.Статус = Источник.Ссылка.Статус Тогда
Возврат; // Статус не менялся
КонецЕсли;
НовыйLabel = ТаблицаМаппинга[Источник.Статус];
Если НовыйLabel = Неопределено Тогда
Возврат; // Нет маппинга для этого статуса
КонецЕсли;
// Убираем старые статусные labels
Для Каждого СтатусныйLabel Из СтатусныеLabels Цикл
GitHubAPI.УдалитьLabel(Источник.НомерIssue, СтатусныйLabel);
КонецЦикла;
// Ставим новый
GitHubAPI.ДобавитьLabel(Источник.НомерIssue, НовыйLabel);
КонецПроцедуры
Важный нюанс: защита от бесконечного цикла. Когда 1С обновляет label в GitHub, GitHub отправляет webhook labeled обратно в 1С. 1С получает webhook, находит задачу, видит что статус совпадает с текущим -- и ничего не делает. Но если не предусмотреть эту проверку, можно получить бесконечный пинг-понг: 1С меняет label, GitHub шлёт webhook, 1С обновляет статус, срабатывает подписка, 1С меняет label... Бесконечный цикл, пока не кончатся лимиты GitHub API.
Защита двойная: проверка "статус не менялся" в подписке и проверка источника изменения. Если изменение пришло из webhook (а не из интерфейса 1С) -- обратную синхронизацию не запускаем. Для этого в модуле HTTP-сервиса перед записью документа устанавливаю флаг ДополнительныеСвойства.ИсточникGitHub = Истина. Подписка проверяет этот флаг.
Что дало на практике
Систему запустили три месяца назад. За это время через неё прошло около 180 задач. Вот что изменилось.
Первое -- прозрачность. Менеджер проекта видит все задачи в GitHub, не заходя в 1С. Фильтрация по labels, milestone, assignee -- всё штатными средствами. Не нужно просить доступ к базе или ждать выгрузку отчёта.
Второе -- трассируемость. Каждый коммит ссылается на issue через #номер. Можно открыть задачу и увидеть все коммиты, которые к ней относятся. Можно открыть коммит и понять, зачем он сделан. Это бесценно при разборе инцидентов: "почему эта функция работает именно так?" -- смотришь issue, читаешь обсуждение, понимаешь контекст. Именно такую трассируемость даёт полноценный CI/CD пайплайн: от задачи до деплоя -- одна цепочка.
Третье -- скорость. Раньше цикл был таким: менеджер пишет в чат, разработчик переносит в свой список, статус обновляется устно на созвоне. Сейчас: менеджер создаёт issue, разработчик берёт в работу (label "In Progress"), менеджер видит обновление мгновенно. Без созвонов, без вопросов "а что со второй задачей?".
Четвёртое -- данные для анализа. GitHub API позволяет выгрузить все issues с датами создания, закрытия, сменами labels. Lead time (от создания до закрытия), cycle time (от "In Progress" до "Done"), количество задач в работе одновременно -- всё считается автоматически. Не нужен отдельный BI-инструмент.
Есть и ограничения, о которых честно. GitHub Issues -- не Jira. Нет иерархии задач (epic → story → subtask). Нет встроенной оценки трудозатрат. Нет досок в стиле Kanban из коробки (Projects есть, но это отдельная сущность). Для проекта с 3-8 людьми и сотней задач в квартал -- хватает с запасом. Для enterprise-команды из 30 человек -- скорее всего нет.
Отдельная боль -- GitHub API rate limits. 5000 запросов в час для authenticated пользователя. Звучит много, но при активной обратной синхронизации (каждая смена статуса = 2-3 API-вызова) можно упереться. На практике не упёрся ни разу, но лимит держу в голове.
Ещё нюанс: формат описания. В GitHub Issues используется Markdown. В 1С -- форматированная строка или HTML. Конвертация из Markdown в формат 1С -- отдельная история. Я пошёл простым путём: сохраняю body как есть в реквизит типа "Строка (неограниченная)", а для отображения в форме 1С использую поле HTML-документа. Не идеально, но читаемо.
Стоил ли весь этот проект затраченных усилий? Однозначно. Три дня на разработку и тестирование. Экономия -- минут двадцать в день на ручную синхронизацию задач и статусов. Но главная ценность не в минутах, а в том, что задачи перестали теряться. Раньше между "написал в чат" и "появилось в 1С" -- пропасть. Сейчас -- webhook.


