↓ Перейти к основному содержимому
  1. Posts/
  2. Self-Hosting/

ProHomelab: автоматизация публикации статей через Forgejo

·4948 слов·24 минут· loading · loading · ·
Stilicho2011
Автор
Stilicho2011
Пишу о homelab, self-hosting, автоматизации и open-source решениях
Оглавление
Автоматизация публикации ProHomelab - This article is part of a series.
Part : This Article

ProHomelab: автоматизация публикации статей - подробная инструкция
#

Эта заметка описывает, как настроить автопубликацию статей: вы пишете в Obsidian → статья сама уходит в Forgejo → это запускает сборку сайта на Hugo (тема Blowfish) → готовый сайт сам заливается на хостинг. Ручной hugo -D и FileZilla больше не нужны.

Note

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

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

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


Как это выглядит на схеме
#

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

Как устроена автопубликация статей

Коротко, своими словами, по человечески:

  • Вы сохраняете заметку в Obsidian.
  • Она сама улетает в репозиторий со статьями на Forgejo (это просто хранилище текста, ничего больше).
  • Этот репозиторий «будит» репозиторий сайта - говорит ему «появилось что-то новое, пора пересобрать сайт».
  • Раннер (фоновая программа на вашем сервере) видит этот сигнал, забирает свежие статьи, запускает Hugo и собирает из них готовые HTML-страницы.
  • Готовый сайт автоматически заливается на хостинг - именно то, что видят посетители.

Всё, что происходит между «сохранил в Obsidian» и «статья на сайте» - это и есть автоматизация, которую мы дальше настроим по шагам.


0. Термины, которые встретятся ниже
#

  • git - система контроля версий. Отслеживает изменения файлов и умеет их отправлять на сервер.
  • репозиторий (repo) - папка, за которой следит git.
  • remote - адрес удалённого сервера (в нашем случае Forgejo), куда репозиторий отправляет изменения.
  • commit - «снимок» изменений с комментарием, сохранённый в истории git.
  • push - отправка коммитов на remote-сервер.
  • pull / clone - скачивание репозитория (или его изменений) с сервера к себе.
  • submodule - репозиторий внутри репозитория (у вас так подключена тема Blowfish).
  • CI/CD, Forgejo Actions - механизм «если в репозитории что-то поменялось - автоматически выполни набор команд» (у GitHub это называется GitHub Actions, у Forgejo - Forgejo Actions).
  • раннер (runner) - программа, которая физически выполняет эти автоматические команды.
  • workflow - YAML-файл, описывающий, какие шаги и когда выполнять.
  • YAML - текстовый формат файлов конфигурации (отступы важны, табы использовать нельзя, только пробелы).
  • secrets - зашифрованные переменные (пароли, токены), которые Forgejo Actions подставляет в скрипт, не показывая их в логах.
  • токен доступа (Personal Access Token) - длинная строка-пароль для программного доступа к Forgejo вместо обычного пароля аккаунта.
  • SSH - протокол для подключения к серверу через терминал.
  • systemd-сервис - способ в Linux запускать программу в фоне и держать её постоянно включённой, даже после перезагрузки.

1. Архитектура
#

Два репозитория в Forgejo:

  1. prohomelab-content - только статьи (у меня это папка 01-Projects\Prohomelab в Obsidian). Без файлов темы Hugo, без служебных файлов Obsidian.
  2. prohomelab-site - весь Hugo-проект: тема Blowfish, конфиги, layouts, скрипты сборки.

Почему два, а не один: Obsidian Git может версионировать только одну конкретную подпапку, и мешать в неё Hugo-специфичные файлы незачем. Может я конечно и ошибаюсь, но пока вот так.

Порядок событий при публикации статьи:

  1. Сохранили заметку в Obsidian.
  2. Плагин Obsidian Git сам коммитит и пушит изменение в prohomelab-content.
  3. Push в prohomelab-content запускает свой workflow, который через Forgejo API «будит» второй репозиторий.
  4. На сервере раннер: скачивает свежий контент → чинит обсидиановский синтаксис картинок → кладёт статьи в Hugo-проект → hugo -D --minify → заливает public/ на хостинг по FTPS.

Важное отличие от того, как это делает GitHub: в Forgejo (по крайней мере в версии, использованной здесь, v13) нет API-события repository_dispatch с произвольным именем события, как в GitHub Actions. Есть только workflow_dispatch - запуск заранее известного конкретного workflow-файла по имени через POST /repos/{owner}/{repo}/actions/workflows/{имя-файла}.yml/dispatches. Вся схема ниже сразу построена на этом. Я в этом не особо разбирался и сначала получал ошибку 404, все волосы с головы повыдергивал и в монитор кинул. В итоге все решил, в разделе Troubleshooting в конце статьи расписано, как эту ошибку опознать и починить.


Глава 1. Приводим в порядок Hugo-репозиторий на сервере
#

Шаг 1.1. Подключитесь к серверу по SSH
#

ssh root@IP_ВАШЕГО_СЕРВЕРА

Дальше все команды этого раздела - внутри этой SSH-сессии.

Шаг 1.2. Перейдите в папку проекта
#

