Меня зовут Авенир Воронов и я отвечаю за Лабораторию инноваций в компании "ДАР". В этой статье я расскажу про опыт работы с агентами курсора и великолепный Github Cli, который сэкономил кучу времени.
Вступление
Лето. Время отпусков. Делать нечего.
Можно залипать в рилсах или сериальчиках на пляже, но зачем, если можно создать что-то прикольное с мобильного через агентов. Начнем с самого простого (стоит 20 USD в месяц или 3 тыс руб в год, если немного поискать).

Итак создаем репозиторий (в моем случае такой) или берем готовый. Даем задачу агенту создать игру.

Пока агент делает игру немного поговорим про UI. Агент изначально работает на Claude Sonnet. Когда токены заканчиваются, предлагает перейти на Auto режим.
Агенты могут работать параллельно. Я рекомендую вести древовидные записи задач для агентов, чтобы самому не потеряться. Иначе уже при запуске 3-4 агентов можно забыть с кем и о чем договаривался. Агенты любят:
Неправильно выбирать решение
Не доделывать работу до конца
Не запускать код у себя перед тем, как передать в тестирование
Запускать код у себя, но не передавать в тестирование
Проявлять излишнюю инициативу
Не проявлять инициативу и просить у вас подтверждение на каждое действиею

Поэтому надо четко отслеживать статус ваших взаимоотношений с каждым агентом. Или хотя бы в Obsidian записывать.

Для общения с агентами есть две вкладки. Первое — чат, в котором существует возможность в ходе работы спрашивать «Получается ли?»

Вторая вкладка, Diff, позволяет видеть результаты действий агента.
В итоге можно сделать Pull Request и Merge прямо в агенте.
Также стоит учитывать следующие вещи:
1. Агенты порождают ветки независимо от первоначальных настроек. Если за этим не следить, можно остаться с большим количеством веток с абсолютно ничего не говорящими названиями.
2. Агенты могут терять связь с Git, но способны работать с ним через стандартные консольные инструменты. Иногда это превращается в необходимость. Например, кнопка создания Pull Request может не работать без объяснения причины. Тогда следует подсказать агенту, где почитать инструкции, дать нужные адреса, токены. Это может привести к небольшой анархии в репозиториях, поэтому следите за правами, которые вы даете агентам.

В опциях к агентам есть экспериментальная функция BugBot, которая находит в вашем коде баги. Функция хорошая, так как проверяет в том числе безопасность вашего кода, а не только отвалившиеся кнопки на главном экране. Включается она на вкладе Dashboard.

Пока мы изучали агента, получилось некоторое подобие игры, в которую даже любопытно играть самому.

Можно показать знакомым, собрать обратную связь и добавить функции.

Переходим к дизайну и просим агентов добавить пустые картинки в проект и написать промпты для Midjourney для каждой картинки, с указанием размеров. Готовые промпты и адреса картинок сгружаем в md файл.

Пересылаем промпты в свой генератор и сохраняем в игру.

Агент довольно хорошо «обошелся» со скроллами. Вертикальные прокрутки поставил на все меню, а горизонтальные на подменю (выбор профессии, мечты, навыков и т. д.)

Практически все идеи по кастомизации исходили от агента.
В частности, оценка навыков в рублях и статистика продолжительности жизни были сгенерированы на основе поиска статистики в сети.

Текст на плашках изначально выходил за края. Но достаточно было указать на этот факт, чтобы агент всё исправил.

Интерфейс основного экрана получился практически с первого раза. Все доработки совершал сам агент без необходимости открывать верстку.

На этом этапе самым сложным было объяснить агентам, что такое игровой кубик. Нейросети не понимали принципа расположения точек.
Вариант с генерацией трехмерных кубиков через mesh.ai тоже не увенчался успехом. Кубики выходили странными, точки плыли по граням.

Мы с агентом попробовали написать код для трехмерного куба и натянуть текстуру через питон, но в итоге «сошлись» на простом скрипте для блендера. Cursor сгенерировал скрипт сразу хорошо.

К этому моменту проект уже разросся, и я попросил описать архитектуру, чтобы понимать, что вообще происходит. Расписать попросил по C4. И вот что получилось:
Первый слой из одного квадрата.

Пользователь взаимодействует с приложением через графический интерфейс.
Второй слой агент описал одним контейнером с 4 слоями. Вполне логично для такого простого приложения.

UI слой: Активности (MainActivity, GameActivity, CharacterCreationActivity, ProfessionSelectionActivity, RulesActivity) и layout-файлы.
Логика игры: GameManager, модели (Player, GameState, Card, Asset и др.), обработка событий, расчёты.
Данные: GameDataManager (предоставляет профессии, мечты и др.), хранение состояния в памяти (Parcelable)
Ресурсы: Изображения, иконки, стили, строки, layout-описания
На третьем слое агент нарисовал 4 компонента.
1. UI-компоненты (экраны, интерфейс)

