Hugo + GitHub Actions: prosty pipeline wdrożenia

Spis treści
Wstęp#
Najprostszy sposób na automatyczne wdrażanie strony w Hugo wygląda tak: trzymamy cały projekt w repozytorium na GitHubie, a GitHub Actions po każdym pushu buduje stronę i wysyła wygenerowany katalog public/ na nasz serwer przez SSH z użyciem rsync.
Założenia#
Zakładam tutaj dwie rzeczy.
Po pierwsze, masz już lokalnie działający projekt Hugo. Na przykład utworzony komendą hugo new site twoja_strona i sprawdzony w ten sposób, że lokalne wywołanie komendy hugo poprawnie generuje katalog public/.
Po drugie, Twój hosting udostępnia dostęp przez SSH. Czyli masz dane takie jak: login, adres hosta oraz katalog docelowy, do którego mają trafić pliki strony. Zwykle będzie to coś w stylu public_html albo www.
Przygotowanie repozytorium#
Na tym etapie robię zwykle trzy proste kroki.
Po pierwsze, wrzucam cały projekt Hugo do nowego repozytorium na GitHubie. Katalog public/ wyłączam z repozytorium, dodając go do .gitignore, ponieważ ten katalog będzie generowany w pipeline, a nie trzymany w historii gita.
Po drugie, w pliku konfiguracyjnym hugo.toml albo hugo.yaml (w starszych wersjach config.yaml albo config.toml) ustawiam poprawną wartość baseURL. Ta wartość powinna wskazywać na docelową domenę, pod którą strona będzie dostępna. Dzięki temu wygenerowane linki na stronie będą od razu poprawne po wdrożeniu.
Klucz SSH i sekrety w GitHub#
Na tym etapie potrzebny jest osobny klucz SSH przeznaczony wyłącznie do deployu, żeby nie mieszać go z kluczami używanymi na co dzień. Na swojej maszynie generuję więc nową parę kluczy, na przykład komendą: ssh-keygen -t ed25519 -f ~/.ssh/hugo_deploy.
Kolejny krok to powiązanie tego klucza z serwerem, na który będziemy wysyłać pliki. Zawartość pliku publicznego hugo_deploy.pub dopisuję do pliku ~/.ssh/authorized_keys na hostingu, dla tego konkretnego użytkownika, którego konto będzie używane do deployu. Dzięki temu GitHub Actions po stronie serwera zostanie rozpoznany jak zwykły użytkownik łączący się po SSH, ale z ograniczeniem tylko do tego, co pozwala konto na hostingu.
Następnie przechodzę do konfiguracji sekretów w repozytorium na GitHubie. W ustawieniach repozytorium wybieram: Settings → Secrets and variables → Actions → New repository secret i tam definiuję zmienne, z których skorzysta workflow. To miejsce jest przeznaczone właśnie na tego typu dane jak klucze i hasła, tak żeby nie trafiały do kodu ani do historii gita.

