Как я запустил маркетплейс опенсорс‑софта, используя GitHub вместо бэкенда и БД

—

от автора

Я давно люблю и слежу за миром Open Source. Но у Гитхаба есть огромная проблема: интересные, самобытные проекты часто теряются и не получают заслуженного хайпа, если у их создателя нет прокачанного Твиттера или связей. В то же время на главную страницу трендов регулярно выходят гиганты, за которыми стоят корпорации.Вторая боль — входной барьер для обычных людей. Обычный дизайнер, маркетолог или копирайтер, придя на GitHub за бесплатной программой, впадает в ступор. Где тут кнопка «Скачать»? Что такое ветки, коммиты и релизы? Куда нажимать, чтобы оно просто запустилось?

VibeMarket — витрина бесплатных open‑source приложений в духе App Store. В ней подборка бесплатных программ, рейтинг по звёздам GitHub и кнопка «Скачать», которая отдаёт установщик под вашу систему. Есть песочница, куда любой разработчик может подать своё приложение. Своего сервера, базы данных и модераторов у проекта нет.

Всю работу бэкенда берёт на себя GitHub: Pages раздаёт сайт, Issues служат формой заявок, реакции 👍 под заявкой работают как голосование, а робот в Actions раз в час пересобирает каталог. По коду это выглядит так: один index.html примерно на 3400 строк, робот на 570 строк, модуль разбора релизов на 100 строк и необязательный сервер для входа через GitHub на 400 строк. Что получилось в итоге: четыре языка интерфейса — русский, английский, испанский и китайский; страница сама понимает, нужен вам.dmg,.msi или.AppImage, и для Apple Silicon или Intel; есть песочница со сторонними приложениями, где заявку подаёт сам автор, сообщество голосует, а робот публикует; плюс жалобы, автоматическое скрытие и аварийный стоп‑кран.

Интерфейс маркета

Интерфейс маркета

Витрина: один HTML‑файл без сборки

