Автоматизация блога на Hugo с помощью CircleCI
В прошлой заметке “Реинкарнация сайта” я рассказывал, как сейчас устроен мой сайт, но давайте все же углубимся в детали и я подробно расскажу, как можно автоматизировать процесс публикации заметок в блоге, когда у вас статический сайт.
На первый взгляд статический сайт выглядит идеальным решением, потому как собирается относительно быстро, собой представляет набор файлов и не требователен к хостингу. Однако, работать с ним может быть не очень удобно, особенно если мы говорим о формате блога, где важно фокусироваться на содержимом, а не на его обслуживании.
В случае со статическим сайтом, мы сначала пишем текст, добавляем оформление, компилируем генератором и загружаем результат куда-то на хостинг. Если мы исправили ошибку в тексте, мы проделываем тоже самое. И так каждый раз, на каждое изменение. Согласитесь, непростительная трата времени. Это можно и нужно автоматизировать.
В моем случае все осложняется еще и тем, что у сайта два языка, для каждого языка своя версия сайта и свой адрес, что означает практически два отдельных хостинга, какими бы простыми они не были. Однако, когда рутинные операции автоматизированы, из недостатка этот факт превращается в преимущество.
Общая схема работы
Исходя из задач и сложностей, описанных выше, родилась следующая схема организации работы над сайтом.
Forestry
Этот сервис выступает в качестве системы управления и редактора для заметок. У него есть возможность настраивать шаблоны и добавлять в них поля для front matter, а для содержимого сервис предоставляет удобный WYSIWYG-редактор. Одним словом, это графический интерфейс сайта, который все данные берет из git-репозитория и туда же их сохраняет в виде комитов после сохранения. Я опишу, как я использую этот сервис, в отдельной заметке.
Тема и оформление
Theme — это отдельный репозиторий с оформлением сайта. Я вынес оформление в отдельный репозиторий, чтобы не смешивать историю изменений кода и содержимого. К тому же, выполнив определенные условия по оформлению, эту тему в последствии можно опубликовать в публичном каталоге тем для Hugo.
Этот репозиторий подключается к основному как подмодуль. Это важно, так как упрощает в последствии конфигурацию процесса сборки. В основном репозитории он описывается в файле .gitmodules в таком формате:
[submodule "themes/klimchuk"]
path = themes/klimchuk
url = git@github.com:Alroniks/hugo-theme-klimchuk.git
Исходный код и содержимое
На схеме Source. Это основной репозиторий сайта, где хранится содержимое сайта: заметки блога, описания мест, где был, проекты и другие страницы. На изменения именно в этом репозитории реагирует CircleCI и запускает сборку сайта.
После сборки сайт публикуется в отдельные репозитории на GitHub, где уже настроены GitHub Pages со своими доменами каждый.
Настройка CircleCI
CircleCI для работы предлагает две основные сущности: workflows и jobs. Вся работа происходит в задачах (jobs), а процесс (workflow) же описывает то, в каком порядке будут выполняться задачи, причем можно указывать зависимости между задачами.
Обработка самих задач происходит в Docker-контейнерах. Преимущество такого подхода в том, что уже существует множество Docker-образов (images), которые можно использовать, а если не нашлось, то такой образ можно сделать самому.
Вся конфигурация для CircleCI описывается в одном файле в формате YML, который должен быть размещен в репозитории по адресу .cirleci/config.yml
.
Самая базовая конфигурация выглядит вот так:
version: 2
workflows: # описание рабочих процессов
version: 2
deployment: # название процесса
jobs: # список задач, запускаемых в рамках процесса
- build
jobs: # описание самих задач непосредственно
compilation:
docker: # описывается конфигурация для Docker, какой образ использовать и прочие параметры
- image: cibuilds/hugo:latest
steps: # команды, которые будут запущены в рамках этой задачи
- checkout # команда, которая помещает в задачу исходники из репозитория, обязательная
- run: echo 'running' # произвольная команда
Я намеренно не поясняю в деталях, как добавить CircleCI к вашему репозиторию, потому что с этим прекрасно справляется документация.
Задачи
В моем случае, у меня используется два типа задач. Первый тип — это компиляция сайта и его базовая проверка с помощью htmlproofer. Второй тип — публикация. Задача публикации зависит от задачи компиляции, а это значит, что если компиляция не удалась, то ничего не будет опубликовано.
Задача compilation
Для задачи компиляции я использую уже готовый Docker-образ cibuilds/hugo:latest
.
В классической теории CI/CD использовать тег
latest
считается плохой практикой, так как обновление версии может внести несовместимые изменения и сборка окажется сломанной.
Я намеренно использую latest-версию, чтобы отслеживать изменения в движке Hugo и адаптировать свой сайт под эти изменений. В случае проблем сборка не случится и я получу уведомление и смогу это исправить. Но если для вас важнее обеспечить регулярность публикации, лучше использовать теги с указанием конкретных версий.
Теперь я расскажу про шаги, которые выполняются в рамках этой задачи. Первым делом необходимо установить в контейнере git, чтобы иметь возможность работы с git-модулями. Это делается командой apt-get update && apt-get install -y git
.
Следующим шагом я забираю код из основного репозитория. Для этого CircleCI предоставляет простую команду checkout
. Исходники сайта получены, но сборка невозможна без темы. Тема поставляется в виде git-модуля, а это значит, что в основном репозитории хранится только ссылка на комит из репозитория, указанного в качестве модуля и нужно явно обновить содержимое темы командой git submodule sync && git submodule update --init
.
Итак, весь код получен, можно собирать сайт и проверять результат. Сборка сайта делается простой командой HUGO_ENV=production hugo -v -d $HUGO_BUILD_DIR
, где $HUGO_BUILD_DIR
— это специальная переменная окружения, объявленная в разделе настроек environment
самой задачи и указывает, по какому пути будет храниться результат сборки. Сайт собирается на двух языках, каждый из которых собирается в свой каталог и проверить нужно оба, поэтому следующие два шага — это последовательная проверка русской и английской версий сайта с помощью htmlproofer.
И в качестве последнего шага используется специальная команда persist_to_workspace
, которая сохраняет данные компиляции для передачи в следующую задачу, так как по умолчанию, задачи в CircleCI атомарные и после выполнения полностью убирают за собой.
Итоговое описание задачи в файле конфигурации выглядит вот так:
compilation:
docker:
- image: cibuilds/hugo:latest
environment:
TZ: "Europe/Minsk"
working_directory: ~/hugo
environment:
HUGO_BUILD_DIR: public
steps:
- run: apt-get update && apt-get install -y git
- checkout
- run:
name: Update Theme
command: git submodule sync && git submodule update --init
- run:
name: Build the site
command: HUGO_ENV=production hugo -v -d $HUGO_BUILD_DIR
- run:
name: Test RU website
command: htmlproofer $HUGO_BUILD_DIR/ru --allow-hash-href --check-html --empty-alt-ignore --disable-external --check_favicon --check_opengraph
- run:
name: Test EN website
command: htmlproofer $HUGO_BUILD_DIR/en --allow-hash-href --check-html --empty-alt-ignore --disable-external --check_favicon --check_opengraph
- persist_to_workspace:
root: /root/hugo/public
paths: .
Задача публикации
Для своего сайта в качестве хостинга я использую GitHub Pages, а это значит, что для публикации содержимого мне достаточно отправить изменения в соответствующий репозиторий. Задача публикации описывает этот процесс.
Первые шаги такие же, как и в случае компиляции, установить git и загрузить код. Так как Hugo на этом этапе не нужен, я использую базовый образ cibuilds/base
для работы.
Чтобы иметь возможность отправлять изменения из CircleCI в git-репозитории, необходимо добавить SSH-ключ в настройках проекта в CircleCI. Важно, чтобы это был User Key, потому что Deploy Key дает доступ только к репозиторию с исходниками, а задача — публиковать изменения в другие. Кроме того, после создания ключа, в конфигруации нужно явно его указать. Указывается он в виде fingerprint-представления ключа, это значение можно найти как на GitHub, так и в настройках проекта в CircleCI. В конфигурации ключ указывается командой add_ssh_keys
.
В задаче компиляции последним шагом была команда, которая сохраняла результат работы задачи и для публикации нам нужен этот результат. Он загружается командой attach_workspace
.
GitHub Pages для привязки репозитория к домену требует специальный файл CNAME, в котором указывается доменное имя. Так как эти файлы нужны только в репозиториях с GitHub Pages, Hugo эти файлы не генерирует, поэтому нужно их создать перед публикацией. Делается это командой echo "klimchuk.by" > public/ru/CNAME
Сайт скомпилирован и готов к публикации. Дальше можно пойти двумя путями. Простой будет заключаться в том, чтобы внутри каталога с нужной языковой версией инициализировать новый git-репозиторий, указать для него нужный remote-репозиторий и отправить изменения в ветку master с флагом --force
. Минусы такого способа более чем очевидны. Не видна история изменений скомпилированного сайта, но более того, сборка происходит каждый раз, безусловно, даже когда на сайте не было изменений. Если менялась какая-либо конфигурация, которая не затрагивает содержимое, тогда скомпилированная версия не будет отличаться от предыдущей и в публикации нет смысла.
Поэтому стоит использовать более сложный, но правильный вариант. Шаги будут следующие: создать рабочий каталог, клонировать в неё репозиторий с уже опубликованным сайтом, скопировать новую версию сайта из предыдущей задачи, добавить в git изменения, зафиксировать их и отправить в репозиторий.
В последнем шаге, если нет изменений для комита, задача просто завершится. Для такой проверки можно использовать следующую конструкцию: git diff-index --quiet --cached HEAD -- && echo "No changes!" && exit 0 || echo $COMMIT_MESSAGE
.
Полное описание задачи выглядит так:
russian:
docker:
- image: cibuilds/base
steps:
- run: apk update && apk add git
- checkout
- add_ssh_keys:
fingerprints:
- "-:-:-"
- attach_workspace:
at: public
- run:
name: Set Up Custom Domain
command: echo "klimchuk.by" > public/ru/CNAME
- run:
name: Create Working Directory
command: mkdir publishing
- run:
name: Clone Resources
command: git clone git@github.com:Alroniks/compiled-klimchuk-russian.git --branch master --single-branch publishing
- run:
name: Copy Compilled Stuff
command: /bin/cp -R public/ru/* publishing
- run:
name: Set Git Config and Add Changes
command: |
cd publishing
git config credential.helper 'cache --timeout=120'
git config user.email "ci@klimchuk.com"
git config user.name "CircleCI Deployment Bot"
git add --all
- run:
name: Commit
command: |
cd publishing
COMMIT_MESSAGE="Update pages on $(date +'%Y-%m-%d %H:%M:%S')"
git diff-index --quiet --cached HEAD -- && echo "No changes!" && exit 0 || echo $COMMIT_MESSAGE
git commit -m "${COMMIT_MESSAGE}"
git push git@github.com:Alroniks/compiled-klimchuk-russian.git master
Для английского языка задача описывается аналогично, меняется только адрес репозитория назначения и доменное имя.
Процессы
Процессы позволяют описать логику и последовательность выполнения задач по развертыванию и сборке в рамках CI/CD. Это может быть удобно, если для разных событий в репозитории требуются различные сценарии сборки. Например, если был создан релиз, тогда запускается сборка артефактов и сборка приложения с его дальнейшей публикацией, а если это был новый pull request, то нужно запустить тесты.
Я использую два процесса, один для обычного развертывания, который реагирует на каждый комит в ветку master, а другой для автоматической публикации заметок, который запускается по расписанию.
Развертывание
Процесс развертывания описывается обычным упоминание задач, которые нужно запустить. Задачи публикации зависят от задачи компиляции и реагируют на изменения в ветке master. В конфигурации это выглядит вот так:
deployment:
jobs:
- english:
requires:
- compilation
filters:
branches:
only: master
- russian:
requires:
- compilation
filters:
branches:
only: master
- compilation
Зависимость указывается с помощью параметра requires
, в котором указываются задачи, завершения которых нужно дождаться.
Отложенная публикация
Очень приятным открытием стал тот факт, что CircleCI поддерживает запуск процесса CI/CD по расписанию. В случае блога, это очень полезная возможность, так как позволяет настроить автоматическую публикацию заметок. Работает это просто, у заметки датой публикации указывается дата в будущем, сам сайт генерируется раз в сутки в определенное время и заметки, дата публикации которых настала, автоматически появляются на сайте.
В CircleCI это настраивается достаточно просто путем указания расписания сборок в cronjob-формате. Но важно помнить, что интервалы вида */10
не поддерживаются.
В конфигурации такой процесс отличается тем, что дополнительно описывается секция triggers, где настраивается расписание. Так как тригеры описаны для всего процесса, в списке задач не нужно указывать фильтры для веток. Из недостатков, сборка всегда происходит по времени в зоне UTC.
autopublish:
triggers:
- schedule:
cron: "30 6 * * *" # every day at 6:30 am by UTC
filters:
branches:
only:
- master
jobs:
- english:
requires:
- compilation
- russian:
requires:
- compilation
- compilation
Все задачи в рамках одного процесса выполняются параллельно, что значительно может ускорить весь процесс сборки и публикации. Так выглядит успешно завершенный процесс в интерфейсе CircleCI:
В результате всех выше описанных манипуляций мы получаем статический сайт, генерируемый Hugo, который умеет сам собираться и публиковаться. Стоимость такого решения — бесплатно. Автору остается только писать качественный контент.
Полная рабочая конфигурация этого сайта доступна в репозитории на GitHub.
Сябры, акрамя блога, я амаль што рэгулярна пішу адмысловую рассылку пра праграмванне і тэхналогіі, дзе раз на двы тыдні збіраю самыя цікавыя навіны тэхналогій і раблю агляд цікавых інструментаў, якія мне трапіліся. Таму хутчэй падпісвайцеся, каб не прапусціць штосці цікавае наступным разам!