Разработчик — натура творческая. У него нет времени на рутинные задачи, о которых может позаботиться машина. Поэтому все, что можно автоматизировать, должно быть автоматизировано.
Привет! Меня зовут Никита. Я разработчик Taiga UI, библиотеки Angular-компонентов, которая активно используется в нашей компании «Тинькофф». Я расскажу про решение одной из таких рутинных задач на нашем проекте с помощью написания с нуля своего Github App на Node.js.
Постановка проблемы
На проекте мы активно пишем скриншотные тесты с использованием фреймворка Cypress.
После внесения правок в код и открытия Pull Request в CI начинается Github Workflow на запуск всех тестов, которые и спасают наш будущий релиз от внесения багов в компоненты UI Kit. Как только какой-либо тест падает, все скриншоты прикрепляются архивом как артефакты к данному workflow, которые разработчик может скачать и изучить. К сожалению, мы живем не в идеальном мире, где не допускаем ошибок, и тесты периодически падают. Когда тестов слишком много, такое, казалось бы, простое действие, как скачивание архива и поиск скриншотов с различиями состояний «до»/«после», становится изнурительным занятием. А как было бы круто, если бы была возможность упростить этот процесс!
В Cypress для этого есть официальный платный инструмент — Dashboard. Но тарифы, которые там предлагаются, выглядят чрезмерно дорогими для наших нужд.
Есть альтернативное неофициальное решение, которое успело набрать популярность, — Sorry Cypress. Его авторы предлагают свой вариант Dashboard, но уже с более низкими ценами или с возможностью хостинга всей инфраструктуры на свои сервера. Этот неофициальный вариант уже кажется более приемлемым. Но мы решили написать простенький Github-бот.
Как работают Github Apps
Если сильно упрощать, то Github App — это набор функций-колбэков, которые вызываются при срабатывании нужного события (webhook-event) в репозитории. Список всех доступных событий представлен на странице Github Docs. Сами функции-колбэки обычно внутри себя дергают API Github, которые и приводят к созданию в репозитории новых комментариев, веток, файлов и т. п.
Всю работу с прослушиванием нужных событий и отправкой нужных API-запросов можно выполнять и на нативном js. Но гораздо проще это сделать с помощью уже готовых популярных решений, предоставляющих некоторое абстрагирование от всего этого. Мы воспользуемся фреймворком Probot, созданным для написания Github-приложений.
Процесс инициализации нового приложения через cli-команды и шаги его запуска хорошо описаны на официальной странице фреймворка, разбирать мы их не будем. При создании приложения рекомендуем выбирать шаблон, написанный на Typescript: строгая типизация позволит вам избежать некоторых ошибок (о том, как можно выжать максимум из возможностей данного языка, читайте в этой статье). При создании текущего приложения мы также будем использовать шаблон с Typescript.
Прослушиваем события репозитория
Нашему боту достаточно прослушивать только три типа событий: когда workflow начинается и завершается, а также когда PR закрывается. Открываем в сгенерированном приложении index.ts
файл и добавляем следующий код:
import {Probot} from 'probot';
export = (app: Probot) => {
app.on('workflow_run.requested', async context => {
// ...
});
app.on('workflow_run.completed', async context => {
// ...
});
app.on('pull_request.closed', async context => {
// ...
});
};
Примечание: не забываем в Github на странице настроек приложения дать боту права на прослушивание событий workflow_run
и pull_request
.
В коде видно, что каждая функция, пробрасываемая как колбэк на события репозитория, принимает аргумент context
.
Этот контекст содержит множество полезной информации о «прослушиваемом» событии. Например, так будет выглядеть утилита-селектор для получения имени workflow, который вызвал данный webhook-event:
import {Context} from 'probot/lib/context';
import {
EventPayloads
} from '@octokit/webhooks/dist-types/generated/event-payloads';
type WorkflowRunContext = Context<EventPayloads.WebhookPayloadWorkflowRun>;
export const getWorkflowName = (context: WorkflowRunContext): string =>
context.payload.workflow?.name || '';
Также внутри context.payload
содержится нужная нам информация: id workflow, название ветки, на которой сработал данный workflow, номер открытого pull request и множество другой информации.
Используем Github API
Фреймворк Probot внутри себя использует Node.js-модуль '@octokit/rest'. Чтобы получить доступ к REST-API-методам Github, достаточно обратиться к context.octokit…
. Весь перечень доступных действий смотрите здесь.
Нашему боту для создания комментариев к PR нужны следующие методы:
context.octokit.issues.createComment
(создать новый комментарий к PR).context.octokit.rest.issues.updateComment
(отредактировать контент уже существующего комментария к PR).
Пусть вас не смущает, что мы используем методы из объекта issue
. Pull request — это issue, содержащий код. Поэтому все методы, применимые к issue, применимы и к pull requests.
Для загрузки артефактов со скриншотами упавших тестов мы используем методы:
context.octokit.actions.listWorkflowRunArtifacts
(перечень метаинформации о доступных артефактах данного workflow).context.octokit.actions.downloadArtifact
(загрузка архивов-артефактов по их id).
Итак, у нас есть файлы со скриншотами, и мы знаем, как создавать комментарии. Комментарии понимают Markdown-синтаксис, а в данный формат можно вставлять изображения как base64-строки. Кажется, что еще полшага — и все будет готово… но нет. Markdown, который использует Github, не поддерживает возможность вставить таким образом изображения: только по ссылке из внешнего источника.
Но и эту проблему можно решить: можно загрузить нужный файл (который мы планируем прикрепить к отчету об упавших тестах) на отдельную ветку репозитория и получить доступ к этому изображению через https://raw.githubusercontent.com/...
. Код для решения данной проблемы будет следующий:
const GITHUB_CDN_DOMAIN = 'https://raw.githubusercontent.com';
const getFile = async (path: string, branch?: string) => {
return context.octokit.repos.getContent({
...context.repo(),
path,
ref: branch
}).catch(() => null);
}
// returns url to uploaded file
const uploadFile = async ({file, path, branch, commitMessage}: {
file: Buffer,
path: string,
commitMessage: string,
branch: string
}): Promise<string> => {
const {repo, owner} = context.repo();
const content = file.toString('base64');
const oldFileVersion = await getFile(path, branch);
const sha = oldFileVersion && 'sha' in oldFileVersion.data
? oldFileVersion.data.sha
: undefined
const fileUrl = `${GITHUB_CDN_DOMAIN}/${owner}/${repo}/${branch}/${path}`;
return context.octokit.repos
.createOrUpdateFileContents({
owner,
repo,
content,
path,
branch,
sha,
message: commitMessage,
})
.then(() => fileUrl);
}
// returns urls to uploaded images
const uploadImages = async (
images: Buffer[],
pr: number,
workflowId: number,
i: number
): Promise<string[]> => {
const {repo, owner} = context.repo();
const path = `__bot-screens/${owner}-${repo}-${pr}/${workflowId}-${i}.png`;
return Promise.all(images.map(
(file, index) => uploadFile({
file,
path,
commitMessage: 'chore: upload images of failed screenshot tests',
branch: 'screenshot-bot-storage',
})
));
}
После закрытия PR загруженные изображения всегда можно удалить. И для всех этих действий также есть свои методы в библиотеке @octokit/rest.
Деплой готового кода
Деплой — неизбежный этап в жизни каждого приложения. Официальная документация фреймворка Probot предлагает подробную инструкцию, как осуществить развертывание вашего готового приложения на различные популярные сервисы. Мы свое Node.js-приложение развернули на Glitch. Этот сервис предоставляет возможность бесплатного хостинга, а ограничения бесплатного аккаунта несущественны для такого простого приложения, как Github-бот.
Исходный код получившегося бота, который мы активно используем в нашем проекте, можно изучить на репозитории Github. Разработка получила название Argus (многоглазый великан из древнегреческой мифологии). Она проработана гораздо глубже, чем можно описать в этой статье, но основное ядро получившегося приложения было описано выше.
Вместо заключения
Написание Github-бота — очень простое занятие. Оно практически не требует глубоких знаний языка или фреймворка. Весь процесс создания в основном сводится к изучению документации со списком webhook-событий репозитория, а также документации REST-API-методов Github, чтобы найти и применить их под вашу задачу.
В этой статье мы построили Github-приложение, которое следит за workflow, содержащим скриншотные тесты. Если тесты падают, то бот загружает артефакты, находит в них скриншоты с разницей состояний «до»/«после», а потом прикрепляет их как комментарий к PR.
Полученный код мы задеплоили как бота под названием Lumberjack (лесоруб). Он уже активно следит за нашим проектом Taiga UI. Но бот написан таким образом, что вы легко его сможете настроить и под свой проект — достаточно пригласить его в свой репозиторий и указать, за какими workflow ему стоит следить.
mark_ablov
Вместо
getContent
лучше использовать другой метод для получения SHA текущей версии файла.Заливка файлов не так важна у вас, но можно в один коммит включать несколько файлов, хоть это и более муторно - нужно создать git blob'ы для каждого файла, затем git tree, создать сам коммит и обновить branch head на sha новосоздонного коммита.
nsbarsukov Автор
Спасибо за советы, учту при доработке бота!