Автоматизация блога на Hugo с помощью CircleCI

Рассказываю, как можно сделать свой сайт на двух языках, сгенерировать его с помощью Hugo, разместить на бесплатном хостинге GitHub Pages и сделать так, чтобы это все происходило автоматически благодаря CircleCI.

В прошлой заметке “Реинкарнация сайта” я рассказывал, как сейчас устроен мой сайт, но давайте все же углубимся в детали и я подробно расскажу, как можно автоматизировать процесс публикации заметок в блоге, когда у вас статический сайт.

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

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

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

Общая схема работы

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

Схема орагиназации блога на 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:

Рабочий процесс в CircleCI

В результате всех выше описанных манипуляций мы получаем статический сайт, генерируемый Hugo, который умеет сам собираться и публиковаться. Стоимость такого решения — бесплатно. Автору остается только писать качественный контент.

Полная рабочая конфигурация этого сайта доступна в репозитории на GitHub.

На чытанне спатрэбілася 9 хвілін Нататка змяшчае 1896 слоў

Сябры, акрамя блога, я амаль што рэгулярна пішу адмысловую рассылку пра праграмванне і тэхналогіі, дзе раз на двы тыдні збіраю самыя цікавыя навіны тэхналогій і раблю агляд цікавых інструментаў, якія мне трапіліся. Таму хутчэй падпісвайцеся, каб не прапусціць штосці цікавае наступным разам!