GitHub Actions для аналитика: автоматизация без очереди к разработке
Кратко
Аналитик редко владеет инфраструктурой: нет сервера, нет kube-кластера, нет прав на прод-окружение. Зато есть репозиторий с кодом и привычка запускать скрипты руками, когда нужно получить цифру. Ручной запуск — это не пайплайн, а ритуал: забыл запустить — отчёта нет, ноутбук выключен — данных нет, разработчики заняты — очередь на CI висит неделю.
GitHub Actions закрывает этот зазор без бюджета и без очереди к разработке. Это бесплатный раннер, который запускает ваш Python по cron, кладёт результат в репозиторий, шлёт отчёт в Telegram и падает в лог, когда что-то сломалось. Для небольших аналитических задач — парсинг, ежедневный дайджест, регенерация датасета, прогон тестов на метрики — этого достаточно. В этой заметке разбираю, как устроить такой workflow от YAML до рабочего скрипта, не потратив ни минуты админского времени.
Почему именно GitHub Actions
Когда задача «запускать скрипт по расписанию» встаёт впервые, у аналитика обычно три варианта:
- Локальный cron. Работает, но машина должна быть включена. Отчёт в 7 утра — значит, ноутбук не спит. Сразу нарушает правило «не запускать руками».
- Свой сервер или облако. Требует денег и настроек. Аналитик — не админ: поднимать VPS, настраивать supervisor, следить за сертификатами — это чужой профиль. Google Cloud Scheduler решает задачу, но раскидывает проект по нескольким сервисам, когда все файлы уже лежат в git.
- GitHub Actions. Раннер поднимается сам, код уже в репозитории, расписание задаётся одной строкой cron. Бесплатно для приватных репозиториев: 2 000 минут в месяц и 500 МБ под артефакты. Для аналитического скрипта, который крутится пару минут в день, лимит практически неощутим — мой пет-проект за 20 дней потратил 133 минуты.
Триггеры бывают трёх видов:
- Расписание — cron в UTC, например
0 7 * * *для запуска каждый день в 07:00. - Событие —
push,pull_request, релиз. Удобно для прогона тестов на метрики при каждом изменении кода. - Вручную —
workflow_dispatch, кнопка в интерфейсе для разовых запусков.
Для аналитика это три разных сценария: ежедневный дайджест по cron, regression-тесты на каждый PR и ручной прогон ad-hoc без открытия ноутбука локально.
Архитектура минимального проекта
Репозиторий для автоматизированного аналитического скрипта выглядит так:
analytics-bot
│
├── .github
│ └── workflows
│ └── run_daily.yml
│
├── .gitignore
├── requirements.txt
├── parser.py
├── cleanup.py
├── parsed_data.csv
└── README.md
Три роли файлов:
run_daily.yml— описание workflow: когда запускать, что ставить, какие скрипты дёргать, куда коммитить результат.parser.py— основной скрипт: собирает данные, считает метрики, отправляет отчёт в Telegram, обновляетparsed_data.csv.cleanup.py— служебный скрипт: удаляет строки старше трёх месяцев, чтобы CSV не разрастался.
Один YAML — один сценарий. Когда задач становится несколько, заводите отдельные workflow-файлы, а не валите всё в один job. Это позволяет параллелить выполнение и не смешивать логику: ежедневный парсинг и еженедельная очистка данных — разные файлы.
Рабочий workflow: YAML от и до
Минимальный run_daily.yml, который запускается по cron в 07:00 UTC, ставит зависимости, гоняет парсер, коммитит обновлённый CSV и подчищает старые данные:
name: Daily analytics bot
on:
schedule:
- cron: '0 7 * * *'
workflow_dispatch: {}
env:
BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
CHANNEL_ID: ${{ secrets.TELEGRAM_CHANNEL }}
jobs:
daily-run:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install -r requirements.txt
- name: Run parser
run: python parser.py
- name: Commit CSV if changed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add parsed_data.csv
git diff --cached --quiet || git commit -m "Update CSV with latest data"
git push
- name: Delete old data
run: python cleanup.py
Ключевые моменты:
- Cron в UTC. Если вы в другом часовом поясе, учитывайте смещение.
0 7 * * *— это 07:00 UTC, в Москве это 10:00. workflow_dispatchдобавляет кнопку ручного запуска в интерфейсе Actions. Полезно для отладки и ad-hoc прогонов.GITHUB_TOKEN— встроенный токен для коммита и пуша в тот же репозиторий. Отдельно его создавать не нужно, GitHub выдаёт автоматически.actions/setup-python@v5— фиксированная версия Python. Без этого шага раннер использует системный интерпретатор, версию которого вы не контролируете.git diff --cached --quiet || git commit— коммитит только если есть изменения. Пустой коммит не создаётся, история не засоряется.
Для триггера на push в main добавляется отдельный блок:
on:
schedule:
- cron: '0 7 * * *'
push:
branches:
- main
workflow_dispatch: {}
Это позволяет гонять regression-тесты при каждом изменении кода, а не только по расписанию. Для аналитика, который правит расчёт метрики, это страховка: сломанный инвариант остановит билд до merge.
Скрипт аналитика: парсинг, расчёт, отчёт
Python-часть остаётся обычным скриптом. GitHub Actions не диктует структуру кода — он просто запускает то, что вы написали. Секреты из YAML приходят как переменные окружения, их читает os.getenv.
import os
import requests
import pandas as pd
BOT_TOKEN = os.getenv("BOT_TOKEN")
CHANNEL_ID = os.getenv("CHANNEL_ID")
def collect_data() -> pd.DataFrame:
# здесь ваш парсинг: API, БД, скрапинг
# возвращаем DataFrame с дневными метриками
return pd.DataFrame({
"date": ["2026-08-27"],
"visits": [1240],
"signups": [38],
"conversion": [0.0306],
})
def build_report(df: pd.DataFrame) -> str:
row = df.iloc[-1]
return (
f"Дайджест за {row['date']}\n"
f"Визиты: {row['visits']}\n"
f"Регистрации: {row['signups']}\n"
f"Конверсия: {row['conversion']:.1%}"
)
def send_to_telegram(text: str) -> None:
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
requests.post(url, data={
"chat_id": CHANNEL_ID,
"text": text,
"parse_mode": "HTML",
})
if __name__ == "__main__":
df = collect_data()
df.to_csv("parsed_data.csv", index=False, mode="a", header=False)
send_to_telegram(build_report(df))
Скрипт остаётся тестируемым локально: переменные окружения поднимаются через .env и python-dotenv, а в CI приходят уже из secrets. Это значит, что отладку можно вести на машине, не расходуя лимит минут раннера.
Зависимости фиксируются в requirements.txt:
pip freeze > requirements.txt
Файл попадает в репозиторий, и шаг python -m pip install -r requirements.txt ставит именно те версии, с которыми скрипт работал локально. Без этого раннер однажды утром упадёт, потому что вышла новая мажорная версия библиотеки.
Secrets: токены без утечки
Токен Telegram-бота, ключ API, пароль к БД — всё это нельзя класть в код. Секреты хранятся в настройках репозитория: Settings → Secrets and variables → Actions → New repository secret. После создания значение больше нигде не видно — только имя.
В workflow секрет подставляется как ${{ secrets.TELEGRAM_TOKEN }}, в Python приходит через os.getenv("BOT_TOKEN"). Файл .env должен быть в .gitignore:
.env
*.csv
*.log
CSV тоже стоит игнорировать, если он генерируется раннером и коммитится автоматически. Это страхует от случайного коммита локального файла с конфиденциальными данными.
Важно: секреты не передаются в fork-репозитории и не видны в pull request из форка. Это защита от утечки через PR, но это же означает, что workflow, зависящий от секрета, нельзя полноценно прогнать из чужого форка.
Кэширование, артефакты и триггеры
Когда скрипт тяжёлый — ставит много зависимостей или строит модель — на первый план выходят кэширование и артефакты.
Кэш зависимостей. actions/setup-python@v5 умеет кэшировать pip по умолчанию. Если хочется точнее, используйте actions/cache@v4:
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
Кэч попадает по ключу, который зависит от хэша requirements.txt. Меняется состав зависимостей — кэш инвалидируется, иначе переиспользуется. Для проекта с pandas, scikit-learn и statsmodels это режет время установки с минут до секунд.
Артефакты. Результат прогона — CSV, PNG с графиком, HTML-отчёт — можно загрузить как артефакт:
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: daily-report
path: report.html
retention-days: 14
Артефакт живёт 14 дней и доступен через интерфейс Actions. Удобно для отладки: прогон упал, вы открываете вкладку, скачиваете отчёт, смотрите, что именно скрипт сгенерировал перед падением. Бесплатный тариф даёт 500 МБ под артефакты — для HTML и CSV этого много.
Триггеры на push и PR. Для regression-тестов на метрики — отдельный workflow:
name: Metrics regression tests
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: python -m pip install -r requirements.txt
- run: python -m pytest tests/test_metrics.py -v
Этот workflow не зависит от secrets и спокойно гоняется из форка. Изменение расчёта конверсии, ломающее инвариант «сумма по сегментам = итог», остановит PR до merge. Подробнее про regression-тесты на инварианты — в заметке про воспроизводимые пайплайны данных.
Ограничения и где заканчивается бесплатный тариф
Бесплатный тариф GitHub Free для приватных репозиториев: 2 000 минут в месяц и 500 МБ под артефакты. Для аналитических скриптов это много. Мой пример с ежедневным парсингом потратил 133 минуты за 20 дней — месячный лимит уходит с огромным запасом.
Где лимит реально начинает давить:
- Тяжёлые прогоны. Если скрипт крутит ML-модель по часу каждый день, 2 000 минут исчерпываются за месяц. Это сигнал, что задача переросла free-тариф и пора думать про отдельный раннер или сервер.
- macOS-раннеры. Расход минут по тарифу в 10 раз больше, чем на Linux. Если вам не нужен macOS специально — берите
ubuntu-latest. - Публичные репозитории. Для них лимитов нет, но данные и секреты будут видны. Аналитические пайплайны обычно держат приватными.
- Время одного workflow. Один job не может выполняться дольше 6 часов. Для аналитика это почти никогда не проблема, но знать стоит.
Журнал выполнений и ошибки смотрятся через вкладку Actions → Jobs. Каждый шаг отдельный лог, print в Python виден в выводе — это и есть основной канал отладки. Если скрипт упал, вы открываете упавший step, читаете traceback, правите код, пушите, раннер поднимается снова.
На практике
Минимальная связка для аналитика: parser.py + run_daily.yml + секреты в репозитории + .gitignore. Этого достаточно, чтобы ежедневный дайджест приходил в Telegram без поднятого ноутбука и без разработчиков. Когда к парсингу добавляется генерация отчёта в HTML и отправка в канал — это отдельный шаг в том же workflow, не новый сервис.
В Telegram-боте для отчётности этот паттерн работает в production: скрипт собирает метрики, формирует сообщение, шлёт в чат. В SQL Analytics Case Study тот же подход применяется для regression-тестов на SQL-инварианты — прогоны на каждый PR, без ручного запуска.
Подсказки
- Начинайте с одного шага по cron. Запустите workflow с одним
echoи однимpython --version, убедитесь, что раннер поднимается. Потом добавляйте свой скрипт. - Отрабатывайте скрипт локально. Лимит минут не бесконечный, а отладка через Actions медленная: каждый прогон — это минута-две на поднятие раннера.
- Храните токены только в Secrets. Коммит с утёкшим токеном — это отзыв ключа и новый выпуск, а не «просто неудобство».
- Логируйте ключевое через
print. В Actions это единственный канал видеть, что происходит внутри скрипта.print(f"rows={len(df)}")перед отправкой — страховка от пустого отчёта. - Разделяйте jobs. Если в проекте несколько задач — парсинг, отчёт, очистка данных — выносите их в разные workflows. Это позволяет параллелить выполнение и изолировать сбои.
- Следите за consumption. Вкладка Settings → Billing показывает, сколько минут потрачено за месяц. macOS-раннеры тратят в 10 раз больше минут, чем Linux.
Выводы
GitHub Actions для аналитика — это не про CI/CD в классическом смысле, а про автоматизацию без бюджета. Cron заменяет включённый ноутбук, secrets заменяют .env на сервере, артефакты заменяют сетевые шары, триггеры на push заменяют ревью кода на глаз. Бесплатный тариф покрывает почти любой небольшой аналитический сценарий, а когда перестаёт — это сигнал, что задача выросла из пет-проекта.
Три вещи, которые стоит сделать в первую очередь: поднять минимальный workflow с одним cron-запуском, вынести все токены в secrets, добавить print на ключевые шаги скрипта. Дальше — по мере необходимости: кэширование для тяжёлых зависимостей, артефакты для отчётов, regression-тесты на каждый PR. Без очереди к разработке и без поднятого ноутбука.