Ręczne przeciąganie motywów przez FTP w 2026 roku kończy się zwykle tak samo: brakuje jednego pliku z inc/, composer.lock jest z innego laptopa, a produkcja leży, bo ktoś nadpisał wp-config.php. Continuous Integration i Continuous Deployment nie są fanaberią enterprise - to sposób, by każda zmiana w Gicie przeszła ten sam build, te same testy i ten sam sposób wgrania na serwer.
Ten przewodnik dotyczy WordPressa z motywem lub wtyczką w repozytorium (Composer, npm, ewent. Docker), nie „strony klikanej wyłącznie w panelu”. Jeśli cały kod żyje tylko w bazie i w mediach, najpierw przenieś motywy i custom code do Gita - bez tego CI/CD nie ma czego budować.
Oficjalna dokumentacja runnerów: GitHub Actions. Twierdzenie WordPressa o hardeningu serwera warto czytać równolegle: Hardening WordPress.
Dlaczego nie wgrywać WordPressa przez FTP
Trzy problemy wracają w audytach polskich sklepów WooCommerce i stron agencji:
- Niespójność środowiska - lokalnie PHP 8.2 i Composer 2.7, na VPS PHP 8.1 bez rozszerzenia
intl. Lokalnycomposer install„działa”, produkcja pada na class not found. - Brak audytu - po incydencie nie da się powiedzieć, który commit był na żywo. Panel hostingu pokazuje datę pliku, nie SHA z Gita.
- Sekrety na dysku - zapisane hasła SFTP w FileZilli albo w
.envcommitowanym „na chwilę” to klasyczna droga do wycieku.
CI/CD rozwiązuje to mechanicznie: build na znanym obrazie (np. shivammathur/setup-php albo własny Docker), artefakt wersjonowany SHA, deploy po SSH kluczem trzymanym w Secrets.
Deploy WordPressa przez GitHub Actions krok po kroku
Typowy workflow dla motywu custom + wtyczek z Composer:
- Trigger - push do
mainalbo merge Pull Requestu po review. Gałąźdevelopczęsto idzie tylko na staging. - Build -
composer install --no-dev --optimize-autoloader,npm ci,npm run build. Wersje PHP i Node pinujesz w workflow (php-version: '8.3',node-version: '24'). - Test - PHPUnit dla wtyczek, ewent. Playwright na krytyczny flow koszyka. Fail = stop, zero deployu.
- Artefakt - pakujesz katalog release bez
.git, beznode_modules, zvendor/i zbudowanymassets/dist/. - Deploy - rsync/SCP do
releases/<run_id>/, potem atomowe przełączenie symlinku.
Pinuj wersje, które jeszcze dostają poprawki. PHP 8.2 ma poprawki bezpieczeństwa tylko do 31 grudnia 2026, PHP 8.3 do 31 grudnia 2027 (wspierane wersje PHP). Node.js 20 stracił wsparcie 30 kwietnia 2026; Node.js 22 jest wspierany do 30 kwietnia 2027, a Node.js 24 do 30 kwietnia 2028 (harmonogram wydań Node.js). Wersję minor PHP dobierasz do produkcji, nie do najnowszego wydania.
Fragment idei (skrócony) w GitHub Actions, z głównymi tagami dla czytelności; dlaczego produkcyjny workflow pinuje SHA commitów, wyjaśnia sekcja o zabezpieczeniu workflow niżej:
name: deploy-production
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-dev --optimize-autoloader
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
- run: npm ci && npm run build
- name: rsync release
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
# zapisz klucz, rsync do releases/$GITHUB_SHA, ln -sfnPełny skrypt symlinku trzymasz w repo jako bin/release.sh - w workflow tylko go wywołujesz. Dzięki temu ten sam skrypt działa z laptopa w awarii, gdy Actions leży.
Jak zablokować dwa równoległe deploye WordPressa w GitHub Actions
Dwa szybkie merge’e do main uruchamiają dwa przebiegi workflow. Bez blokady oba robią rsync do releases/ i przełączają current w kolejności, której nikt nie wybrał. Grupa concurrency w GitHub Actions pozwala na najwyżej jeden działający przebieg w grupie, a nowo zakolejkowany czeka jako pending (dokumentacja GitHub: kontrola współbieżności).
Przy deployu liczą się dwa szczegóły. Domyślnie grupa trzyma jeden działający i jeden oczekujący przebieg, a nowszy anuluje starszy oczekujący, więc wygrywa ostatni commit, a ten ze środka jest pomijany. cancel-in-progress: true anulowałoby też działający job, co potrafi przerwać rsync w połowie katalogu release. Na produkcji zostaw false, a true ustawiaj w workflow testowych na Pull Requestach, gdzie liczy się tylko najnowszy commit.
concurrency:
group: deploy-production
cancel-in-progress: falseSymlink przełącza się dopiero po pełnym rsync, więc anulowany przebieg oczekujący nigdy nie dotyka current.
Ochrona produkcji przez środowiska GitHub Actions
Dodaj environment: production do joba deploy i skonfiguruj to środowisko w ustawieniach repozytorium. Działają wtedy trzy zabezpieczenia (dokumentacja GitHub: deploymenty i środowiska):
- Wymagani recenzenci: do sześciu osób lub zespołów, a jedna akceptacja zwalnia job. Z włączoną blokadą samoakceptacji osoba, która uruchomiła deploy, nie może go zatwierdzić.
- Gałęzie i tagi deployu: tylko
mainalbo tagi wydań mogą wdrażać naproduction, więc workflow z gałęzi feature nie dosięgnie serwera produkcyjnego. - Sekrety środowiska: dostępne tylko dla jobów, które wskazują środowisko, a przy wymaganych recenzentach dopiero po akceptacji. Produkcyjny
DEPLOY_SSH_KEYtrzymasz tutaj, nie w sekretach repozytorium.
Sprawdź plan, zanim na tym oprzesz proces. W GitHub Free, Pro i Team wymagani recenzenci i wait timer działają tylko w repozytoriach publicznych. Gałęzie deployu i sekrety środowiska działają w repozytoriach prywatnych na Pro i Team, ale nie na Free. Repozytoria agencji WordPress są zwykle prywatne, więc na tych planach bramki akceptacji nie ma i ochronę niosą reguły gałęzi.
Jak zabezpieczyć workflow deployu w GitHub Actions
Job deployu trzyma klucz SSH do produkcji, więc plik workflow zasługuje na takie samo review jak kod PHP. Wytyczne GitHub o bezpiecznym użyciu Actions wymieniają środki, które mają tu znaczenie:
- Pinuj zewnętrzne akcje do pełnego SHA commita. Tag typu
@v2da się przesunąć albo usunąć. Przepięcie SHA wymagałoby kolizji SHA-1. SHA bierzesz z repozytorium samej akcji, nie z forka. GitHub wskazuje dziś plik blokady zależności workflow jako mocniejszą opcję dla repozytoriów, które go wdrożą. - Daj
GITHUB_TOKENminimum. Ustawpermissions: contents: readna górze i podnoś uprawnienia per job tylko tam, gdzie krok tego wymaga. Deploy przez rsync i SSH potrzebuje wyłącznie odczytu repozytorium. - Nie wstawiaj niezaufanych danych prosto do
run:. Tytuł Pull Requestu albo nazwa gałęzi wewnątrz${{ }}w skrypcie shell to droga do wstrzyknięcia kodu. Przekaż wartość przez pośrednią zmienną środowiskową. - Jeden sekret na jedną wartość. Dane strukturalne, np. blok JSON, potrafią zepsuć maskowanie w logach, więc host, użytkownik, ścieżka i klucz to osobne sekrety.
Ta sama strona zaznacza, że maskowanie obejmuje tylko sekrety użyte w jobie i dostępne dla runnera, więc nigdy nie rób echo wartości pochodnych, np. zdekodowanego klucza.
Wdrożenie WordPressa bez przestoju przez symlink
Nadpisywanie plików „w miejscu” na public_html oznacza, że przez kilka sekund połowa requestów widzi stary functions.php i nowy template. Przy WooCommerce to potrafi zepsuć sesję koszyka.
Model atomowy:
- katalog bazowy:
/var/www/site/ - releases:
/var/www/site/releases/20260920-142211/ - shared: uploads,
wp-config.php, cache object - poza release current: symlink do aktywnego release
Po udanym rsync:
ln -sfn /var/www/site/releases/20260920-142211 /var/www/site/currentNginx root wskazuje na .../current/public (albo odpowiednik). Przełączenie jest natychmiastowe. Stare release kasujesz po N dniach albo zostawiasz ostatnie trzy na rollback.
WordPress uploads muszą być poza release - inaczej każdy deploy kasuje media albo wymaga kopiowania gigabajtów. Klasyczny układ: shared/uploads linkowany do wp-content/uploads w każdym release.
Staging WordPress i porównanie strategii wdrożeń
W 2026 roku ścieżka od gałęzi feature przez Pull Request i staging do main i produkcji nie jest opcjonalna dla sklepów i stron leadowych. Staging powinien mieć:
- tę samą wersję PHP i rozszerzeń co produkcja
- kopię bazy (anonimizowaną, jeśli są dane osobowe RODO)
- te same sekrety płatności w trybie sandbox
Preview per PR (tymczasowy URL) ma sens przy redesignie i przy zmianach CSS, mniej przy migracjach schematu bazy - te i tak testujesz na stagingu z prawdziwą kopią.
| Strategia | Ryzyko | Koszt utrzymania | Kiedy ma sens |
|---|---|---|---|
| Ręczne FTP | Bardzo wysokie | Niski start, wysoki koszt awarii | Tylko sandbox / nauka |
| Git hook na serwerze | Średnie | Niski | Małe portfolio, jeden deweloper |
| CI + rsync atomowy | Niskie | Średni | Biznes, WooCommerce, agencje |
| Blue/green lub dwa VPS | Minimalne | Wysoki | Duży ruch, SLA, płatności |
Blue/green (dwa pełne środowiska, przełączenie load balancera) rzadko opłaca się poniżej kilkuset równoległych sesji. Dla większości polskich sklepów wystarcza atomowy symlink + rollback katalogu.
Testy automatyczne w potoku CI/CD WordPressa
Potok bez testów tylko szybciej wysyła zepsuty kod. W repozytorium WordPress sprawdzają się cztery warstwy, od najszybszej do najwolniejszej:
- Testy jednostkowe w PHPUnit sprawdzają pojedyncze funkcje wtyczki lub motywu bez bazy danych. Biegną przy każdym pushu.
- Testy integracyjne ładują WordPressa z tymczasową bazą testową.
wp scaffold plugin-testsgeneruje szkielet:phpunit.xml.dist,bin/install-wp-tests.shdo instalacji pakietu testów i bazy,tests/bootstrap.php, przykładowy test i zestaw reguł.phpcs.xml.dist. Domyślny plik CI jest dla CircleCI;--ci=githubzapisuje zamiast niego konfigurację GitHub Actions (WP-CLI: wp scaffold plugin-tests). - Analiza statyczna w PHPStan i PHP_CodeSniffer łapie błędy typów i naruszenia standardu kodowania bez uruchamiania kodu.
- Testy end-to-end w Playwright przechodzą realne ścieżki użytkownika: formularz kontaktowy, koszyk i checkout WooCommerce. To najwolniejsza warstwa, więc uruchamiasz ją na Pull Requestach do
maini na stagingu, nie przy każdym commicie.
Czerwony job w dowolnej warstwie zatrzymuje workflow przed krokiem artefaktu, więc nic nie trafia na serwer.
Sekrety i migracje bazy przy wdrożeniu WordPress
Nigdy nie commituj:
- kluczy SSH
- haseł bazy
AUTH_KEY/SECURE_AUTH_KEYzwp-config.php- tokenów API bramek płatności
W GitHubie leżą w Settings, dalej Secrets and variables, dalej Actions. Wstrzykujesz je jako env w kroku deploy. Na serwerze wp-config.php leży w shared/ i nie jest częścią artefaktu z CI.
WP-CLI w potoku po przełączeniu symlinku:
wp cache flush(jeśli object cache)wp rewrite flushpo zmianie CPTwp plugin listjako smoke w logu
Jak uruchomić WP-CLI na serwerze produkcyjnym z runnera
Te komendy muszą działać na serwerze, na nowym release. WP-CLI robi to z runnera przez globalny parametr --ssh ([<scheme>:][<user>@]<host>[:<port>][<path>]) albo przez aliasy w projektowym wp-cli.yml (podręcznik WP-CLI: zdalne uruchamianie komend):
@staging:
ssh: [email protected]/var/www/site/current/public
@prod:
ssh: [email protected]/var/www/site/current/publicZ tym plikiem w repozytorium wp @prod cache flush wykonuje się na produkcji. WP-CLI nadal musi być zainstalowane na serwerze i dostępne jako wp: runner tylko przekazuje komendę przez SSH.
Kolejne komendy warte kroku po przełączeniu, wszystkie w dokumentacji komend WP-CLI:
wp core is-installednic nie wypisuje i odpowiada kodem wyjścia, więc nieudane sprawdzenie wywraca krok.wp core update-db --dry-runporównuje wersje bazy przed właściwymwp core update-dbpo aktualizacji rdzenia.wp core verify-checksumsporównuje pliki rdzenia z sumami kontrolnymi WordPress.org i wyłapuje pliki zmienione poza Gitem.wp maintenance-mode activateideactivateobsługują rzadki release, który musi zmienić schemat bazy, gdy nic do niej nie zapisuje.
Migracje bazy (custom tabele, ACF JSON już w repo) planuj osobno od deployu plików. Rollback symlinku nie cofa ALTER TABLE. Jeśli migracja jest destrukcyjna, najpierw backup (wp db export na stagingu i produkcji), potem migracja, potem dopiero przełączenie ruchu na kod, który jej wymaga - albo feature flag w PHP.
Dokumentacja instalacji zależności PHP: Composer install - w CI zawsze --no-dev na produkcji i zablokowany composer.lock.
Najczęstsze błędy w potoku CI/CD WordPress
Z praktyki wdrożeń na VPS (Hetzner, DigitalOcean, własne OVH) i na managed z SSH:
.gitignorezjadavendor/- produkcja dostaje motyw bez autoloadera. Albo budujesz vendor na runnerze i wrzucasz do artefaktu, albo uruchamiasz Composer na serwerze w kroku po rsync (wtedy serwer musi mieć Composer i sieć do Packagist).- różne
ABSPATH/ ścieżki - skrypt zakłada/var/www/html, a hosting ma/home/user/domains/.... Pinuj ścieżki w Secrets per środowisko. - opcache trzyma stary kod - po
ln -sfnwywołaj reload PHP-FPM alboopcache_resetprzez WP-CLI/mu-plugin tylko dla deploy usera. - cron i kolejki - Action Scheduler WooCommerce może odpalać joby na starym kodzie przez kilka minut; po dużym release warto chwilę obserwować failed jobs.
Smoke test po deployu (curl z runnera albo osobny job):
- strona główna HTTP 200
/wp-login.php200- jeden krytyczny URL produktu lub landing z formularzem
- opcjonalnie HEAD do CDN, jeśli assety mają hash w nazwie
Jeśli smoke padnie, automatyczny rollback symlinku jest tańszy niż telefon od klienta o 23:00.
Motywy blokowe i mu-pluginy w Gicie
Nie cały WordPress nadaje się do tego samego potoku. Treści w bazie (wpisy, ACF field groups jeśli nie eksportujesz JSON, Elementor CSS) i tak żyją poza release. CI/CD chroni kod, nie zastępuje backupu bazy.
Praktyczne podziały:
- Motyw klasyczny lub hybrydowy z Vite - pełny build w CI, artefakt z
style.css+dist/. - Block theme (FSE) - trzymasz
theme.jsoni szablony HTML w Gicie; eksport zmian z Site Editora wraca do repo przez PR, inaczej produkcja i Git rozjeżdżają się po pierwszym kliknięciu „Zapisz”. - Must-use pluginy - wgrywane razem z release; nie aktualizujesz ich z panelu. To dobre miejsce na healthcheck deployu i wyłączenie file editora.
- Wtyczki z wordpress.org - albo pinujesz wersje w Composer (
wpackagist), albo aktualizujesz je osobnym jobem z review. Mieszanie „panel kliknął Update” z „CI nadpisał katalog” kończy się losowym stanemplugins/.
Na stronach agencji w PL często widzę hybrydę: custom motyw + WooCommerce z wpackagist + dwie wtyczki premium wgrane jako zip do private/ i instalowane Composerem z path repository. Potok musi znać wszystkie trzy źródła, inaczej staging i produkcja różnią się jednym ZIP-em sprzed pół roku.
Checklista przed wdrożeniem WordPressa z GitHub Actions
Zanim podłączysz main do produkcji, przejdź tę listę raz - oszczędza weekend:
- Repozytorium prywatne albo bez sekretów w historii (
git log -pnie pokazuje starych.env). - Branch protection: wymagany PR, wymagany zielony workflow
test. - Osobne Secrets:
DEPLOY_HOST_STAGING,DEPLOY_HOST_PROD, osobne klucze SSH. - Staging zsynchronizowany z produkcją co do wersji PHP (nie „u nas działa 8.3, u klienta 8.1”).
- Backup bazy przed pierwszym atomowym przełączeniem - nawet jeśli rollback kodu jest gotowy.
- Monitoring uptime (UptimeRobot, Better Stack, cokolwiek) z alertem SMS/Slack na URL produkcyjny.
- Runbook na jednej stronie: jak ręcznie
ln -sfnna poprzedni release, kto ma dostęp do serwera.
Dopiero potem włączasz auto-deploy z main. Wcześniej wystarczy workflow „build + test” na PR i ręczny workflow_dispatch na staging.
Podsumowanie
CI/CD dla WordPressa to nie „magiczny YAML”, tylko powtarzalny kontrakt: ten sam PHP, ten sam lockfile, ten sam artefakt, przełączenie bez okna z połową plików. Zaczynasz od Gita i stagingu, dokładasz Actions z buildem Composer/npm, kończysz atomowym current i smoke testem. FTP zostaje narzędziem awaryjnym, nie procesem.
Jeśli budujesz taki potok pod sklep lub stronę leadową i potrzebujesz kogoś, kto utrzyma go razem z kodem motywu, napisz przez formularz kontaktowy - opisuję zakres wdrożenia i utrzymania CI/CD przy realnym stacku WordPress.