W praktyce dodaję kilka kluczowych sekretów, które później wykorzysta skrypt deployujący. Typowy zestaw to:
DEPLOY_KEY– zawartość prywatnego klucza SSH (ten plik bez rozszerzenia .pub),DEPLOY_HOST– adres serwera, na przykład ssh.domena.pl,DEPLOY_USER– login użytkownika na serwerze,DEPLOY_DIRECTORY– katalog docelowy, do którego mają trafić pliki, na przykład/home/user/public_html.
Taki układ pozwala z jednej strony trzymać całą logikę deployu w pliku workflow, a z drugiej – wszystkie wrażliwe dane w bezpiecznych sekretach repozytorium, co jest standardową praktyką przy automatycznych deploymentach z GitHub Actions.
Struktura repo i podstawowy build#
W tym podejściu repozytorium traktuję jako źródło prawdy dla całego projektu Hugo, a nie tylko miejsce przechowywania dla wygenerowanego HTML-a. To oznacza, że w repo powinny znaleźć się katalogi takie jak content/, layouts/, themes/, static/ oraz pliki konfiguracyjne config.* – czyli komplet źródeł, z których hugo jest w stanie zbudować stronę od zera. Dzięki temu GitHub Actions może odtworzyć cały proces builda na czystym runnerze, bez żadnych artefaktów z lokalnego środowiska.
Sam katalog public/ traktuję jako artefakt buildu, więc nie trzymam go w repozytorium. Dodaję go do .gitignore, żeby nie zaśmiecać historii zmian plikami, które i tak są generowane automatycznie przy każdym uruchomieniu hugo. Z tego samego powodu ignoruję też plik .hugo_build.lock – to pusty plik tworzony automatycznie przez Hugo jako lock przy buildzie lub komendach typu hugo new, więc bezpiecznie można traktować go jak lokalny plik tymczasowy i nie trzymać w repo. Taki wzorzec dobrze się sprawdza również przy innych statycznych generatorach – kod w repo, wynik tylko w pipeline i na serwerze lub w CDN.
W workflow GitHub Actions podstawowy krok builda jest bardzo prosty. Najczęściej wystarczy komenda hugo, ewentualnie hugo –minify, jeżeli chcemy od razu mieć zminifikowane CSS i JS. Po tym kroku runner ma w katalogu public/ kompletny zestaw statycznych plików gotowych do wysłania na serwer.
Żeby uniknąć niespodzianek przy aktualizacjach, warto zainstalować konkretną wersję Hugo w każdym uruchomieniu workflow. Do tego można użyć gotowych akcji z GitHub Marketplace, takich jak peaceiris/actions-hugo albo hugo-setup. Pozwalają one wskazać wersję Hugo (np. 0.152.2 lub latest) i w spójny sposób przygotować środowisko na runnerze przed uruchomieniem komendy hugo.
Instalacja motywu Hugo (submodule)#
Build w GitHub Actions zadziała tak samo jak lokalnie tylko wtedy, gdy motyw jest dostępny w repozytorium — w katalogu themes/<nazwa_motywu> albo jako moduł Hugo, bo generator właśnie tam szuka motywów podczas buildu. Jeśli motywu nie ma na GitHubie, runner po prostu nie będzie miał z czego zbudować widoków i build zakończy się błędem.
W tym opisie zakładam jedno konkretne podejście: motyw jako submodule gita. To daje dwie rzeczy naraz: trzymasz motyw jako osobne repo (łatwiejsze aktualizacje z upstreamu), a jednocześnie blokujesz się na konkretnym commicie w swoim projekcie, więc buildy są powtarzalne.
Git zapisuje submodule jako wskaźnik do konkretnego commita w repozytorium motywu, a nie jako „zawsze najnowszą wersję” z gałęzi. Dzięki temu motyw nie zaktualizuje się samoczynnie przy kolejnym buildzie – runner GitHub Actions zawsze pobierze dokładnie tę wersję, którą masz zapisaną w głównym repozytorium. Jeśli chcesz przejść na nowszą wersję motywu, robisz to świadomie: aktualizujesz submodule lokalnie, sprawdzasz, czy wszystko działa, i dopiero wtedy commitujesz nowy SHA do swojego repo.
Jeżeli wcześniej klonowałeś motyw „na sztywno” do themes/theme-name (np. git clone … themes/theme-name), najpierw usuń ten katalog albo przenieś go w inne miejsce:
rm -rf themes/theme-name
Następnie dodaj motyw jako submodule, wskazując jego oficjalne repozytorium i docelowy katalog:
git submodule add https://github.com/{theme-name}.git themes/theme-name
git status
W efekcie w głównym repo pojawi się plik .gitmodules oraz wpis themes/theme-name widoczny jako submodule w drzewie projektu. Te zmiany trzeba normalnie wciągnąć do historii — tak jak każdy inny kod — żeby na GitHubie faktycznie istniał plik .gitmodules i referencja do katalogu themes/theme-name. Bez tego runner Actions nie będzie wiedział, że w ogóle ma pobrać dodatkowe repozytorium z motywem.
Workflow GitHub Actions (deploy przez rsync)#
W kolejnym kroku dokładamy workflow, który po każdym pushu sam wykona cały proces build + deploy. W repozytorium tworzę plik .github/workflows/deploy.yml i wypełniam go definicją joba, który: pobiera kod, instaluje wskazaną wersję Hugo, buduje stronę do katalogu public/, a na końcu wywołuje akcję rsync-deployments (albo podobną) do wysłania plików na serwer. Tego typu akcje są dostępne w GitHub Marketplace i opierają się o rsync po SSH, więc dobrze pasują do wcześniej przygotowanego klucza i sekretów.
name: Deploy Hugo Site
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.152.2' # Pick specific version
extended: true
- name: Build Hugo
run: hugo --minify --gc
- name: Deploy via Rsync
uses: Burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: public/
remote_path: ${{ secrets.DEPLOY_DIRECTORY }}
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
Klucz uses w poszczególnych krokach to wskazanie gotowej akcji z GitHuba, czyli zewnętrznego „klocka”, który wykonuje za nas konkretne zadanie w kroku workflow. Taka akcja to po prostu zapakowany kawałek kodu (najczęściej JavaScript lub Docker), który uruchamia wewnątrz runnera odpowiednią sekwencję poleceń systemowych.
Taki workflow ustawiam zwykle tak, aby uruchamiał się na push do głównej gałęzi. Dzięki temu każdy merge do niej automatycznie kończy się nowym buildem i synchronizacją plików na serwerze. W definicji kroku rsync wskazuję katalog źródłowy: public/, docelowy: DEPLOY_USER@DEPLOY_HOST:DEPLOY_DIRECTORY oraz opcje, takie jak –archive, –compress i –delete, żeby zachować pełną synchronizację.
Flaga --delete jest tu szczególnie istotna. Dzięki niej rsync usuwa na serwerze pliki, których nie ma już w aktualnym katalogu public/, więc hosting zawsze odzwierciedla dokładny stan ostatniego builda. Nie zostają stare wersje podstron ani zasobów, które kiedyś istniały w projekcie, a potem zostały usunięte.
Flagi builda Hugo: –minify i –gc#
W kroku builda stosuję dwie flagi, które optymalizują wynik i zapewniają spójność z lokalnymi buildami.
Flaga --minify włącza automatyczną minifikację wygenerowanych plików HTML, CSS, JavaScript, JSON, SVG i XML. Hugo wykorzystuje wewnętrznie bibliotekę tdewolff/minify, która redukuje rozmiar plików poprzez usunięcie zbędnych białych znaków, komentarzy i optymalizację struktury kodu. W przypadku typowego bloga pozwala to zmniejszyć całkowity rozmiar HTML i CSS o 20-30%, co przekłada się bezpośrednio na szybsze ładowanie strony i lepsze wyniki w PageSpeed Insights. Minifikacja jest bezstratna – strona wygląda i działa identycznie, tylko pliki są mniejsze.
Flaga --gc to skrót od „garbage collection" (odśmiecanie) i każe Hugo wyczyścić nieużywane pliki z cache’a buildu. Podczas budowania Hugo może generować tymczasowe przetworzone zasoby (zminifikowane CSS, pliki z fingerprintem, przeskalowane obrazy) w katalogu resources/_gen/. Flaga --gc usuwa pliki, które zostały wygenerowane, ale nie są już wykorzystywane w finalnym wyniku. W GitHub Actions, gdzie każdy runner startuje ze świeżym stanem, ta flaga ma mniejsze znaczenie niż przy lokalnych buildach, gdzie cache gromadzi się przez wiele uruchomień. Mimo to stosuję ją w workflow z dwóch powodów: po pierwsze, zapewnia to, że komenda builda jest identyczna z tą, którą uruchamiasz lokalnie (co ułatwia odtwarzalność i debugowanie), a po drugie, gwarantuje, że nawet w ramach jednego builda ewentualne osierocone pliki tymczasowe zostaną usunięte przed deploymentem.
Stosowanie obu flag – hugo --minify --gc – to dobra praktyka dla buildów produkcyjnych, dająca mniejsze pliki dla użytkowników i czysty, przewidywalny wynik builda.
Uruchomienie i debugowanie#
Kiedy plik workflow jest gotowy, robię zwykły commit i git push na GitHuba. W zakładce „Actions” w repozytorium od razu pojawia się nowe uruchomienie workflow, gdzie mogę podejrzeć szczegóły poszczególnych kroków. To dobre miejsce, żeby zweryfikować, czy instalacja Hugo, sam build i krok z rsync przechodzą bez błędów.
Jeśli coś pójdzie nie tak, logi z joba zazwyczaj bardzo jasno wskazują przyczynę. Błędy związane z SSH (np. zły klucz, host, użytkownik albo port) zobaczysz w kroku deployu, błędne ścieżki katalogów wyjdą przy rsync, a problemy z konfiguracją samego Hugo pojawią się już na etapie komendy hugo. To pozwala dość szybko namierzyć, czy trzeba poprawić sekrety, ścieżki na serwerze, czy raczej konfigurację projektu.
Po udanym przebiegu workflow przechodzę do przeglądarki i sprawdzam, czy pod docelową domeną widać nową wersję strony. Od tego momentu każdy kolejny commit do głównej gałęzi automatycznie odświeża zawartość na hostingu, więc deploy staje się naturalnym elementem „git push”, a nie osobnym, ręcznym krokiem.
Jeżeli Twój hosting nie udostępnia SSH, schemat builda Hugo w GitHub Actions zostaje taki sam, zmienia się tylko ostatni krok odpowiedzialny za wysyłkę. Zamiast rsync możesz wtedy użyć jednej z akcji FTP lub SFTP dostępnych w Marketplace, podmieniając jedynie konfigurację sekretnych danych dostępowych i komendę deployującą. Reszta pipeline’u – checkout, setup Hugo, build – działa identycznie.
Podsumowanie#
Wdrożenie tego pipeline’u zajmuje zazwyczaj około 15 minut, a oszczędza mnóstwo czasu w przyszłości. Zamiast ręcznie budować stronę i kopiować pliki przez FTP, po prostu robisz git push, a reszta dzieje się sama. Jeśli raz to skonfigurujesz, proces publikacji nowych postów staje się niemal przezroczysty – skupiasz się na pisaniu, a technologia po prostu działa w tle.
Deploy Hugo – TL;DR lista#
- Przygotuj repo: Wypchnij projekt Hugo na GitHuba (bez katalogu public/ i pliku .hugo_build.lock).
- Zadbaj o motyw: Jeśli używasz zewnętrznego motywu, dodaj go jako submodule (git submodule add …), żeby GitHub Actions mógł go pobrać.
- Klucze SSH:
- Wygeneruj nową parę kluczy (ssh-keygen -t ed25519).
- Klucz publiczny (.pub) dodaj do ~/.ssh/authorized_keys na swoim hostingu.
- Klucz prywatny dodaj jako sekret DEPLOY_KEY w ustawieniach repozytorium na GitHubie.
- Sekrety: W repozytorium (Settings → Secrets) ustaw zmienne: DEPLOY_HOST, DEPLOY_USER, DEPLOY_KEY i DEPLOY_DIRECTORY.
- Workflow: Utwórz plik .github/workflows/deploy.yml, który:
- Robi checkout z submodules: true.
- Instaluje Hugo i buduje stronę (hugo –minify).
- Wysyła zawartość public/ przez rsync na serwer.
- Push: Wrzuć zmiany na main i sprawdź w zakładce Actions, czy build przeszedł na zielono.