Я расскажу о создании Discord-бота на Python, который запускает сборку проекта в Unity Cloud Build и создаёт ссылку на скачивание для QA из внешней команды.
Настройка и поддержка полноценного CI занимает много времени и средств. При этом команде чаще всего нужна базовая функциональность. Так случилось и с нашим проектом. Потребовался простой способ сборки проекта в Unity Cloud Build и передачи его на внешний QA.
На начальном этапе были такие условия и ограничения:
- репозиторий проекта — Bitbucket (есть поддержка GitHub и GitLab);
- наличие лицензии Unity Teams Advanced, которая разблокирует доступ к Unity Cloud Build;
- ограниченный доступ к Unity Cloud Build для части команды;
- сборка версии проекта не после каждого коммита в ветку, а только после завершения работы над функциональностью или багом.
Для этих целей был создан Discord-бот на Python (этот язык выбрал, как самый быстрый и удобный для автоматизации). Он позволил команде запускать сборку версии в Unity Cloud Build, и после её успешного завершения формировал ссылку для скачивания QA, как внутри команды, так и вне её.
Боты для Discord на Python: проект для начинающих
Перед тем, как начать писать бот, необходимо подготовить окружение. Для этого настроим Unity Cloud Build и Discord-гильдию (сервер) на сборку проекта под разные платформы. После этого настроим отправку сообщений об успешном завершении сборки из Unity Cloud Build в канал. И создадим бота, работающего с Unity Cloud Build и Discord.
Настройка проекта в Unity Cloud Build
Переходим в Unity Dashboard: https://dashboard.unity3d.com/landing и выбираем Cloud Build:
Далее выбираем Create project и вводим имя нашего проекта (для примера, назовем его Dungeon Crawl Prototype):
После этого в списке проектов появится созданный проект. Переходим к нему и выбираем SET UP CLOUD BUILD:
На следующей странице выбираем с каким Git репозиторием будем интегрировать наш проект. В нашем случае это GitHub:
Подключаем GitHub и переходим к настройке сборки проекта:
Добавляем новую сборку SETUP NEW TARGET:
Выбираем платформу. В нашем примере было создано две сборки под Windows 64 и Android:
Вводим название Target Label, в Branch ветку в git репозитории. Для тестового примера был установлен флаг Auto Detect Version:
Для примера был выбран Debug ключ и тестовый Bundle ID:
Далее нажимаем NEXT: BUILD, после чего настройка сборки завершена. Те же шаги необходимо повторить и для других платформ. На этом заканчиваем настройку Unity Cloud Build. В будущем понадобится создать Discord-оповещения для старта построения сборки и результата сборки. Сделаем это после настройки сервера.
Создание и настройка Discord-гильдии (сервера)
Переходим по ссылке к Discord https://discord.com/, скачиваем приложение и создаем аккаунт. После того как приложение установлено и произведена авторизация в аккаунт, необходимо создать новый Discord-сервер (далее будем называть их гильдиями). Создаем новую гильдию нажав на «+». Выбираем пункт Create My Own → For me and my friends и вводим название гильдии без пробелов.
После создания сервера необходимо добавить новые роли. Для этого переходим в настройки и создаем три:
- bot для нашего бота, ci для пользователей:
- ci для пользователей, которые смогут давать команды боту:
- qa для тех, кому будет доступна ссылка на скачивания сборки.
Далее создадим три канала:
- ci для отправки команд боту;
- build для сообщений от Unity Cloud Build;
- testing отправки ссылок QA.
На этом настройка гильдии закончена. Следующим шагом будет создание Discord-бота.
Создание и настройка Discord-бота
Боты для Discord создаются на Discord Developer Portal: https://discord.com/developers/applications. Необходимо перейти по ссылке и авторизоваться. Затем создаем новое приложение, нажав на New Application. Вводим имя приложения и нажимаем Create:
Далее создаем бота. Для этого переходим в созданное приложение, выбираем вкладку Bot и нажимаем Add Bot:
Далее необходимо настроить разрешения для бота. Переходим в пункт OAuth2 и выбираем следующие опции:
После выбора всех опций копируем url кнопкой Copy. Затем в браузере переходим по скопированной ссылке, выбираем Discord-гильдию, созданную ранее, и проверяем, выставленные разрешения:
Далее в настройках Discord-гильдии добавляем нашего бота в роль bot, которую создали ранее:
Осталось настроить Discord-оповещения для Unity Cloud Build (о начале и завершении сборки, и её результате):
Настройка Discord оповещения для Unity Cloud Build
Возвращаемся в Unity Cloud Build и переходим в раздел Notifications, в котором выбираем Integrations page:
Далее создаем новую интеграцию, нажав на NEW INTEGRATION:
Выбираем Discord и нажимаем NEXT:
В появившемся окне выбираем события, при которых будут отправляться сообщения в гильдию. Затем нажимаем NEXT:
В появившемся окне авторизации выбираем нашу гильдию и канал, куда приходить сообщения. Нажимаем Authorize:
На этом настройка оповещений и окружения завершена. На следующем этапе создадим Discord-бот на Python.
Создание Discord-бота на Python
Настройка проекта
Для создания бота будем использовать Python 3.9, а для работы с Discord — библиотеку discord.py: https://pypi.org/project/discord.py/. Также понадобится библиотека python-dotenv для получения переменных окружения из .env файла.
Создаём новый проект и добавляем в него requirements.txt с таким содержанием:
aiohttp==3.7.3
python-dotenv==0.15.0
discord.py==1.6.0
Так же создаём файл .env с переменными:
DISCORD_TOKEN=
DISCORD_GUILD=
DISCORD_BOT_NAME=
UNITY_API_KEY=
UNITY_ORGANIZATION_ID=
PROJECT_ID=PROJECT_NAME=
DISCORD_GUILD — название Discord гильдии.
DISCORD_BOT_NAME — имя бота.
DISCORD_TOKEN — можно получить на Discord Developer Portal в разделе Bot, скопировав токен:
UNITY_API_KEY можно получить в настройках Unity Cloud Build:
UNITY_ORGANIZATION_ID и PROJECT_ID так же можно получить в Unity Cloud Build. Для этого открываем конфигурацию сборки и нажимаем на EDIT BASIC INFO:
Далее в строке браузера можно увидеть UNITY_ORGANIZATION_ID и PROJECT_ID:
PROJECT_NAME — имя проекта, который собирается в Unity Cloud Build:
Написание бота
Далее я опишу основные моменты реализации Discord-бота. Создаём класс DiscordBot который наследуется от discord.Client. Он реализует API, позволяющее подключиться к Discord гильдии:
import discord
class DiscordBot(discord.Client):
def __init__(self):
super(DiscordBot, self).__init__()
Далее необходимо подключиться к Discord-гильдии:
await self.start('discord bot token', bot=True)
Метод on_ready, позволяет реагировать на событие подключения бота к Discord-гильдии:
async def on_ready(self):
for guild in self.guilds:
if guild.name == 'self guild name here':
print('Connected to the Discord guild')
return
print('Fails connect to the Discord guild')
await self.close()
Для обработки сообщений необходимо реализовать метод on_message:
async def on_message(self, message: discord.Message):
try:
# Do not process self messages or messages not from bot supported channels
message_channel = message.channel.name
if message.author == self.user or message_channel not in ['ci', 'build']:
return
if message_channel == 'build':
await self._process_build_event(message)
else:
# React only for messages that mention bot
for mention in message.mentions:
if self.user == mention:
await self._process_bot_command(message)
return
except Exception as e:
print(f'Exception: {e}')
В первой проверке отсекаем все сообщения от самого бота, а также сообщения, пришедшие не из каналов, поддерживаемых ботом. Затем разделяем сообщения на два типа: команды боту и сообщения от Unity Cloud Build. В зависимости от результата передаём сообщение нужному обработчику.
Обработчик сообщений для выполнения команд ботом. Метод _get_command_for_bot читает сообщение, ищет в нем команду и параметры команды. После этого выполняется метод конкретной команды:
async def _process_bot_command(self, message: discord.Message):
command, params = self._get_command_for_bot(message)
if command == 'build':
await self._start_build(message, params)
elif command == 'build_target_info':
await self._build_target_info(message, params)
else:
print(f'Unsupported command for bot: {command}')
Для работы с Unity Cloud Build создаём класс UnityCloudBuildWorker:
from aiohttp import ClientSession
class UnityCloudBuildWorker(object):
def __init__(self, base_url: str, project_id: str, api_key: str):
self._base_url = base_url
self._project_id = project_id
self._cloud_build_targets = {
'qa_windows': 'qa-windows-64-bit',
'qa_android': 'qa-android'
}
self._supported_builds = list(self._cloud_build_targets.keys())
self._session = None
self._base_header = {
'Content-Type': 'application/json',
'Authorization': f'Basic {api_key}',
}
async def start_worker(self):
assert self._session is None
self._session = ClientSession()
async def stop_worker(self):
if self._session is not None:
await self._session.close()
self._session = None
Разберём пример отправки команды на сборку проекта. Для этого в канал «ci» боту отправляется команда @AleksPinGames_DEV_Bot build qa_windows. Она обозначает, что необходимо начать сборку QA Windows.
Обработчик в DiscordBot получает сообщение и набор параметров и отправляет в чат ответ о начале выполнения задачи. Далее в UnityCloudBuildWorker отправляется запрос на сборку проекта через Unity Cloud Build. После получения ответа бот сообщает о том, какая сборка начала строиться и какая ветка использовалась в Git репозитории:
async def _start_build(self, message: discord.Message, params: str):
await message.channel.send('Send start build command to Unity Cloud Build')
result = await self._unity_cloud_build_worker.cmd_start_build(params)
await message.channel.send(result)
UnityCloudBuildWorker получает команду и выполняет метод cmd_start_build. Затем формируется url запроса и выполняется запрос. Полное описание API для работы с Unity Cloud Build можно найти тут: https://build-api.cloud.unity3d.com/docs/1.0.0/index.html#intro:
async def cmd_start_build(self, build_target: str) -> str:
build_target_id = self._cloud_build_targets.get(build_target, None)
if build_target_id is None:
return f'Unsupported build target: {build_target}'
url = f'{self._base_url}/projects/{self._project_id}/buildtargets/{build_target_id}/builds'
headers = {'clean': 'true'}
response = await self._send(self._send_post, url, headers)
result = await response.json() if response is not None else {}
self._logger.info(f'{self._log_tag} cmd_start_build result: {result}')
if result and len(result) > 0:
result_msg = f"Start building {result[0]['buildTargetName']} | Branch: {result[0]['scmBranch']}"
else:
result_msg = f'Error starting build for target: {build_target}'
return result_msg
async def _send(self, send_method: callable, url: str, headers: dict = None):
try:
if headers is not None:
headers.update(self._base_header)
else:
headers = self._base_header
result = await send_method(url, headers)
return result
except Exception as e:
print(f'Exception: {e}')
return None
async def _send_get(self, url: str, headers: dict = None):
return await self._session.get(url=url, headers=headers, ssl=True)
async def _send_post(self, url: str, headers: dict = None):
return await self._session.post(url=url, headers=headers, ssl=True)
После сборки проекта Unity Cloud Build отправляет в канал build сообщение о результате сборки. На это сообщение реагирует обработчик сообщений от Unity Cloud Build.
Обработчик сообщений от Unity Cloud Build проверяет от кого пришло сообщение. Все сообщения от Unity с данными по сборке вычитываются и формируется ссылка для канала QA testing. Сообщение в канал QA попадёт только в случае build_success = True. Также в данном обработчике удаляется ссылка на скачивание собранного проекта непосредственно в Unity Cloud Build. Остаётся только внешняя ссылка, не требующая авторизации в Unity Team.
async def _process_build_event(self, message: discord.Message):
try:
if message.author.name != 'Unity' or len(message.embeds) <= 0:
return
to_delete_idx = None
build_success = False
embed = message.embeds[0]
for idx, field in enumerate(embed.fields):
field_name = field.name
if field_name == 'Build success':
build_success = True
elif field_name == 'Download':
to_delete_idx = idx
if build_success:
if to_delete_idx is not None:
embed.remove_field(to_delete_idx)
channel = self._server_text_channels['testing']
await channel.send(embed=embed)
except Exception as e:
print(f'Exception: {e}')
Запуск бота как сервис
Для удобства запуска бота на удаленном сервере мы добавили в репозиторий возможность собрать и запустить бота в качестве Docker-контейнера. Для этого были добавлены Dockerfile:
FROM python:3.9.2-buster
ENV APP_DIR /unity_cloud_build_discord_bot
ENV PYTHONPATH $PYTHONPATH:$APP_DIR
WORKDIR $APP_DIR
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN apt-get update && apt-get install -y --no-install-recommends locales locales-all
COPY . $APP_DIR
CMD python app/discord_bot.py
И Makefile, в котором описаны команды up и down для запуска и остановки контейнера с ботом:
TAG ?= latest
CONTAINER_NAME ?= unity_cloud_build_discord_bot
IMAGE_NAME ?= $(CONTAINER_NAME):$(TAG)
build:
docker build . -t $(IMAGE_NAME)
up: build
docker run -d \
--name $(CONTAINER_NAME) \
--env-file .env \
$(IMAGE_NAME)
down:
docker stop $(CONTAINER_NAME)
docker rm $(CONTAINER_NAME)
Все инструкции и полная версия кода бота доступны в GitHub-репозитории: https://github.com/AleksandrPindyk/unity_cloud_build_discord_bot
Итог
На этом всё 🙂 Мы запустили бот в качестве Docker-контейнера и настроили на Discord-гильдию, где идёт общение разработчиков и внешних QA. После нескольких дней тестирования мы пришли к выводу, что использовать бота удобнее, чем запускать сборку через веб-страницу Unity Cloud Build. В следующей версии добавим поддержку нескольких проектов и разворачивание WebGL-версии сразу на удаленном сервере в Docker-контейнере. Более детальную информацию о возможностях разработки Discord-ботов на Python, можно получить на сайте с официальной документацией: https://discord.com/developers/docs/intro.