Деплой React проекта на web хостинг без Node.js

13.03.2021

Пару месяцев назад я задумался об автоматическом развертывании проекта при обновлении репозитория с этим проектом на gitlab. В тот момент коллега работал над одним из важных проектов компании, и так уж получилось что проект на React, который требует компиляции при помощи node.js, который найдешь не на каждом web хостинге. Но теперь у меня тоже есть проект на React и мне не нрваится руками запускать компиляция и заливать по ftp.

У читателя может возникнуть вопрос: а почему бы не сделать все по нормальному, к чему все эти ограничения в виде web хостинга и отсутсвия машины для компиляции? Ответ прост: нужно было сделать здесь и сейчас, на том что есть и это не обсуждалось :)

Схема с использованием удаленного build репозитория была сделана как практическая хотелка для скилла, возможно более грамотный подход описан здесь же в главе "Вариант с архивом", он требует меньше, но делает столько же.

Условия

Исходные данные:

  • свой gitlab хостинг куда разрабы могут ходить только как пользователи, никаких ssh
  • проект на React с частыми апдейтами (компиляция по ~20 минут) и заливками по ftp
  • web хостинг на reg.ru
В наших условиях нужна компиляция проекта React, для этого нужен node.js, доступа к git серверу по ssh не имеем, значит нам нужно скомпилировать проект на локальной машине, в автоматическом режиме при git push.

Коротко о предполагаемой схеме - реагировать на обновление репозитория и:

  • компилировать исходники React проекта на node.js
  • удалять с хостинга старые данные
  • заливать на хостинг новые скомпилированные данные

Решение

git hooks

Внезпно обнаруживаем git хуки и читаем вывод команды терминала git help hooks. Стоит заметить что:

git хуки бывают клиентскими (выполняются на машине разработчика) и серверными (выполняются на сервере).

Реализация клиентских хуков расположена по адресу .git/hooks относительно репозитория. Здесь представлены не все возможные хуки, а лишь часть. Чтобы пример хука заработал достаточно из имени файла удалить точку с расширением (.sample).

Судя по git help hooks наиболе подходящим вариантом для реагирования на обновление репозитория, а точнее на предстоящее обновление, является хук pre-push, который срабатывает непосредственно перед отправкой данных на удаленный репозиторий.

Серверные хуки в контексте gitlab называеются WebHooks. Кратко: при поступлении какого-либо события (например обновление репозитория) в проект, может быть отправлен http запрос по любому указанному в настройках адресу. Это дает возможность отслеживать определенные события с проектом и реагировать на них.

Возможности web хостинга

Хостинг на reg.ru, самый дешевый. Оказывается даже на web хостинге можно зайти на сервер по ssh. Кроме того, на хостинге есть git, значит с ним можно будет работать из php, но нет node.js, что ожидаемо.

Схема деплоя

На основании рассмотренного можно сделать такое решение:

  • реализовать хук pre-push, который запустит компиляцию React проекта, а по итогу компиляции инициализирует новый репозиторий и с перезаписью отправит его в специальный удаленный репозиторий на gitlab, в котором будет хранится скомпилированный React проект
  • в этом специльном репозитории настроим WebHook, который вызовет скрипт на хостинге сайта и склонирует специальный репозиторий в нужное место, обновив тем самым сайт

Реализация

Доступ к репозиторию пока осуществляется по логину и паролю, поэтому в тексте ниже используется именно этот метод аутентификации.

Создадим репозиторий для хранения скомпилированного React проекта.

Создадим файл build.bash (в любом месте, но я создал его прямо в репозитории с исходниками) и поместим туда следующее:

#!/bin/sh
cd /var/www/html/my-app/
npm run-script build
cd /var/www/html/my-app/build/
git init
git add .
git commit -m 'build'
git push -f --set-upstream https://login:password@git.address.domain/my-app-build.git master

Здесь осуществляется переход в директорию React проекта и его компиляция, затем переход в директорию с билдом и создание локального репозитория. А в конце заливка билда в удаленный репозиторий с полной перезаписью всего содержимого репозитория.

Перейдем в директорию с хуками в репозитории (.git/hooks), который надо размернуть и создадим файл pre-push со следующим содержимым:

#!/bin/sh
nohup /var/www/html/my-app/build.bash &

nohup в начале и & в конце команды нужны чтобы скрыть вывод терминала

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

И сделаем оба файла исполняемыми:

chmod 775 /var/www/html/my-app/build.bash /var/www/html/my-app/.git/hooks/pre-push

Теперь создадим WebHook у проекта на gitlab, указав URL скрипта, секретный токен (по которому будем идентифицировать источник) и отметим только push:

Создание WebHook на gitlab
Создание WebHook на gitlab

Затем на web хостинге сайта в корневой директории создадим php скрипт __deploy-client.php куда поместим следующий код:

//запись информации о последнем запросе в тестовый файл
file_put_contents(dirname(__FILE__)."/".basename(__FILE__, ".php").".txt", print_r($_SERVER, true));

//проверка что запрос пришел с нужного git хостинга и что токен указан верный
if($_SERVER["REMOTE_ADDR"] != "xxx.xxx.xxx.xxx" || $_SERVER["HTTP_X_GITLAB_TOKEN"] != "sdf51s65dg1sd31fs3df1w6rg")
	exit();
	
//если скрипт дошел до этого места значит можно разворачивать

//директория куда копируем
$sDir = dirname(__FILE__)."/client";

//удаление старой версии билда
shell_exec("rm -rf $sDir/* $sDir/.[^.]*");

//клонирование репозитория с билдом в нужную директорию
shell_exec("git clone https://login:password@git.address.domain/my-app-build.git $sDir/");

Вариант с архивом

Суть такова: заменяем специальный репозиторий с билдом на архив.

Немного меняем build.bash:

#!/bin/sh
cd /var/www/html/tilda.local/htdocs/my-app/
npm run-script build
cd build
zip -r build.zip *
curl --data-binary @build.zip https://domain.zone/__deploy/client.php?token=token

В предпоследней строке создаем zip файл из содержимого директории build после компиляции. А на последней строке отправляем post запрос на целевой сайт где требуется развернуть новую версию интерфйса сайта.

Теперь немного поменяем скрипт деплоя на сайте:

//записываем инфу о последнем запросе в файл
file_put_contents(dirname(__FILE__)."/".basename(__FILE__, ".php").".txt", print_r($_SERVER, true).print_r($_FILES, true));

//сверка токенов
if($_GET["token"] != "token")
	exit();
	
//POST данные записываем в файл (там архив)
file_put_contents(dirname(__FILE__)."/build.zip", file_get_contents('php://input'));

//директория куда разворачиваем
$sDir = $_SERVER["DOCUMENT_ROOT"]."/client";

//удаляем все из директории
shell_exec("rm -rf $sDir/* $sDir/.[^.]*");

//распаковываем архив в директорию
shell_exec("unzip -d $sDir/ build.zip ");

При использовании архива для развертывания наличие git сервера (а значит и удаленного репозитория) необязательно, деплой можно повесить на хук post-commit

Итог

В итоге имеем автоматическое развертывание React проекта на сайт, размещенный на web хостинге. В данном случае компиляция осуществляется на машине разработчика.