MainActivity — главный экран
GameActivity — основной игровой процесс
CharacterCreationActivity — расширенное создание персонажа
ProfessionSelectionActivity — выбор профессии и мечты
RulesActivity — экран с правилами
Adapters (отображение списков):
adapters/AssetAdapter.kt
adapters/ProfessionAdapter.kt
adapters/DreamAdapter.kt
(и другие в adapters/)
2. Компонент бизнес-логики

GameManager — управление состоянием игры
3. Компонент моделей (структура данных)

models/Player.kt — игрок
models/GameState.kt — состояние игры
models/Card.kt — карточки, профессии, мечты, активы, пассивы, инвестиции
models/Asset.kt, models/Liability.kt, models/Investment.kt — отдельные сущности
models/FinancialEntry.kt — финансовый журнал
models/ProfessionalRisk.kt — профессиональные риски
(и другие в models/)
4. Компонент данных

data/GameDataManager.kt — статические игровые данные
data/FinancialTemplates.kt — шаблоны финансовых профилей
Дальше можно продолжать и без агента. Но захотелось ускорить процесс работы.
Про CICD и Github
Первой мыслью было - написать пайпы CICD, которые позволили бы тестировать apk автоматически. Чтобы сделать CICD в GitHub используются Github Actions.

Это Yaml, который лежит в .github/workflows
Workflow (рабочий процесс) — файл YAML.
Event (событие) — триггер, по которому стартует workflow (например, push, pull_request, issue). Задается в том же yaml по слову on:
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
Job (задача) — логически обособленный набор шагов (steps), выполняющихся параллельно или последовательно.
jobs:
# Основная сборка и тестирование
build-and-test:
...
# Параллельная генерация скриншотов для телефона
screenshots-phone:
...
Step (шаг) — отдельная инструкция: скрипт shell, исполнение actions, использование стороннего действия.
- name: 🔧 Grant execute permission for gradlew
run: chmod +x gradlew
Пример Workflow:
name: 🚀 Stable Build & Release
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
env:
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true
MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: "true"
jobs:
# Основная сборка и тестирование
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
version_code: ${{ steps.version.outputs.version_code }}
date: ${{ steps.date.outputs.date }}
apk_path: ${{ steps.apk_path.outputs.path }}
# should_release больше не нужен
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Получаем полную историю для changelog
- name: 🔍 Determine if this is a release build
id: should_release
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "value=true" >> $GITHUB_OUTPUT
else
echo "value=false" >> $GITHUB_OUTPUT
fi
- name: 🔖 Bump version (only for releases)
id: bump_version
if: steps.should_release.outputs.value == 'true'
run: |
# Получаем текущую версию
CURR_VERSION=$(grep -oP 'versionName\s+"\K[^"]+' app/build.gradle)
CURR_CODE=$(grep -oP 'versionCode\s+\K[0-9]+' app/build.gradle)
# Инкрементируем minor версию (x.y -> x.(y+1))
MAJOR=$(echo $CURR_VERSION | cut -d. -f1)
MINOR=$(echo $CURR_VERSION | cut -d. -f2)
if [ -z "$MINOR" ]; then MINOR=0; fi
NEW_MINOR=$((MINOR+1))
NEW_VERSION="$MAJOR.$NEW_MINOR"
NEW_CODE=$((CURR_CODE+1))
# Обновляем build.gradle
sed -i "s/versionName \"$CURR_VERSION\"/versionName \"$NEW_VERSION\"/" app/build.gradle
sed -i "s/versionCode $CURR_CODE/versionCode $NEW_CODE/" app/build.gradle
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "new_code=$NEW_CODE" >> $GITHUB_OUTPUT
echo "Version bumped: $CURR_VERSION → $NEW_VERSION ($CURR_CODE → $NEW_CODE)"
# Коммитим изменения
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add app/build.gradle
git commit -m "🔖 Bump version to $NEW_VERSION ($NEW_CODE) [auto]" || echo "No version change to commit"
git push || echo "No changes to push"
- name: 📝 Generate release notes (only for releases)
id: release_notes
if: steps.should_release.outputs.value == 'true'
run: |
echo "## 🎮 Financial Success v${{ steps.bump_version.outputs.new_version }}" > RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### 📅 Дата релиза: $(date '+%Y-%m-%d %H:%M UTC')" >> RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### 🔄 Последние изменения:" >> RELEASE_NOTES.md
git log --oneline -10 >> RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### 📋 Изменения из CHANGELOG.md:" >> RELEASE_NOTES.md
if [ -f "docs/CHANGELOG.md" ]; then
tail -n 20 docs/CHANGELOG.md >> RELEASE_NOTES.md
fi
cat RELEASE_NOTES.md
- name: 📝 Commit release notes (only for releases)
if: steps.should_release.outputs.value == 'true'
run: |
git add RELEASE_NOTES.md
git commit -m "📝 Update release notes [auto]" || echo "No release notes change to commit"
git push || echo "No changes to push"
- name: ☕ Setup JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: 🔍 Check dependencies
run: |
echo "🔍 Checking dependencies..."
# Проверка Java
java_version=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d'.' -f1)
echo "☕ Java version: $java_version"
if [ "$java_version" != "21" ]; then
echo "⚠️ Warning: Recommended Java 21, found: $java_version"
fi
# Проверка gradlew
if [ ! -f "gradlew" ]; then
echo "❌ Error: gradlew not found"
exit 1
fi
echo "✅ Dependencies checked"
- name: 🤖 Setup Android SDK
uses: android-actions/setup-android@v3
- name: 📦 Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/build-cache
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: 🔧 Grant execute permission for gradlew
run: chmod +x gradlew
- name: � Clean project
run: |
echo "🧹 Cleaning project..."
./gradlew clean --no-daemon
if [ $? -eq 0 ]; then
echo "✅ Project cleaned successfully"
else
echo "❌ Project clean failed"
exit 1
fi
- name: �� Run Unit Tests
run: |
echo "🧪 Running unit tests..."
./gradlew test --no-daemon
if [ $? -eq 0 ]; then
echo "✅ Unit tests passed"
else
echo "❌ Unit tests failed"
exit 1
fi
- name: 🔨 Build Debug APK
run: |
echo "🔨 Building debug APK..."
./gradlew assembleDebug --no-daemon
if [ $? -eq 0 ]; then
echo "✅ Debug APK built successfully"
else
echo "❌ Debug APK build failed"
exit 1
fi
- name: 📊 Get version info
id: version
run: |
VERSION=$(grep -oP 'versionName\s+"\K[^"]+' app/build.gradle)
VERSION_CODE=$(grep -oP 'versionCode\s+\K[0-9]+' app/build.gradle)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT
echo "Version: $VERSION ($VERSION_CODE)"
- name: 📅 Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d_%H-%M')" >> $GITHUB_OUTPUT
- name: 📱 Prepare APK for distribution
id: apk_path
run: |
echo "📱 Preparing APK for distribution..."
# Проверяем, что APK существует
if [ ! -f "app/build/outputs/apk/debug/app-debug.apk" ]; then
echo "❌ Error: Debug APK not found"
ls -la app/build/outputs/apk/debug/ || echo "Debug directory not found"
exit 1
fi
mkdir -p releases/debug
cp app/build/outputs/apk/debug/app-debug.apk releases/debug/
cd releases/debug
# Переименовываем с версией и датой
newname="FinancialSuccess-v${{ steps.version.outputs.version }}-${{ steps.date.outputs.date }}-debug.apk"
mv -f app-debug.apk "$newname"
echo "path=releases/debug/$newname" >> $GITHUB_OUTPUT
# Создаем символическую ссылку на latest
ln -sf "$newname" "latest-debug.apk"
# Очищаем старые APK (оставляем только 5 последних)
ls -tp FinancialSuccess-v*-debug.apk | grep -v '/$' | tail -n +6 | xargs -r rm -- 2>/dev/null || true
echo "✅ APK prepared: $newname"
echo "📊 APK files:"
ls -la
- name: 📤 Upload APK Artifact
uses: actions/upload-artifact@v4
with:
name: FinancialSuccess-v${{ steps.version.outputs.version }}-${{ steps.date.outputs.date }}
path: releases/debug/*.apk
retention-days: 30
- name: 💾 Commit APK files (only for releases)
if: steps.should_release.outputs.value == 'true'
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add releases/
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "📱 Auto-commit APK v${{ steps.version.outputs.version }} - ${{ steps.date.outputs.date }}
🔧 Build info:
- Version: ${{ steps.version.outputs.version }}
- Version Code: ${{ steps.version.outputs.version_code }}
- Date: ${{ steps.date.outputs.date }}
- Branch: ${{ github.ref_name }}
- Commit: ${{ github.sha }}"
git push
fi
# Параллельная генерация скриншотов для телефона
screenshots-phone:
runs-on: ubuntu-latest
needs: build-and-test
# Убрано условие should_release
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
- name: 📥 Download APK
uses: actions/download-artifact@v4
with:
name: FinancialSuccess-v${{ needs.build-and-test.outputs.version }}-${{ needs.build-and-test.outputs.date }}
- name: 🔧 Install Android SDK and platform-tools
env:
ANDROID_SDK_ROOT: ${{ runner.home }}/android-sdk
ANDROID_HOME: ${{ runner.home }}/android-sdk
PATH: ${{ runner.home }}/android-sdk/emulator:${{ runner.home }}/android-sdk/platform-tools:${{ runner.home }}/android-sdk/cmdline-tools/latest/bin:${{ env.PATH }}
run: |
wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O cmdline-tools.zip
unzip cmdline-tools.zip -d $HOME
mkdir -p $HOME/android-sdk/cmdline-tools
mv $HOME/cmdline-tools $HOME/android-sdk/cmdline-tools/latest
export ANDROID_SDK_ROOT=$HOME/android-sdk
export ANDROID_HOME=$HOME/android-sdk
export PATH=$PATH:$HOME/android-sdk/emulator:$HOME/android-sdk/platform-tools:$HOME/android-sdk/cmdline-tools/latest/bin
yes | sdkmanager --licenses
sdkmanager "platform-tools" "emulator" "system-images;android-34;default;x86_64"
# Проверяем, что system-image реально скачан
ls -la $ANDROID_SDK_ROOT/system-images/android-34/default/x86_64/
# Добавляем platform-tools в PATH для всех следующих шагов
echo "$HOME/android-sdk/platform-tools" >> $GITHUB_PATH
- name: 🔍 Проверка наличия adb в PATH (phone)
run: |
echo "PATH: $PATH"
which adb
adb version
- name: 🎭 Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
export PATH="$PATH":"$HOME/.maestro/bin"
echo "n" | maestro --version
- name: 📱 Start Android Emulator and Install APK (Phone)
shell: bash
env:
ANDROID_SDK_ROOT: ${{ runner.home }}/android-sdk
ANDROID_HOME: ${{ runner.home }}/android-sdk
PATH: ${{ runner.home }}/android-sdk/emulator:${{ runner.home }}/android-sdk/platform-tools:${{ runner.home }}/android-sdk/cmdline-tools/latest/bin:${{ env.PATH }}
run: |
set -e
echo "SDK root: $ANDROID_SDK_ROOT"
echo "PATH: $PATH"
ls -la $ANDROID_SDK_ROOT/system-images/android-34/default/x86_64/
rm -rf ~/.android/avd/*
# Создание AVD, если не существует
if ! avdmanager list avd | grep -q "ci_nexus5"; then
echo "no" | avdmanager create avd --name "ci_nexus5" --package "system-images;android-34;default;x86_64" --device "Nexus 5"
fi
# Запуск эмулятора в фоне
emulator -avd "ci_nexus5" -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -accel off -no-snapshot -no-metrics &
EMULATOR_PID=$!
# Пауза после запуска эмулятора (важно для CI)
sleep 30
# Ожидание появления устройства в adb (таймаут 120с)
emulator_timeout=120
emulator_elapsed=0
for i in {1..60}; do
if adb devices | grep -q "emulator"; then
echo "✅ Эмулятор появился в adb"
break
fi
if [ $i -eq 60 ]; then
echo "❌ Не удалось запустить эмулятор за $emulator_timeout секунд"
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
sleep 2
emulator_elapsed=$((emulator_elapsed + 2))
if [ $((emulator_elapsed % 30)) -eq 0 ]; then
echo "⏳ Ожидание эмулятора: $emulator_elapsed секунд из $emulator_timeout..."
fi
done
# Ожидание загрузки Android (sys.boot_completed, таймаут 180с)
boot_timeout=180
boot_elapsed=0
for i in {1..90}; do
if adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
echo "✅ Android полностью загружен"
break
fi
if [ $i -eq 90 ]; then
echo "❌ Android не загрузился за $boot_timeout секунд"
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
sleep 2
boot_elapsed=$((boot_elapsed + 2))
if [ $((boot_elapsed % 30)) -eq 0 ]; then
echo "⏳ Ожидание загрузки Android: $boot_elapsed секунд из $boot_timeout..."
fi
done
# Разблокировка экрана
adb shell input keyevent 82
adb shell input keyevent 82
sleep 2
# Установка APK
if [ ! -f "releases/debug/latest-debug.apk" ]; then
echo "❌ Error: latest-debug.apk not found in releases/debug/"
ls -la releases/debug/
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
adb install releases/debug/latest-debug.apk
if adb shell pm list packages | grep -q financialsuccess; then
echo "✅ APK успешно установлен"
else
echo "❌ Error: APK installation failed"
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
# ... скриншоты будут сниматься следующим шагом ...
# Остановка эмулятора
kill $EMULATOR_PID 2>/dev/null || true
pkill -f emulator 2>/dev/null || true
- name: 📸 Run Maestro Screenshots (Phone)
run: |
export PATH="$PATH":"$HOME/.maestro/bin"
mkdir -p screenshots/phone
# Пробуем специфичную конфигурацию для телефона
local config_file="maestro/screenshots-stable.yaml"
if [ ! -f "$config_file" ]; then
config_file="maestro/screenshots.yaml"
fi
if [ -f "$config_file" ]; then
echo "Using phone screenshots configuration: $config_file"
maestro test "$config_file" --format junit --output screenshots/phone/
echo "📱 Phone screenshots completed!"
else
echo "⚠️ Warning: Maestro configuration file not found: $config_file"
echo "📱 Phone screenshots skipped!"
fi
- name: 📤 Upload Phone Screenshots
uses: actions/upload-artifact@v4
with:
name: phone-screenshots-v${{ needs.build-and-test.outputs.version }}
path: screenshots/phone/
retention-days: 30
# Параллельная генерация скриншотов для планшета
screenshots-tablet:
runs-on: ubuntu-latest
needs: build-and-test
# Убрано условие should_release
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
- name: 📥 Download APK
uses: actions/download-artifact@v4
with:
name: FinancialSuccess-v${{ needs.build-and-test.outputs.version }}-${{ needs.build-and-test.outputs.date }}
- name: 🔧 Install Android SDK and platform-tools
env:
ANDROID_SDK_ROOT: ${{ runner.home }}/android-sdk
ANDROID_HOME: ${{ runner.home }}/android-sdk
PATH: ${{ runner.home }}/android-sdk/emulator:${{ runner.home }}/android-sdk/platform-tools:${{ runner.home }}/android-sdk/cmdline-tools/latest/bin:${{ env.PATH }}
run: |
wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O cmdline-tools.zip
unzip cmdline-tools.zip -d $HOME
mkdir -p $HOME/android-sdk/cmdline-tools
mv $HOME/cmdline-tools $HOME/android-sdk/cmdline-tools/latest
export ANDROID_SDK_ROOT=$HOME/android-sdk
export ANDROID_HOME=$HOME/android-sdk
export PATH=$PATH:$HOME/android-sdk/emulator:$HOME/android-sdk/platform-tools:$HOME/android-sdk/cmdline-tools/latest/bin
yes | sdkmanager --licenses
sdkmanager "platform-tools" "emulator" "system-images;android-34;default;x86_64"
# Проверяем, что system-image реально скачан
ls -la $ANDROID_SDK_ROOT/system-images/android-34/default/x86_64/
# Добавляем platform-tools в PATH для всех следующих шагов
echo "$HOME/android-sdk/platform-tools" >> $GITHUB_PATH
- name: 🔍 Проверка наличия adb в PATH (tablet)
run: |
echo "PATH: $PATH"
which adb
adb version
- name: 🎭 Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
export PATH="$PATH":"$HOME/.maestro/bin"
echo "n" | maestro --version
- name: 📱 Start Android Emulator and Install APK (Tablet)
shell: bash
env:
ANDROID_SDK_ROOT: ${{ runner.home }}/android-sdk
ANDROID_HOME: ${{ runner.home }}/android-sdk
PATH: ${{ runner.home }}/android-sdk/emulator:${{ runner.home }}/android-sdk/platform-tools:${{ runner.home }}/android-sdk/cmdline-tools/latest/bin:${{ env.PATH }}
run: |
set -e
echo "SDK root: $ANDROID_SDK_ROOT"
echo "PATH: $PATH"
ls -la $ANDROID_SDK_ROOT/system-images/android-34/default/x86_64/
rm -rf ~/.android/avd/*
# Создание AVD, если не существует
if ! avdmanager list avd | grep -q "ci_nexus10"; then
echo "no" | avdmanager create avd --name "ci_nexus10" --package "system-images;android-34;default;x86_64" --device "Nexus 10"
fi
# Запуск эмулятора в фоне
emulator -avd "ci_nexus10" -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -accel off -no-snapshot -no-metrics &
EMULATOR_PID=$!
# Пауза после запуска эмулятора (важно для CI)
sleep 30
# Ожидание появления устройства в adb (таймаут 120с)
emulator_timeout=120
emulator_elapsed=0
for i in {1..60}; do
if adb devices | grep -q "emulator"; then
echo "✅ Эмулятор появился в adb"
break
fi
if [ $i -eq 60 ]; then
echo "❌ Не удалось запустить эмулятор за $emulator_timeout секунд"
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
sleep 2
emulator_elapsed=$((emulator_elapsed + 2))
if [ $((emulator_elapsed % 30)) -eq 0 ]; then
echo "⏳ Ожидание эмулятора: $emulator_elapsed секунд из $emulator_timeout..."
fi
done
# Ожидание загрузки Android (sys.boot_completed, таймаут 180с)
boot_timeout=180
boot_elapsed=0
for i in {1..90}; do
if adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
echo "✅ Android полностью загружен"
break
fi
if [ $i -eq 90 ]; then
echo "❌ Android не загрузился за $boot_timeout секунд"
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
sleep 2
boot_elapsed=$((boot_elapsed + 2))
if [ $((boot_elapsed % 30)) -eq 0 ]; then
echo "⏳ Ожидание загрузки Android: $boot_elapsed секунд из $boot_timeout..."
fi
done
# Разблокировка экрана
adb shell input keyevent 82
adb shell input keyevent 82
sleep 2
# Установка APK
if [ ! -f "releases/debug/latest-debug.apk" ]; then
echo "❌ Error: latest-debug.apk not found in releases/debug/"
ls -la releases/debug/
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
adb install releases/debug/latest-debug.apk
if adb shell pm list packages | grep -q financialsuccess; then
echo "✅ APK успешно установлен"
else
echo "❌ Error: APK installation failed"
kill $EMULATOR_PID 2>/dev/null || true
exit 1
fi
# ... скриншоты будут сниматься следующим шагом ...
# Остановка эмулятора
kill $EMULATOR_PID 2>/dev/null || true
pkill -f emulator 2>/dev/null || true
- name: 📸 Run Maestro Screenshots (Tablet)
run: |
export PATH="$PATH":"$HOME/.maestro/bin"
mkdir -p screenshots/tablet
# Пробуем специфичную конфигурацию для планшета
local config_file="maestro/screenshots-tablet.yaml"
if [ ! -f "$config_file" ]; then
config_file="maestro/screenshots.yaml"
fi
if [ -f "$config_file" ]; then
echo "Using tablet screenshots configuration: $config_file"
maestro test "$config_file" --format junit --output screenshots/tablet/
echo "📱 Tablet screenshots completed!"
else
echo "⚠️ Warning: Maestro configuration file not found: $config_file"
echo "📱 Tablet screenshots skipped!"
fi
- name: 📤 Upload Tablet Screenshots
uses: actions/upload-artifact@v4
with:
name: tablet-screenshots-v${{ needs.build-and-test.outputs.version }}
path: screenshots/tablet/
retention-days: 30
# Финальная сборка релиза
create-release:
runs-on: ubuntu-latest
needs: [build-and-test, screenshots-phone, screenshots-tablet]
# Убрано условие should_release
permissions:
contents: write
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
- name: 📥 Download APK
uses: actions/download-artifact@v4
with:
name: FinancialSuccess-v${{ needs.build-and-test.outputs.version }}-${{ needs.build-and-test.outputs.date }}
- name: 📥 Download Phone Screenshots
uses: actions/download-artifact@v4
with:
name: phone-screenshots-v${{ needs.build-and-test.outputs.version }}
- name: 📥 Download Tablet Screenshots
uses: actions/download-artifact@v4
with:
name: tablet-screenshots-v${{ needs.build-and-test.outputs.version }}
- name: 📸 Prepare screenshots for repository
run: |
mkdir -p screenshots/phone screenshots/tablet
# Копируем скриншоты в правильные папки
if [ -d "screenshots/phone" ]; then
cp -r screenshots/phone/* screenshots/phone/ 2>/dev/null || true
fi
if [ -d "screenshots/tablet" ]; then
cp -r screenshots/tablet/* screenshots/tablet/ 2>/dev/null || true
fi
echo "📱 Phone screenshots:"
ls -la screenshots/phone/ 2>/dev/null || echo "No phone screenshots"
echo ""
echo "📱 Tablet screenshots:"
ls -la screenshots/tablet/ 2>/dev/null || echo "No tablet screenshots"
- name: 📝 Commit Screenshots to Repository
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add screenshots/
if git diff --staged --quiet; then
echo "No screenshots to commit"
else
git commit -m "📸 Auto-generated screenshots v${{ needs.build-and-test.outputs.version }} - ${{ needs.build-and-test.outputs.date }}
📱 Screenshots:
- Phone: $(ls screenshots/phone/*.png 2>/dev/null | wc -l) files
- Tablet: $(ls screenshots/tablet/*.png 2>/dev/null | wc -l) files
- Total: $(find screenshots/ -name "*.png" 2>/dev/null | wc -l) files"
git push
fi
- name: 🏷️ Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ needs.build-and-test.outputs.version }}-${{ needs.build-and-test.outputs.date }}
name: "🎮 Financial Success v${{ needs.build-and-test.outputs.version }}"
body_path: RELEASE_NOTES.md
files: |
releases/debug/*.apk
screenshots/phone/*.png
screenshots/tablet/*.png
draft: false
prerelease: false
generate_release_notes: false
- name: 📊 Build Summary
run: |
echo "🎉 Release completed successfully!"
echo ""
echo "� Results:"
echo "�� Version: ${{ needs.build-and-test.outputs.version }}"
echo "🔢 Version Code: ${{ needs.build-and-test.outputs.version_code }}"
echo "📅 Date: ${{ needs.build-and-test.outputs.date }}"
echo "� APK: releases/debug/FinancialSuccess-v${{ needs.build-and-test.outputs.version }}-${{ needs.build-and-test.outputs.date }}-debug.apk"
echo "📸 Phone screenshots: screenshots/phone/"
echo "📸 Tablet screenshots: screenshots/tablet/"
echo ""
echo "� Files:"
ls -la releases/debug/ || echo "No APK files found"
echo ""
echo "📸 Phone screenshots:"
ls -la screenshots/phone/ 2>/dev/null || echo "No phone screenshots"
echo ""
echo "📸 Tablet screenshots:"
ls -la screenshots/tablet/ 2>/dev/null || echo "No tablet screenshots"
Извините, за длинный скрипт, но он так хорош!
У Actions есть множество замечательных особенностей. Во-первых, агенты, да и любой LLM, прекрасно их понимают и умеют писать. Во-вторых, есть целый магазин уже готовых Actions И, кстати, там не только Actions. Например, в нем есть интеграции с трекером Jira, мессенджерами и различными ИИ.

Как пример, в моем листинге используется step checkout@v4, который взят отсюда.
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
Он клонирует код в облако github workspaces и далее wokflow может его собирать, переписывать, запускать, делать практически всё, на что хватит вашей безграничной фантазии.
В-третьих, у Actions есть замечательный информативный дашбоард с артефактами, которым можно пользоваться с мобильного телефона.


Github CLI
Мы получили автоматическую сборку, но получили еще одну точку потенциального сбоя. Теперь может ломаться не только приложение и агенты, но и CI/CD. И разумеется агенты будут заваливать папйплайн регулярно своими замечательными переделками, о которых вы их не просили.
В этот момент появилась идея написать агенту команду не включаться, пока сборка не станет зеленой. Но для этого надо дать возможность управлять Gothub actions. И такая возможность есть. Для этого мы будем использовать инструмент gothub CLI. Достаточно попросить агента установить его себе и далее он получает полную власть.
Например, пользователи вашего замечательного приложения могут заводить свои "хотелки" и баги через WebUI (пример)

А агент сможет обрабатывать их через github cli.
gh issue list
Showing 4 of 4 open issues in RobotAvi/MoneyGame
ID TITLE LABELS UPDATED
#35 При нажатии на Активы(вторая кнопка снизу) во время игры, ничего не происходит about 4 days ago
#33 При создании персонажа выбираю дату начала, но игра все равно начинается с 1 января 2024 about 4 days ago
#31 При создании персонажа не работает кнопка "Добавить цель" about 4 days ago
Он сможет запустить и далее вывести все запущенные workflow.
gh run list
STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE
X Merge branch 'main' o... 🚀 Stable Build &... main push 16317785712 1m53s about 44 minutes agoX Merge branch 'main' o... 🚀 Stable Build &... main push 16316849391 2m6s about 1 hour ago
X Merge branch 'main' o... 🚀 Stable Build &... main push 16316340343 5m17s about 1 hour ago
X Merge branch 'main' o... 🚀 Stable Build &... main push 16315742627 2m37s about 2 hours ago
X Merge branch 'main' o... 🚀 Stable Build &... main push 16315344190 10m11s about 2 hours ago
X 🔧 Update Android emu... 🚀 Stable Build &... main push 16315013756 10m42s about 2 hours ago
X 🔧 Update APK install... 🚀 Stable Build &... main push 16314726566 4m5s about 3 hours ago
X 🚀 Stable Build & Rel... 🚀 Stable Build &... main workflow_dispatch 16314541927 6m33s about 3 hours ago
X 🚀 Stable Build & Rel... 🚀 Stable Build &... main workflow_dispatch 16314338809 4m39s about 3 hours ago
X Merge pull request #5... 🚀 Stable Build &... main push 16313315708 5m15s about 4 hours ago
X Merge pull request #6... 🚀 Stable Build &... main push 16313305267 2m8s about 4 hours ago
X Исправить видимость т... 🚀 Stable Build &... cursor/bc-74e564d... pull_request 16313302064 6m30s about 4 hours ago
X Добавить зарплату, до... 🚀 Stable Build &... cursor/bc-20aef14... pull_request 16313293278 4m58s about 4 hours ago
X Update merge and rele... 🚀 Stable Build &... main push 16313285574 1m44s about 4 hours ago
X Merge pull request #5... 🚀 Stable Build &... main push 16278741446 5m32s about 1 day ago
X Merge pull request #5... 🚀 Stable Build &... main push 16278735173 2m19s about 1 day ago
X Fix game start date d... 🚀 Stable Build &... cursor/fix-game-s... pull_request 16278689493 4m36s about 1 day ago
X Fix skill selection n... 🚀 Stable Build &... cursor/fix-skill-... pull_request 16278669631 5m31s about 1 day ago
X Merge pull request #5... 🚀 Stable Build &... main push 16278054384 8m13s about 1 day ago
X Fix ignored game star... 🚀 Stable Build &... cursor/fix-ignore... pull_request 16277568754 5m14s about 1 day ago
Посмотреть внутрь исполняемого Workflow.
gh run view 16317785712
X main 🚀 Stable Build & Release · 16317785712
Triggered via push about 46 minutes ago
JOBS
✓ build-and-test in 1m30s (ID 46087758362)
X screenshots-tablet in 9s (ID 46087864204)
✓ Set up job
✓ 📥 Checkout code
✓ 📥 Download APK
X 🔧 Install Android SDK and platform-tools
- 🔍 Проверка наличия adb в PATH (tablet)
- 🎭 Install Maestro
- 📱 Start Android Emulator and Install APK (Tablet)
- 📸 Run Maestro Screenshots (Tablet)
- 📤 Upload Tablet Screenshots
✓ Post 📥 Checkout code
✓ Complete job
X screenshots-phone in 17s (ID 46087864215)
✓ Set up job
✓ 📥 Checkout code
✓ 📥 Download APK
X 🔧 Install Android SDK and platform-tools
- 🔍 Проверка наличия adb в PATH (phone)
- 🎭 Install Maestro
- 📱 Start Android Emulator and Install APK (Phone)
- 📸 Run Maestro Screenshots (Phone)
- 📤 Upload Phone Screenshots
✓ Post 📥 Checkout code
✓ Complete job
- create-release in 0s (ID 46087884918)
ANNOTATIONS
X Process completed with exit code 127.
screenshots-tablet: .github#29
X Process completed with exit code 127.
screenshots-phone: .github#29
ARTIFACTS
FinancialSuccess-v1.67-2025-07-16_11-07
To see what failed, try: gh run view 16317785712 --log-failed
View this run on GitHub: https://github.com/RobotAvi/MoneyGame/actions/runs/16317785712
Заглянуть в конкретную Job'у внутри workflow.
gh run view 16317785712 --job 46087864204
! both run and job IDs specified; ignoring run ID
X main 🚀 Stable Build & Release · 16317785712
Triggered via push about 48 minutes ago
X screenshots-tablet in 9s (ID 46087864204)
✓ Set up job
✓ 📥 Checkout code
✓ 📥 Download APK
X 🔧 Install Android SDK and platform-tools
- 🔍 Проверка наличия adb в PATH (tablet)
- 🎭 Install Maestro
- 📱 Start Android Emulator and Install APK (Tablet)
- 📸 Run Maestro Screenshots (Tablet)
- 📤 Upload Tablet Screenshots
✓ Post 📥 Checkout code
✓ Complete job
ANNOTATIONS
X Process completed with exit code 127.
screenshots-tablet: .github#29
To see the logs for the failed steps, try: gh run view --log-failed --job=46087864204
View this run on GitHub: https://github.com/RobotAvi/MoneyGame/actions/runs/16317785712
И увидеть логи.
gh run view 16317785712 --job 46087864204 --log
! both run and job IDs specified; ignoring run ID
screenshots-tablet Set up job 2025-07-16T11:07:56.9908742Z Current runner version: '2.326.0'
screenshots-tablet Set up job 2025-07-16T11:07:56.9941910Z ##[group]Runner Image Provisioner
screenshots-tablet Set up job 2025-07-16T11:07:56.9943260Z Hosted Compute Agent
screenshots-tablet Set up job 2025-07-16T11:07:56.9944128Z Version: 20250711.363
screenshots-tablet Set up job 2025-07-16T11:07:56.9945088Z Commit: 6785254374ce925a23743850c1cb91912ce5c14c
screenshots-tablet Set up job 2025-07-16T11:07:56.9946593Z Build Date: 2025-07-11T20:04:25Z
screenshots-tablet Set up job 2025-07-16T11:07:56.9947630Z ##[endgroup]
screenshots-tablet Set up job 2025-07-16T11:07:56.9948542Z ##[group]Operating System
screenshots-tablet Set up job 2025-07-16T11:07:56.9949596Z Ubuntu
...
screenshots-tablet 🔧 Install Android SDK and platform-tools 2025-07-16T11:08:03.4199712Z ##[error]Process completed with exit code 127.
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.4377242Z Post job cleanup.
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5297561Z [command]/usr/bin/git version
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5332105Z git version 2.50.1
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5375287Z Temporarily overriding HOME='/home/runner/work/_temp/e0586d2a-154f-42d8-8f82-3c3463313645' before making global git config changes
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5376830Z Adding repository directory to the temporary git global config as a safe directory
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5381847Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/MoneyGame/MoneyGame
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5416858Z [command]/usr/bin/git config --local --name-only --get-regexp
core\.sshCommand
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5448838Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5672398Z [command]/usr/bin/git config --local --name-only --get-regexp
http\.https\:\/\/github\.com\/\.extraheader
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5693891Z http.https://github.com/.extraheader
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5705901Z [command]/usr/bin/git config --local --unset-all http.https://github.com/.extraheader
screenshots-tablet Post 📥 Checkout code 2025-07-16T11:08:03.5735076Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || :"
screenshots-tablet Complete job 2025-07-16T11:08:03.6053273Z Cleaning up orphan processes
Теперь, мы можем написать агенту не подключаться, пока сборка не станет зеленой. И это даже сработает в 8 из 10 случаев. Но, конечно, же придется иногда поработать самим. Для этого нам потребуется IDE, которая тоже есть в Github.
Github CodeSpaces
Мы посмотрели агентов Cursor и собрали на нем приложение. Запустили CI/CD на actions и дали агентам инструмент отслеживания через github cli. И всё это с мобильного телефона, в перерывах на кофе.
Но вайбкодинг когда-то заканчивается, агенты начинают плохо работать. Приходиться нырять в код и звать на помощь CodeSpaces.

CodeSpaces запускается из второй вкладки, которая выпадает при нажатии на Code.
CodeSpaces - это IDE с терминалом и Github Copilot (бесплатный Copilot быстро кончается, но в маркете есть много альтернатив).


Увы, но пользоваться CodeSpace с телефона практически невозможно.
Так что ноутбуки нам еще понадобятся. Легкого кодинга!