Привет, Хабр! На связи Кирилл Веркин. Вообще, я занимаю в СберМаркете должность Senior QA, но ради большей производительности команды жизнь заставила стать немного кодером.
Эта статья может быть интересна тем, кто замечает, что задачи в команде часто теряются, и хочет автоматизировать процесс напоминалок. Я делюсь кодом, поясняя ключевые моменты для таких же новичков в Go. Мой код написан для сочетания GitLab, Jira и Mattermost (корпоративный мессенджер, которым мы пользуемся в СберМаркете), но подобное решение можно реализовать и с другими сервисами.

Зачем нужен бот
Кросс-функциональная команда, к которой я отношусь, состоит из фронтенд-разработчика, мобильного разработчика и трех бэкендеров (с учетом тимлида). Фронтенд и мобильный код уходят на ревью за пределы команды, а вот бэкендеры сами проверяют друг друга, то есть ответственность за поставку бэкенда на тестирование лежит на них. С этим и была связана проблема: мои коллеги забывали проверять задачи на ревью, оно занимало по несколько дней и, соответственно, снижало скорость выхода на продакшен.
Мы видели проседания на графиках, тимлид и проджект выносили вопрос на ретроспективу. Изначально договорились, что каждый из бэкендеров по утрам должен смотреть мердж-реквесты. К сожалению, это не дало плодов, задачи на ревью продолжили ускользать от внимания. Тогда я решил пинговать ответственных в рабочем мессенджере. В мое отсутствие этим занимался тимлид. Проджект-менеджер заметила, что мы тратим время на ручные напоминания, и предложила автоматизировать процесс.
Если на этом моменте вам показалось, что это точно не входит в работу тестировщика, то доля правды в этом есть 🙂 Но такой я человек, с активной жизненной позицией. Я уже писал на Хабре о том, как повысил инженерную культуру своей команды по модели ТММ, не обладая «властью» тимлида. Прочитать можно вот тут.
Первая версия бота
После мини-исследования проджект выяснила, что мы не единственные, кто столкнулся с такой проблемой, и что внутри компании уже есть код для подобных уведомлений. Я подумал, что воспользоваться работающим ботом не так трудно, Ctrl+C — Ctrl+V, и взял на себя задачу по его внедрению в канал команды на платформе Mattermost.
Изначальный код был написан на Ruby и запускался через Schedules в GitLab. Раз в сутки, по будням, соответствующая Job включалась и присылала в канал табличку с мердж-реквестами. Пара правок — и бот начал работать в канале нашей команды. Я, наивный, почувствовал в себе прогерскую мощь 🙂
На этом текст мог бы завершиться, но…
Довольно быстро стало понятно, что эти уведомления нам не подходят: бот пинговал не тех, кто должен был провести ревью, а авторов мердж-реквестов. Им же требовалось снова пинговать ревьюеров. Сами ревьюеры не смотрели таблицу регулярно. То есть ситуация не изменилась, только добавилось звено в виде бота.
Так в игру вступил новый бот.
Рождение нового бота
Чтобы бот соответствовал потребностям нашей команды, он должен был, как и первый, отображать ссылки на мердж-реквесты, но при этом пинговать не авторов, а ревьюеров. Кроме того, я хотел, чтобы уведомления не приходили не только в выходные, но и по праздникам.
Также бот должен был быть связан со статусами в Jira:
-
Code Review — присылает автору уведомление, что можно переводить задачу в тестирование.
-
Ready for Test — присылает уведомление QA, то есть мне, что можно начинать тестирование.
-
Tested — присылает автору уведомление, что код протестирован и его можно деплоить.
-
Needs Refinement — присылает автору уведомление, что есть баги и требуется доработка.
-
Ready for Deploy — присылает автору уведомление, что задача ждет выхода на продакшен. Это полезно, когда релиз-инженеру нужно дополнительное напоминание от автора, чтобы забрать задачу, или когда разработчики забывают проставить нужные лейблы для выкатки в GitLab.
Вместо того чтобы править готовый код, было решено писать новый. С нашей стороны было бы неэтично прийти к код-овнерам и сказать, что нам не нравится, какой функционал они обеспечили, и что мы ждем апрув для своих правок. Со своим уставом в чужой монастырь не ходят 🙂
Вместо Ruby выбрали Golang: это основной язык, который используется в нашей команде. Так мы можем рефакторить бот быстро и без привлечения сторонних сотрудников. Кроме того, тимлид сказал, что если я выучу Go, то в случае глобальной загрузки подхвачу задачи коллег.
В СберМаркете работает менторская система. В ней участвует несколько десятков сотрудников из разных направлений. Каждому присвоен профиль в корпоративной вики — с перечислением скиллов и вопросов, с которыми можно обратиться. Я попросил разработчика из своей команды взять меня в менти, вот тут Лешин профиль на Хабре.
А дальше закрутилось: кипа внутренних материалов, внутренний курс по Go, много парного программирования и менторского терпения. Прошел я и общедоступный курс A Tour of Go: он бесплатный, занимает примерно пять часов. Мне кажется, это хороший способ получить представление о языке.
Непосредственно код
Вот что у нас получилось:
package main import ( "encoding/json" "fmt" "notification-bot/utils" "os" "regexp" ) const ( statusCodeReview = "5" statusNeedsRefinement = "6" statusReadyForTest = "7" statusTested = "8" statusReadyForDeploy = "9" statusTesting = "10" ) var Authors = []string{ "vasya.ytkin", "vasya.pupkin", "vasya.ymkin", } var HeadersGitLab = map[string]string{"PRIVATE-TOKEN": os.Getenv("GITLAB_TOKEN")} var HeadersJira = map[string]string{"Authorization": os.Getenv("JIRA_TOKEN")} var GitlabProjectsUrl = os.Getenv("GITLAB_PROJECTS_URL")} var GitlabMergeRequestsUrl = os.Getenv("GITLAB_MERGE_REQUESTS_URL")} var JiraProjectUrl = os.Getenv("JIRA_PROJECT_URL")} var JiraTaskUrl = os.Getenv("JIRA_TASK_URL")} func main() { table := "| Автор | МР | Ревьюеры |\n|:-------------|:---------------:|:---------------:|\n" var showTable bool var mr string taskIdTemplate := regexp.MustCompile(`[A-Z]+-[0-9]+`) taskList, err := getTaskList() if err != nil { panic(fmt.Sprintf("getTaskList: %s", err.Error())) } // Цикл перебирает авторов/пользователей for i := 0; i < len(Authors); i++ { // GetRequest получает данные о МР пользователя text, err := utils.GetRequest(GitlabMergeRequestsUrl+Authors[i]+ "&scope=all&state=opened&page=1&per_page=50&wip=no", HeadersGitLab) if err != nil { panic(fmt.Sprintf("getRequest: %s", err.Error())) } //Список МР пользователя var data []map[string]any // Мапим json в структуру err = json.Unmarshal(text, &data) if err != nil { panic(fmt.Sprintf("json.Unmarshal: %s", err.Error())) } //Если есть МР if len(data) != 0 { // Буфер для хранения МР var usersMrs string //Перебирает МР for j := 0; j < len(data); j++ { //Получаем список тех, кто должен поставить апрув approvers, err := getApprovers(data[j]["project_id"].(float64), data[j]["iid"].(float64)) if err != nil { panic(fmt.Sprintf("getApprovers: %s", err.Error())) } //Список тех, кто еще не поставил апрув reviewers := getReviewers(Authors[i], approvers) //Если все поставили апрув if reviewers == "" { title := data[j]["title"].(string) //Получает ID задачи из названия МР taskId := taskIdTemplate.FindString(title) //Если мы не нашли ID if taskId == "" { continue } status := taskList[taskId] //Определяем строку для вывода в зависимости от статуса задачи switch status { case statusCodeReview: reviewers = fmt.Sprintf("@%s переведи в тестирование задачу "+ "[%s](JiraTaskUrl%s)", Authors[i], taskId, taskId) case statusNeedsRefinement: reviewers = fmt.Sprintf("@%s нужны уточнения по задаче "+ "[%s](JiraTaskUrl%s)", Authors[i], taskId, taskId) case statusTested: reviewers = fmt.Sprintf("@%s протестирована задача, можно катить "+ "[%s](JiraTaskUrl%s)", Authors[i], taskId, taskId) case statusReadyForDeploy: reviewers = fmt.Sprintf("@%s готова к выкатке задача "+ "[%s](JiraTaskUrl%s)", Authors[i], taskId, taskId) case statusReadyForTest: reviewers = fmt.Sprintf("@kirill.verkin можно тестировать задачу "+ "[%s](JiraTaskUrl%s)", taskId, taskId) case statusTesting: continue default: reviewers = fmt.Sprintf("@%s проверь статус задачи "+ "[%s](JiraTaskUrl%s)", Authors[i], taskId, taskId) } } //Запоминаем МР mr = fmt.Sprintf("[№-%s](%s)", (data[j]["reference"]).(string), data[j]["web_url"]) usersMrs += fmt.Sprintf("| | %s | %s |\n", mr, reviewers) } // Добавляем в таблицу автора и МР, если есть МР if usersMrs != "" { //Флаг для отображения таблицы showTable = true table += fmt.Sprintf("| %s |\n", Authors[i]) table += usersMrs } } } // Определяем, есть ли данные в таблице if !showTable { table = "#### Если появятся МР в течение дня, напиши в тред" } // Выводим, если рабочий день if utils.IsWorkingDay() { err := utils.Send(table, "MR(ы) команды", "gull_scream") if err != nil { panic(fmt.Sprintf("Send: %s", err.Error())) } } }
А теперь по частям!
Для лучшего восприятия кода мы разделили его по функциям. В функции getApprovers() происходит перебор тех, кто должен поставить апрув:
func getApprovers(projectId, iid float64) ([]string, error) { var result []string text, err := utils.GetRequest(fmt.Sprintf(GitlabProjectsUrl, int32(projectId), int32(iid)), HeadersGitLab) if err != nil { return nil, fmt.Errorf("getRequest: %w", err) } var data map[string]any err = json.Unmarshal(text, &data) if err != nil { return nil, fmt.Errorf("json.Unmarshal: %w", err) } rules := data["rules"].([]interface{}) for i := 0; i < len(rules); i++ { rule := rules[i].(map[string]any) approvers := rule["approved_by"].([]interface{}) for j := 0; j < len(approvers); j++ { approver := approvers[j].(map[string]any) result = append(result, approver["username"].(string)) } } return result, nil }
В функции getReviewers() перебирает авторов, которые не поставили апрув в мердж-реквесте:
func getReviewers(author string, approvers []string) string { var reviewers string Peoples: for i := 0; i < len(Authors); i++ { if author == Authors[i] { continue } for j := 0; j < len(approvers); j++ { if Authors[i] == approvers[j] { continue Peoples } } reviewers += fmt.Sprintf("@%s ", Authors[i]) } return reviewers }
В функции getTaskList() происходит перебор задач в проекте:
func getTaskList() (map[string]string, error) { tasks := make(map[string]string) jiraReq, err := utils.GetRequest(JiraProjectUrl, HeadersJira) if err != nil { return nil, fmt.Errorf("getRequest: %w", err) } var jiraData map[string]any err = json.Unmarshal(jiraReq, &jiraData) if err != nil { return nil, fmt.Errorf("json.Unmarshal: %w", err) } issuesData := jiraData["issuesData"].(map[string]any) issues := issuesData["issues"].([]interface{}) for i := 0; i < len(issues); i++ { item := issues[i].(map[string]any) key := item["key"].(string) statusId := item["statusId"].(string) tasks[key] = statusId } return tasks, nil }
Участки кода залогированы, чтобы было понятно, на что смотреть, если возникнет ошибка. Поэтому во всех вызываемых функциях возвращаем ошибки. Например, в функции getApprovers() это выглядит так:
var data map[string]any err = json.Unmarshal(text, &data) if err != nil { return nil, fmt.Errorf("json.Unmarshal: %w", err) }
Вот так в главную функцию main зашита паника (непредвиденная ошибка, которая приводит прекращению работы и закрытию Go-программы):
taskList, err := getTaskList() if err != nil { panic(fmt.Sprintf("getTaskList: %s", err.Error())) }
Для проверки производственного календаря мы подключили внешнюю библиотеку, которая доступна по ссылке. Она есть и для других языков. Я подумал, что библиотека может вернуть непредвиденный результат, поэтому мы реализовали соответствующую проверку:
func IsWorkingDay() bool { countryCode := isdayoff.CountryCodeRussia day, err := isdayoff.New().Today(isdayoff.Params{ CountryCode: &countryCode, }) if err != nil { fmt.Printf("IsWorkingDay: %s", err.Error()) return true } return *day != isdayoff.DayTypeNonWorking }
Общие куски кода вынесли в отдельный пакет, так как в корневом пакете у нас лежат боты и утилиты там мешались.
package utils import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "github.com/anatoliyfedorenko/isdayoff" )
В функции Send() происходит отправка сообщения с атрибутами (заголовок, описание, эмодзи) в канал Mattermost:
func Send(text, username, emoji string) error { message := map[string]string{ "text": text, "username": username, "icon_emoji": emoji, } data, err := json.Marshal(message) if err != nil { return fmt.Errorf("json.Marshal: %w", err) } r := bytes.NewReader(data) _, err = http.Post(os.Getenv("MATTERMOST_HOOK_URL"), "application/json", r) if err != nil { return fmt.Errorf("http.Post: %w", err) } return nil }
В функции GetRequest() получаем данные по атрибуту URL-назначения:
func GetRequest(url string, headers map[string]string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("getRequest: %w", err) } for k, v := range headers { req.Header.Set(k, v) } client := http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("client.Do: %w", err) } text, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("ReadAll: %w", err) } return text, nil }
От Schedules мы отказываться не стали. Бот так же раз в сутки присылает уведомление в канал, но теперь делает запрос в производственный календарь для проверки нерабочих дней.
Вот так сейчас выглядит уведомление с мердж-реквестами:

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

Что было дальше
Команде очень понравился результат. Сравните, как выглядели метрики до внедрения бота и как они выглядят сейчас:


Среднее время нахождения задачи на ревью сократилось с 24 рабочих часов до восьми. Максимальное время раньше было больше недели, а сейчас это три с половиной дня.
Проект прокачал Лешу как ментора, а мне дал ощутимый буст как начинающему кодеру и помог в основной занятости: из тестировщика, который спрашивал, на что могут повлиять правки в коде, стал тем, кто сам может посмотреть код, заметить задетые участки и направить на них тест.
Одна команда из СберМаркета уже забрала наш код на переиспользование. Надеюсь, он будет полезен и кому-то из читателей здесь 🙂
Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.
ссылка на оригинал статьи https://habr.com/ru/articles/819067/
Добавить комментарий