×

Автоматизуємо роботу з Unity Cloud Build. Discord-бот за допомогою Python

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Привіт, мене звати Олександр, я працюю над AI в команді World of Tanks у київській студії Wargaming. Я розповім про створення 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, як всередині команди, так і поза нею.

До того, як почати створювати бота, необхідно підготувати оточення. Для цього налаштуємо Unity Cloud Build та Discord-гільдію (сервер) на збірку проєкту під різні платформи. Після цього налаштуємо відправку повідомлень про успішне завершення збирання з Unity Cloud Build у канал Discord. Та створимо бота, що працює з Unity Cloud Build та Discord.

Налаштування проєкту в Unity Cloud Build

Переходимо в Unity Dashboard: 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 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: 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: 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-репозиторії: github.com/...​y_cloud_build_discord_bot

Висновок

На цьому все 🙂 Ми запустили бот як Docker-контейнера та налаштували на Discord-гільдію, де йде спілкування розробників та зовнішніх QA. Після кількох днів тестування ми прийшли до висновку, що використовувати бота зручніше, ніж запускати збірку через веб-сторінку Unity Cloud Build. У наступній версії додамо підтримку декількох проєктів та розгортання WebGL-версії відразу на віддаленому сервері в Docker-контейнері. Більш детальну інформацію про можливості розробки Discord-ботів на Python, можна отримати на сайті з офіційною документацією: discord.com/developers/docs/intro.

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Wargaming використовує Discord для спілкування?)

Ні, це було зроблено для хобі проєкта)

Підписатись на коментарі