Фронтенд — это один index.html: вёрстка, стили и скрипт в одном файле, без фреймворка и без сборщика. Pages раздаёт файл как есть, поэтому CI для фронтенда не нужен. Любой может открыть исходник страницы и убедиться, что трекеров нет. И нет цепочки поставок, которую можно скомпрометировать: никаких node_modules.Каталог встроен в страницу как снимок. При загрузке страница запрашивает catalog.json со свежими звёздами, песочницей и файлами для скачивания. Если файла нет, например при открытии с диска, всё продолжает работать на снимке. Тексты интерфейса на русском встроены в страницу. Остальные языки лежат в locales/*.json и подгружаются при переключении. Отдельный скрипт check-locales.mjs на каждом деплое сверяет, что во всех языках одинаковые ключи и плейсхолдеры. Иначе забытая строка всплыла бы только у пользователя.

База данных — это JSON‑файл, а пишет в него робот

Робот — это scripts/robot.mjs, обычный скрипт на Node 24 без зависимостей. GitHub Actions запускает его раз в час по расписанию, на каждое событие в Issues (новая заявка, правка, метка) и вручную. За проход робот проверяет заявки, считает голоса, публикует и снимает приложения, обновляет звёзды и файлы для скачивания. Результат он записывает в catalog.json и коммитит, только если что‑то изменилось. Получилась забавная деталь: git log catalog.json — это полный журнал изменений «базы данных» с датами и диффами. Откатить неудачное изменение можно обычным git revert. Проход робота занимает несколько секунд, вся пробежка вместе с публикацией сайта — меньше минуты.

Подводные камени коммит робота не запускает деплой

Первая версия была логичной: робот коммитит catalog.json, push в main запускает workflow публикации, сайт обновляется. Сайт не обновлялся. Оказалось, что события от GITHUB_TOKEN не запускают новые workflow: GitHub так защищается от бесконечных циклов. В публичных репозиториях GitHub отключает cron‑расписание после 60 дней без активности. Для «автономного» проекта это неприятно. Спасает то, что робот коммитит catalog.json, когда меняются звёзды, а у 49 популярных проектов они меняются каждый час. Пока каталог живой, активность в репозитории есть всегда. На сайте есть обычная форма «Добавить приложение» с предпросмотром карточки. Кнопка «Отправить» ничего никуда не шлёт. Она проверяет поля и показывает ссылку на новую задачу на GitHub, где форма уже заполнена.

 Форма «Добавить приложение»

Форма «Добавить приложение»

Где хранить состояние, если нет базы?

Роботу нужно помнить, когда началось голосование и в каком статусе заявка. Базы нет, а catalog.json для этого не годится: там только то, что видит сайт. Решение — HTML‑комментарий внутри комментария робота к заявке. Тут же обнаружилась дыра. Любой может оставить под заявкой комментарий с таким же маркером и датой начала голосования месячной давности. Тогда голосование закончится мгновенно. Поэтому робот доверяет только комментариям от github-actions[bot]: подделать автора комментария через API нельзя. Если голосов хватило, приложение попадает в песочницу с пометкой «Стороннее», а заявка закрывается. Через 90 дней приложение с 500 звёздами и без жалоб переезжает в обычную категорию на главной.

Самое сложное: кнопка «Скачать»

Сначала кнопка GET, как в App Store, только проигрывала анимацию «Installing…» и вела на страницу релизов. Для маркета, смысл которого в упрощении, это была главная фальшь, и её хотелось убрать. Браузер не умеет устанавливать программы, но может скачать установщик. Значит, надо понять, какой из файлов релиза подходит посетителю.

Правила пришлось отлаживать на реальных релизах проектов. Вот что всплыло:

  • darwin содержит win. Если искать Windows раньше macOS, файл lazygit_darwin_arm64.tar.gz уезжает в Windows. Порядок проверок важен.

  • Самый свежий релиз — не последняя версия. У Godot самым новым по дате оказался служебный выпуск старой ветки 3.6, а не актуальная 4.7. Теперь первым берётся релиз с отметкой isLatest, остальные — по дате.

  • GraphQL отдаёт не больше 100 файлов. У VSCodium их 180, и сборки для Windows просто не попадали в выборку. Для таких релизов робот дочитывает файлы через REST с пагинацией.

  • Монорепозитории. У Mullvad последний релиз содержит только приложение для Android, а версии для компьютеров лежат в предыдущем. Поэтому релиз выбирается отдельно для каждой системы: самый свежий, где есть файлы для неё. Bitwarden выпускает из одного репозитория CLI, десктоп и веб, поэтому для него в rules.json задан фильтр тегов ^desktop-v.

  • Особые сборки. У Ollama есть сборки под ROCm и Jetson, у Godot — сборки Mono, у OBS — отладочные dSYM, у HandBrake — консольный HandBrakeCLI.dmg. Узкие сборки показываются, только если обычной для этого процессора нет. Отладочные и консольные отбрасываются всегда.

  • Не всё, что лежит в релизах, — приложение. У Tauri в релизах консольная утилита фреймворка, у ComfyUI — portable‑сборка только для видеокарт NVIDIA. Такие репозитории перечислены в downloads.skip, и кнопка ведёт на сайт проекта.

Итог прохода: 5 GraphQL‑запросов по 1 очку лимита и дочитывание VSCodium через REST. Вместе со звёздами и заявками это 11 запросов к API на весь проход. В catalog.json попадают 288 файлов, это 131 КБ, или 21 КБ после gzip.

Выводы

Свои выводы можете сделать самостоятельно, но главный вывод для меня такой: для каталога, который меняется раз в час и где каждое изменение должно быть прозрачным, GitHub закрывает почти весь бэкенд — хранилище с историей, очередь задач, аутентификацию и права доступа, а также формы, голосование, хостинг и CDN.Свой код нужен только на склейку и на правила.

Если у вас есть бесплатное приложение с открытым кодом:

  1. Добавьте репозиторию тему vibemarket.

  2. Выпустите релиз.

  3. Подайте заявку через форму на сайте.

Github проекта с кодом:
https://github.com/NickGBar/VibeMarket

ссылка на оригинал статьи https://habr.com/ru/articles/1101156/