«Боты должны работать, разработчики должны думать»: пишем Github App на Node.js

Kate

Administrator
Команда форума
Разработчик — натура творческая. У него нет времени на рутинные задачи, о которых может позаботиться машина. Поэтому все, что можно автоматизировать, должно быть автоматизировано.

Привет! Меня зовут Никита. Я разработчик Taiga UI, библиотеки Angular-компонентов, которая активно используется в нашей компании «Тинькофф». Я расскажу про решение одной из таких рутинных задач на нашем проекте с помощью написания с нуля своего Github App на Node.js.

044d544a434c8171281592e39ec36bce.png

Постановка проблемы​

На проекте мы активно пишем скриншотные тесты с использованием фреймворка. 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 нужны следующие методы:

  1. context.octokit.issues.createComment (создать новый комментарий к PR).
  2. context.octokit.rest.issues.updateComment (отредактировать контент уже существующего комментария к PR).
Пусть вас не смущает, что мы используем методы из объекта issue. Pull request — это issue, содержащий код. Поэтому все методы, применимые к issue, применимы и к pull requests.

Для загрузки артефактов со скриншотами упавших тестов мы используем методы:

  1. context.octokit.actions.listWorkflowRunArtifacts (перечень метаинформации о доступных артефактах данного workflow).
  2. 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 ему стоит следить.

 
Сверху