cd /home/prohomelab
pwd

Должно вывести /home/prohomelab (или ваш путь, если Hugo-проект лежит в другом месте).

Шаг 1.3. Укажите git, кто вы
#

git config user.name "Ваше имя"
git config user.email "you@example.com"

Без --global - настройка применится только к этому репозиторию.

Шаг 1.4. Создайте файл .gitignore
#

cat > .gitignore <<'EOF'
public/
resources/_gen/
.hugo_build.lock
EOF
cat .gitignore

public/ (результат сборки Hugo) и resources/_gen (кэш) пересобираются каждый раз заново - хранить их в git не нужно, поэтому мы их исключаем из синхронизации.

Шаг 1.5. Уберите public/ и resources/ из git, если они уже оказались в репозитории.
#

git rm -r --cached public resources 2>/dev/null || true

Шаг 1.6. Первый коммит
#

git add .
git status

Проверьте список - public/ и resources/ не должны быть в списке коммита, остальное (content/, themes/, hugo.toml, .gitmodules и т.д.) - должно быть отмечено зелёным.

git commit -m "Initial commit: Hugo + Blowfish site"

Шаг 1.7. Создайте пустой репозиторий в Forgejo
#

В браузере откройте свой Forgejo (у меня это https://forgejo.ваш-домен.ru) → “+” → New Repository → имя prohomelab-site → НЕ ставьте галочки инициализации (README/.gitignore/лицензия - репозиторий должен остаться пустым) → Create Repository.

Шаг 1.8. Токен доступа вместо пароля (создаем сразу, чтобы потом не спотыкаться на этапе push)
#

Push по HTTPS в Forgejo требует не обычный пароль аккаунта, а Personal Access Token. Создайте один универсальный токен сразу для всех задач этого проекта:

Settings → Applications → Generate New Token → имя, например, prohomelab-ci → права: write:repository (repository access - All) → Generate. Сохраните значение токена - оно показывается только один раз.

Шаг 1.9. Привяжите remote (удаленнный репозиторий) с токеном в URL
#

git remote add origin https://ваш-логин:ВАШ_ТОКЕН@forgejo.ваш-домен.ru/ваш-логин/prohomelab-site.git
git branch -M main
git push -u origin main

Замените ваш-логин на ваш логин Forgejo, forgejo.ваш-домен.ru - на адрес вашего инстанса Forgejo, а ВАШ_ТОКЕН - на токен из шага 1.8. Так push пройдёт сразу без интерактивных запросов пароля.

Шаг 1.10. Проверяем результат
#

Откройте https://forgejo.ваш-домен.ru/ваш-логин/prohomelab-site - должны быть видны hugo.toml, content/, themes/ и т.д.

Как итог, того что мы с вами только что сделали: репозиторий Hugo-сайта теперь находится и версионируется в Forgejo.


Глава 2. Настраиваем Obsidian так, чтобы статьи сами уходили в Forgejo
#

Шаг 2.1. Создайте второй пустой репозиторий в Forgejo
#

Делаем все как в п. 1.7, но с именем prohomelab-content.

Шаг 2.2. Инициализируйте git прямо в папке со статьями (не во всём vault!)
#

На Windows, в PowerShell:

cd "C:\Users\ваш-пользователь\Documents\Obsidian\ваш-vault\01-Projects\Prohomelab"
git init
git config user.name "Ваше имя"
git config user.email "you@example.com"

Если PowerShell не знает команду git, значит вы его ещё не установили. Установите Git for Windows (https://git-scm.com/download/win), настройки по умолчанию, перезапустите PowerShell.

Шаг 2.3. Исключите служебные файлы Obsidian
#

@"
Prohomelab.base
_Prohomelab-Index.md
"@ | Out-File -Encoding utf8 .gitignore
Get-Content .gitignore

Допишите туда любые другие черновики/служебные заметки, которые не должны публиковаться. Пример выше касается моих настроек, у вас может быть другая структура хранилища, поэтому не надо копировать команду бездумно.

Шаг 2.4. Первый коммит и push
#

git add .
git status
git commit -m "Initial content"
git remote add origin https://ваш-логин:ВАШ_ТОКЕН@forgejo.ваш-домен.ru/ваш-логин/prohomelab-content.git
git branch -M main
git push -u origin main

Используем тот же токен из Шага 1.8 - он выдан с правами на все репозитории аккаунта, если вы ещё не забыли.

Шаг 2.5. Настройте плагин Obsidian Git на эту конкретную папку
#

Так как плагин достаточно активно обновляется, то наименование полей к сожалению может поменяться, в таком случае, придется искать по смыслу.

Открыть Obsidian → Settings → Git → раздел Advanced → поле Custom base path:

01-Projects/Prohomelab

(со слэшем /, даже на Windows). Это ключевая настройка - без неё плагин работает с корнем всего vault.

После сохранения плагин может попросить перезагрузить Obsidian - соглашаемся, выбора-то у нас все равно нет.

После перезагрузки, там же в настройках Git (названия полей могут отличаться версии от версии - у современных версий это объединённая формулировка):

  • Auto commit and sync interval (minutes) - например 10. Раз в N минут: если есть несохранённые изменения - коммит и сразу push. (В более старых версиях плагина это могло быть двумя отдельными полями - Vault backup interval и Auto push interval - тогда ставьте туда те же значения, если по каким-то причинам у вас столь древняя версия плагина.)
  • Commit message on auto commit and sync - можно оставить по умолчанию.
  • Pull on startup - включите, чтобы подтягивать изменения при открытии Obsidian (полезно при работе с нескольких устройств).

Главное не путайте с настройкой Automatically refresh Source Control View on file changes - это просто обновление окна интерфейса, к автокоммиту/автопушу никакого отношения не имеет.

Шаг 2.6. Проверьте автокоммит
#

Откройте любую статью, допишите тестовый символ (я обычно пишу, stilicho, ты балбес, но было бы странно и крайне тревожно, если бы и вы написали тоже самое), сохраните. Через палитру команд (Ctrl+P) выполните команду коммита/синка вручную (не дожидаясь таймера), затем проверьте https://forgejo.ваш-домен.ru/ваш-логин/prohomelab-content - должен появиться новый коммит.

Как итог действий, перечисленных в настоящей главе - сохранение заметки в Obsidian долетает до Forgejo без вашего участия.


Глава 3. Ставим раннер на сервер
#

Раннер - фоновая программа, которая ждёт сигнала от Forgejo «в репозитории что-то поменялось, выполни вот эти команды, да побыстрее».

Шаг 3.1. Проверьте, что Forgejo Actions включены
#

Начиная с Forgejo v1.21 Actions включены по умолчанию - специально что-то включать почти наверняка не придётся. Самое простое сразу проверить в браузере, откройте любой репозиторий (prohomelab-site) - сверху должна быть вкладка Actions. Если она уже есть - смело переходите к Шагу 3.2, редактировать app.ini не нужно.

Если вкладки нет, то это все правится это через [actions] в app.ini - тем же способом, что и в статье про GitOps.

Если Forgejo у вас в Docker Compose (самый частый случай в хоумлабе): найдите на хосте, где лежит docker-compose.yml, папку данных - обычно она указана в volumes: как ./forgejo:/data. Тогда конфиг лежит по пути:

<папка-с-docker-compose.yml>/forgejo/gitea/conf/app.ini

Отредактируйте его прямо на хосте (не заходя внутрь контейнера):

nano ./forgejo/gitea/conf/app.ini

Допишите в конец файла:

[actions]
ENABLED = true

Сохраните (Ctrl+O, Enter, Ctrl+X) и перезапустите контейнер:

docker restart forgejo

(имя контейнера смотрите в своём docker-compose.yml, поле container_name).

Проверьте, что настройка применилась:

docker exec forgejo cat /data/gitea/conf/app.ini | grep -A2 "\[actions\]"

Должно показать ENABLED = true, и вкладка Actions появится в репозитории.

Шаг 3.2. Скачайте и проверьте бинарник раннера
#

У code.forgejo.org (в отличие от GitHub) нет alias /latest/download/... - зато есть API, который одной командой отдаёт номер актуального релиза, без похода в браузер и ручного копирования ссылок:

export ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
export RUNNER_VERSION=$(curl -s https://data.forgejo.org/api/v1/repos/forgejo/runner/releases/latest | jq -r .name | cut -c2-)
export FORGEJO_URL="https://code.forgejo.org/forgejo/runner/releases/download/v${RUNNER_VERSION}/forgejo-runner-${RUNNER_VERSION}-linux-${ARCH}"

wget -O forgejo-runner "${FORGEJO_URL}"
wget -O forgejo-runner.asc "${FORGEJO_URL}.asc"

Бинарники Forgejo подписаны GPG-ключом проекта - стоит проверить подпись перед тем, как запускать что-то от имени системного пользователя с доступом к вашему серверу:

gpg --keyserver hkps://keys.openpgp.org --recv EB114F5E6C0DC2BCDD183550A4B61A2DC5923710
gpg --verify forgejo-runner.asc forgejo-runner

В ответ должно быть Good signature from "Forgejo <contact@forgejo.org>". Если вместо этого BAD signature - файл повреждён или подменён при скачивании, удаляйте и качайте заново, дальше не продолжайте.

Note

Строка WARNING: This key is not certified with a trusted signature! ниже - это нормально и не имеет отношения к целостности файла. Она означает только то, что вы лично не подписывали этот ключ в своей цепочке доверия GPG (никто этого не делает ради разовой проверки бинарника), а не то, что с подписью что-то не так. Значение имеет строка Good signature from и совпадение fingerprint’а с тем, что вы запрашивали у keyserver (EB11 4F5E ... C592 3710).

Шаг 3.3. Установите бинарник и создайте отдельного пользователя
#

chmod +x forgejo-runner
sudo mv forgejo-runner /usr/local/bin/forgejo-runner
forgejo-runner -v

sudo useradd --create-home runner

Раннер для сборки сайта работает в режиме host - выполняет задания напрямую в системе, без Docker. Поэтому, в отличие от раннеров, которым нужен доступ к Docker-сокету, добавлять пользователя runner в группу docker здесь не нужно.

Шаг 3.4. Зарегистрируйте раннер
#

Caution

В статьях и видео про Forgejo Actions часто встречается команда forgejo-runner register --instance ... --token ... --name ... - интерактивная регистрация с одноразовым токеном из веб-интерфейса. Эта команда официально помечена как deprecated прямо в документации Forgejo - пока работает, но это уже не рекомендуемый путь. Актуальный способ - декларативный, без интерактивного диалога, в два шага на двух разных машинах.

Получить пару UUID + секрет можно двумя равноценными способами - выберите тот, что удобнее.

Вариант А: через CLI на стороне Forgejo. Заходим на хост, где крутится сам Forgejo (если он у вас в Docker Compose - через docker exec в его контейнер):

docker exec -it forgejo forgejo forgejo-cli actions register \
  --name site-runner \
  --scope ваш-логин \
  --secret $(openssl rand -hex 20)
Note

В отличие от сценария из статьи про Renovate (там раннер обслуживал только один репозиторий и --scope указывался как владелец/репозиторий), здесь один и тот же раннер с меткой host должен быть виден сразу обоим вашим репозиториям - prohomelab-content (там на него завязан notify.yml) и prohomelab-site (там - deploy.yml). Поэтому --scope указываем не на конкретный репозиторий, а на уровне владельца - просто ваш логин Forgejo, без слэша и имени репо. Тогда раннер увидят все ваши репозитории сразу, а не только один.

Команда выведет UUID - его нужно скопировать вместе с секретом на следующий шаг.

Вариант Б: через веб-интерфейс, без консоли вообще. Для нашего сценария (раннер нужен сразу нескольким репозиториям) даже удобнее - открываем страницу личного аккаунта:

https://forgejo.ваш-домен.ru/user/settings/actions/runners
  1. Заходите под своим обычным аккаунтом.
  2. Жмёте Create new runner, в поле имени - что-то понятное, например site-runner.
  3. Жмёте Create - Forgejo сразу показывает страницу с готовыми UUID и секретом (сгенерированным автоматически).
  4. Копируете оба значения - они уходят в config.yaml на следующем шаге.
Note

Секрет показывается один раз и больше нигде не хранится в открытом виде - если закрыли страницу, не скопировав его, придётся удалить эту регистрацию раннера и создать новую.

Шаг 3.5. Сгенерируйте и заполните config.yaml
#

sudo -u runner forgejo-runner generate-config | sudo -u runner tee /home/runner/config.yaml > /dev/null
Note

runner - отдельный системный пользователь, которого мы завели на шаге 3.3, и его домашняя директория /home/runner не открывается вашим обычным логином просто так - права на неё есть только у root и у самого runner. Если попробовать nano /home/runner/config.yaml под своим логином, получите Permission denied. Поэтому дальше для чтения и редактирования файлов в /home/runner используем sudo.

Файл, который получился - это не пустой шаблон с парой строк, а полный пример конфига на несколько сотен строк со всеми секциями и подробным комментарием к каждому полю. Разбираться в нём целиком не нужно - правим только два конкретных места, остальное оставляем как сгенерировалось:

sudo nano /home/runner/config.yaml

Первое место - секция runner:, поле labels. По умолчанию оно пустое:

runner:
  ...
  labels: []

Заменяем на список с одной меткой - именно по ней оба workflow (deploy.yml и notify.yml, у обоих в примерах ниже стоит runs-on: host) найдут этот раннер:

runner:
  ...
  labels:
    - "host:host"

Второе место - секция server:, поле connections. В сгенерированном файле она в самом конце секции server: и по умолчанию пустая - всё, что выше неё (# example:, # codeberg:), закомментировано и просто для примера, трогать не нужно:

server:
  connections:

Прямо под этой строкой, с отступом на два пробела больше, чем у connections:, дописываем свой блок - forgejo здесь просто ключ для этой связки (можно назвать иначе), а дальше три значения с предыдущего шага:

server:
  connections:
    forgejo:
      url: https://forgejo.ваш-домен.ru/
      uuid: <UUID_ИЗ_ШАГА_3.4>
      token: <ТОТ_ЖЕ_СЕКРЕТ_ЧТО_В___SECRET_ВЫШЕ>
Note

В отличие от раннера, который настраивается под Renovate, здесь не нужно трогать секцию container:/поле docker_host - она отвечает за проброс Docker-сокета в контейнер джобы, а наш раннер типа host вообще не запускает задания внутри контейнеров, ему просто нечего пробрасывать.

Сохраните (Ctrl+O, Enter) и выйдите (Ctrl+X).

Шаг 3.6. Создайте systemd-сервис
#

Актуальный unit-файл готов в официальном репозитории раннера - не нужно писать его руками, только скачать и поправить путь до своего конфига:

sudo wget -O /etc/systemd/system/forgejo-runner.service \
  https://code.forgejo.org/forgejo/runner/raw/branch/main/contrib/forgejo-runner.service
sudo sed -i 's|/home/runner/runner-config.yml|/home/runner/config.yaml|' /etc/systemd/system/forgejo-runner.service

По умолчанию в юните прописан путь /home/runner/runner-config.yml - sed выше подменяет его на файл, который мы сгенерировали на шаге 3.5 (config.yaml). Дальше как с любым новым unit-файлом - перечитать конфиги systemd и включить сервис:

sudo systemctl daemon-reload
sudo systemctl enable --now forgejo-runner
sudo systemctl status forgejo-runner

Должно быть active (running).

Проверьте логами, что раннер реально запустился и подключился к Forgejo:

journalctl -u forgejo-runner -f

И в веб-интерфейсе (там же, где регистрировали в Шаге 3.4) - раннер должен появиться со статусом Idle (может занять минуту).

Шаг 3.7. Установите зависимости для сборки и деплоя
#

apt update
apt install -y git lftp locales
locale-gen en_US.UTF-8
update-locale LANG=en_US.UTF-8

Пакет locales и генерация en_US.UTF-8 нужны, чтобы lftp (и некоторые другие утилиты) корректно работали со строками - без этого будут случаться труднообъяснимые ошибки вида “could not convert string to UTF-8”.

Hugo у вас уже установлен, если проходили предыдущую статью про установку Hugo + Blowfish - проверьте на всякий случай:

hugo version

Итог настоящей главы - у Forgejo есть рабочая лошадка на вашем сервере, готовая выполнять сборку.


Глава 4. Скрипт сборки и деплоя
#

Шаг 4.1. Токен для чтения контент-репозитория
#

Тот же способ, что в Шаге 1.8 - можно использовать тот же токен prohomelab-ci (если давали ему write:repository на все репозитории - этого достаточно, он имеет право и на чтение). Если создаете отдельный токен - прав read:repository достаточно для этой задачи.

Шаг 4.2. Скрипт синхронизации контента
#

На сервере:

cd /home/prohomelab
mkdir -p scripts

cat > scripts/sync-content.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

export GIT_TERMINAL_PROMPT=0

CONTENT_REPO_HOST="forgejo.ваш-домен.ru/ваш-логин/prohomelab-content.git"
CONTENT_DIR="/tmp/prohomelab-content"
TARGET_DIR="content/posts"

if [ -n "${CONTENT_REPO_TOKEN:-}" ]; then
  CONTENT_REPO_URL="https://ваш-логин:${CONTENT_REPO_TOKEN}@${CONTENT_REPO_HOST}"
else
  echo "ERROR: CONTENT_REPO_TOKEN is not set" >&2
  exit 1
fi

rm -rf "$CONTENT_DIR"
git clone --depth 1 "$CONTENT_REPO_URL" "$CONTENT_DIR"

rsync -a --delete \
  --exclude='.git' \
  --exclude='Prohomelab.base' \
  --exclude='_Prohomelab-Index.md' \
  "$CONTENT_DIR/" "$TARGET_DIR/"

find "$TARGET_DIR" -type f -name '*.md' -print0 | xargs -0 sed -i -E 's/!\[\[([^]]+)\]\]/![](\1)/g'

echo "Content synced and converted."
EOF

chmod +x scripts/sync-content.sh

(В строке CONTENT_REPO_HOST подставьте свой домен Forgejo и логин вместо forgejo.ваш-домен.ru/ваш-логин.)

Разбор ключевых моментов:

  • export GIT_TERMINAL_PROMPT=0 - запрещает git пытаться интерактивно спросить логин/пароль. Без этой строки, если токен по какой-то причине не пришёл, git может зависнуть намертво на команде clone, ожидая ввод с несуществующего терминала, вместо того чтобы сразу выдать ошибку. Мы словили это на практике - зависание длилось много минут, пока не отменили вручную.
  • Явная проверка if [ -n "${CONTENT_REPO_TOKEN:-}" ] - если секрет не пришёл, скрипт сразу падает с понятной ошибкой ERROR: CONTENT_REPO_TOKEN is not set, а не тратит время на попытку подключения без авторизации.
  • git clone --depth 1 - скачивает только последнее состояние без истории, быстрее.
  • rsync -a --delete - синхронизирует содержимое, удаляя на стороне сайта то, чего больше нет в контенте (иначе удалённые в Obsidian статьи продолжали бы висеть на сайте).
  • sed в конце - конвертирует обсидиановский синтаксис вставки картинок ![](файл.png) в обычный markdown ![](файл.png), который Hugo понимает. Если позже найдёте другие непонятные Hugo конструкции (коллбауты > [!note], внутренние wiki-ссылки [[Заметка]] между статьями) - добавляйте сюда ещё по одной строке sed по тому же принципу.

Проверьте скрипт вручную перед тем, как доверить его автоматике:

export CONTENT_REPO_TOKEN=ВАШ_ТОКЕН
bash scripts/sync-content.sh
unset CONTENT_REPO_TOKEN
ls content/posts

Должно все отработать без вопросов о пароле и показать структуру папок со статьями.

Закоммитьте скрипт сразу, не откладывая - это частая причина ошибки “No such file or directory” при первом запуске через Actions: раннер всегда работает с чистым checkout из git, а не с тем, что физически лежит на диске:

git add scripts/sync-content.sh
git commit -m "Add content sync script"
git push

Шаг 4.3. Файл workflow сборки и деплоя
#

mkdir -p .forgejo/workflows
cat > .forgejo/workflows/deploy.yml <<'EOF'
name: Build and Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: host
    steps:
      - name: Checkout site repo
        uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Sync content from Obsidian repo
        env:
          CONTENT_REPO_TOKEN: ${{ secrets.CONTENT_REPO_TOKEN }}
        run: bash scripts/sync-content.sh

      - name: Build with Hugo
        run: hugo -D --minify

      - name: Deploy via FTP
        env:
          FTP_HOST: ${{ secrets.FTP_HOST }}
          FTP_USER: ${{ secrets.FTP_USER }}
          FTP_PASS: ${{ secrets.FTP_PASS }}
        run: |
          lftp -u "$FTP_USER,$FTP_PASS" "$FTP_HOST" <<'INNEREOF'
          set ssl:verify-certificate no
          mirror -R --delete --verbose public/ /www/ВАШ_ДОМЕН
          bye
          INNEREOF
EOF

Разбор:

  • on: - только push (на ваш собственный push в сайт, например правку темы) и workflow_dispatch (можно запустить вручную из UI, и именно так его будет дёргать второй репозиторий - см. Шаг 4.5). Мы намеренно не используем repository_dispatch - такого API-события в Forgejo нет, попытка его использовать даёт 404 page not found.
  • runs-on: host - выполнять на раннере с меткой host (левая часть лейбла из Шага 3.5).
  • submodules: recursive - подтягивает тему Blowfish при checkout.
  • hugo -D --minify - -D включает черновики (если не хотите публиковать черновики на боевом сайте - уберите флаг, когда будете готовы к полностью боевому режиму), --minify сжимает вывод.
  • set ssl:verify-certificate no - многие shared-хостинги используют для FTPS сертификат, который не проходит строгую проверку доверия, хотя соединение всё равно шифруется. Без этой строки lftp упадёт с Certificate verification: The certificate is NOT trusted. Если у вашего хостинга сертификат нормальный - эту строку можно убрать.
  • Путь /www/ВАШ_ДОМЕН - не угадывайте этот путь, проверьте его вручную (Шаг 4.4), прежде чем вписывать в workflow - от него зависит, куда --delete будет удалять файлы.

Шаг 4.4. Проверьте реальный путь на хостинге, прежде чем гонять --delete вслепую
#

Зайдите в панель управления вашим хостингом и посмотрите поле «Корневая директория» для нужного сайта - часто оно выглядит как www/ваш-домен.ru, а не просто ваш-домен.ru или голое имя проекта без домена. Не полагайтесь на память о том, что было в FileZilla - проверьте заново, ошибка в пути с --delete может стереть не то.

Подключитесь вручную прямо с сервера:

lftp -u ВАШ_FTP_ЛОГИН ftp://АДРЕС_FTP_СЕРВЕРА

Если при первой же команде (ls) получите:

Fatal error: Certificate verification: The certificate is NOT trusted.
  • выполните внутри той же сессии:
set ssl:verify-certificate no

и повторите ls.

Если после этого 530 Login incorrect - перепроверьте пароль (без отображения на экране легко ошибиться), выйдите (exit) и зайдите заново.

Когда зашли - пройдите по структуре и убедитесь, что видите реальные файлы Hugo-сайта (index.html, posts/, sitemap.xml):

ls
cd www
ls
cd ВАШ_ДОМЕН
ls

Запомните точный путь (например /www/prohomelab.com) и подставьте его в mirror -R --delete --verbose public/ ЭТОТ_ПУТЬ в Шаге 4.3 вместо плейсхолдера. Выйдите: exit.

Закоммитьте deploy.yml:

cd /home/prohomelab
git add .forgejo/workflows/deploy.yml
git commit -m "Add deploy workflow"
git push

Шаг 4.5. Второй workflow - «будильник» в репозитории контента
#

Push в prohomelab-content сам по себе не запускает workflow в prohomelab-site - это два независимых репозитория. Нужен маленький workflow, который через API Forgejo запускает конкретный workflow-файл (deploy.yml) во втором репозитории.

На Windows, в PowerShell, в папке контента:

cd "C:\Users\ваш-пользователь\Documents\Obsidian\ваш-vault\01-Projects\Prohomelab"
mkdir .forgejo\workflows -Force

@"
name: Notify site repo

on:
  push:
    branches: [main]

jobs:
  notify:
    runs-on: host
    steps:
      - name: Trigger site build
        env:
          TOKEN: `${{ secrets.FORGEJO_DISPATCH_TOKEN }}
        run: |
          curl -X POST \
            -H "Authorization: token `$TOKEN" \
            -H "Content-Type: application/json" \
            "https://forgejo.ваш-домен.ru/api/v1/repos/ваш-логин/prohomelab-site/actions/workflows/deploy.yml/dispatches" \
            -d '{"ref":"main"}'
"@ | Out-File -Encoding utf8 .forgejo\workflows\notify.yml

Get-Content .forgejo\workflows\notify.yml

Обратите внимание на бэктики ` перед ${{ и $TOKEN - в PowerShell это экранирование, чтобы символы попали в файл буквально, а не были подставлены самим PowerShell. Проверьте вывод Get-Content - там должно быть ровно ${{ secrets.FORGEJO_DISPATCH_TOKEN }} и $TOKEN, без искажений.

Важно про сам эндпоинт: используется /actions/workflows/deploy.yml/dispatches (запуск конкретного workflow по имени файла) с телом {"ref":"main"} - это правильный, рабочий способ в Forgejo. Путь /repos/{owner}/{repo}/dispatches с телом {"event_type": "..."}" (аналог GitHub repository_dispatch) в Forgejo не существует и даст 404 - не используйте его, даже если увидите в примерах для GitHub Actions.

Закоммитьте и запушьте:

git add .forgejo/workflows/notify.yml
git commit -m "Add dispatch workflow"
git push

Шаг 4.6. Секреты
#

В prohomelab-content (Settings → Actions → Secrets):

  • FORGEJO_DISPATCH_TOKEN - токен из Шага 1.8/4.1 (нужны права как минимум на запуск workflow в целевом репозитории - токен с write:repository на все репозитории подходит).

В prohomelab-site:

  • CONTENT_REPO_TOKEN - тот же токен, для чтения контент-репозитория.
  • FTP_HOST - адрес FTP-сервера вашего хостинга.
  • FTP_USER - логин FTP.
  • FTP_PASS - пароль FTP.

Чек-пойнт Главы 4: оба workflow-файла, скрипт и все секреты на месте. ✅


Глава 5. Сквозная проверка
#

Шаг 5.1. Тестовое изменение
#

В Obsidian отредактируйте статью, сохраните, выполните коммит/синк вручную (палитра команд, Ctrl+P), не дожидаясь таймера.

Шаг 5.2. Проверьте notify.yml
#

https://forgejo.ваш-домен.ru/ваш-логин/prohomelab-content/actions → должен быть новый прогон notify.yml со статусом Success.

Шаг 5.3. Проверьте deploy.yml
#

https://forgejo.ваш-домен.ru/ваш-логин/prohomelab-site/actions → должен появиться новый прогон deploy.yml, запущенный через workflow_dispatch (не через push - обратите внимание на пометку триггера). Раскройте все шаги:

  • Checkout site repo - просто отрабатывает.
  • Sync content from Obsidian repo - должно занять секунды (не минуты - если висит долго, смотрите Troubleshooting ниже).
  • Build with Hugo - вывод Hugo, сколько страниц собрано.
  • Deploy via FTP - список файлов, которые lftp заливает.

Итоговый статус job - Success.

Шаг 5.4. Проверка вживую
#

Откройте ваш сайт в браузере, убедитесь, что правка на месте.


Раздел «если что-то не работает» (по нашему реальному опыту)
#

  • git push просит пароль и не принимает обычный пароль аккаунта. Используйте Personal Access Token вместо пароля (Шаг 1.8), либо сразу пропишите его прямо в URL remote: git remote set-url origin https://ваш-логин:ВАШ_ТОКЕН@forgejo.ваш-домен.ru/....

  • docker exec forgejo cat /data/gitea/conf/app.ini не показывает вашу правку [actions]. Значит редактировали не тот файл на хосте - сверьте путь volumes: в docker-compose.yml, конфиг лежит по <volume-host-path>/gitea/conf/app.ini.

  • export RUNNER_VERSION=$(...) в Шаге 3.2 возвращает пустоту или ошибку jq: command not found. На свежем сервере jq часто не установлен по умолчанию - поставьте его (apt install -y jq) и повторите команду. Если jq на месте, а строка всё равно пустая - проверьте вручную, что curl -s https://data.forgejo.org/api/v1/repos/forgejo/runner/releases/latest вообще отвечает (не заблокирован ли исходящий доступ файрволом).

  • gpg --recv EB114F5E... виснет или падает с ошибкой соединения. Keyserver hkps://keys.openpgp.org иногда недоступен из вашей сети - попробуйте ещё раз через минуту, либо укажите другой keyserver тем же флагом --keyserver. Пропускать саму проверку подписи не стоит - именно она страхует от подменённого при скачивании бинарника.

  • Раннер зарегистрирован, но задания в workflow вечно “Waiting for a runner with the following label: host”. Откройте /home/runner/config.yaml (sudo cat /home/runner/config.yaml | grep -A3 "labels") - в секции runner: поле labels должно быть списком с "host:host" (с двоеточием), а не пустым [] и не просто "host". Если поправили - перезапустите сервис: sudo systemctl restart forgejo-runner.

  • Раннер в веб-интерфейсе показывает статус offline, хотя systemctl status forgejo-runner пишет active (running). Почти всегда это опечатка или устаревшее значение в секции server: connections: в /home/runner/config.yaml - uuid/token должны точно совпадать с теми, что показала Forgejo при регистрации в Шаге 3.4 (не с секретом, который вы, возможно, вводили сами в старом --token варианте - здесь секрет генерируется автоматически или через openssl, руками не придумывается).

  • git clone в скрипте синхронизации виснет на много минут вместо секунд. Добавьте в скрипт export GIT_TERMINAL_PROMPT=0 и явную проверку токена перед clone (см. Шаг 4.2) - тогда вместо зависания будет мгновенная понятная ошибка, если что-то не так с токеном.

  • bash: scripts/sync-content.sh: No such file or directory при первом запуске через Actions, хотя скрипт точно есть на диске. Раннер при каждом запуске делает чистый checkout из git - если скрипт не был закоммичен и запушен, в свежей копии его физически нет. Коммитьте и пушьте скрипт сразу после создания, не откладывайте.

  • curl в notify.yml возвращает 404 page not found. Скорее всего используется несуществующий в Forgejo эндпоинт /repos/{owner}/{repo}/dispatches с event_type (аналог GitHub repository_dispatch). В Forgejo используйте /repos/{owner}/{repo}/actions/workflows/{имя}.yml/dispatches с телом {"ref":"main"} (см. Шаг 4.5), и уберите из deploy.yml неработающий триггер repository_dispatch, оставив только push и workflow_dispatch.

  • lftp: command not found на шаге деплоя. Забыли установить lftp на сервере - apt install -y lftp (Шаг 3.7).

  • lftp выдаёт could not convert string to UTF-8. На системе не сгенерирована локаль, хотя LANG на неё ссылается. Выполните apt install -y locales && locale-gen en_US.UTF-8 && update-locale LANG=en_US.UTF-8.

  • lftp: Certificate verification: The certificate is NOT trusted. Обычное дело для shared-хостинга с самоподписанным/недоверенным сертификатом FTPS. Добавьте set ssl:verify-certificate no первой строкой в блоке команд lftp - как в workflow, так и при ручной проверке.

  • 530 Login incorrect в lftp. Обычно просто опечатка в пароле при ручном вводе (пароль не отображается на экране). Выйдите (exit) и зайдите заново, вводя осторожно, или подставьте пароль из менеджера паролей копированием.

  • Деплой прошёл успешно, но файлы легли не в ту папку на хостинге / затёрли что-то не то. Прежде чем доверять --delete автоматике, всегда вручную проверяйте реальный путь через lftp (Шаг 4.4) и сверяйте с «Корневой директорией» в панели управления хостингом - она может отличаться от того, что вы помните по работе в FileZilla.

  • Hugo падает с ERROR the "published" front matter field is not a parsable date. В конкретной статье во фронтматтере в поле published стоит не дата, а, например, true/false (если у вас в конфиге сайта published настроено как алиас даты публикации, а не флаг черновика). Исправьте на настоящую дату вида 2026-08-09 без кавычек. Отдельно, предупреждение “has both draft and published settings… Using draft” - не ошибка, а просто уведомление о приоритете draft над published, когда оба поля заданы одновременно.

  • Картинки на сайте не отображаются. Почти наверняка необработанный обсидиановский синтаксис вставки картинок. Проверьте markdown статьи - возможно, картинка вставлена не как ![](file.png) (это правило уже покрыто скриптом), а каким-то другим способом - тогда нужно добавить ещё одно sed-правило в scripts/sync-content.sh по аналогии.

  • Раннер выполняет только одно задание за раз, а вы отменили не тот прогон. В config.yaml раннера по умолчанию capacity: 1 - задания выполняются строго по очереди. Если запустили несколько прогонов подряд (например, тестируя), лишние встанут в очередь “Waiting” - это нормально, не пытайтесь запускать параллельно, дождитесь текущего или явно отмените (кнопка Cancel на странице прогона).


Что делать дальше, когда всё настроено
#

Обычный рабочий цикл: открыли Obsidian → написали или отредактировали статью → сохранили → (по желанию) сразу выполнили коммит-и-синк, если не хотите ждать автопуш по таймеру. Дальше всё происходит само: Forgejo → раннер → сборка → заливка на хостинг. FileZilla и ручной hugo -D из этого процесса полностью исключены.

Если статья ещё не готова к публикации, но вы хотите закоммитить прогресс - держите draft: true во фронтматтере; при hugo -D она всё равно попадёт на сайт (это удобно для предпросмотра, но помните об этом - если хотите публиковать только готовое, уберите флаг -D из команды hugo в deploy.yml, тогда черновики не будут собираться вовсе).

Автоматизация публикации ProHomelab - This article is part of a series.
Part : This Article

Related

Установка Hugo + тема Blowfish с нуля: подробная инструкция

··2434 слов·12 минут· loading · loading
Разворачиваю Hugo Extended и тему Blowfish с чистого листа: от подготовки LXC и установки Hugo до создания сайта, подключения темы, настройки структуры контента и первой production-сборки.

GitOps для homelab: разворачиваем Forgejo и Komodo с нуля

··5745 слов·28 минут· loading · loading
Подробный туториал по GitOps для начинающих: разворачиваем Forgejo как собственный git-сервер, Komodo для управления Docker-стеками и настраиваем автодеплой по push, с разбором каждого параметра конфигурации.