Automating CI/CD Workflows for Enterprise Full Site Editing (FSE) Block Themes and theme.json for Premium Gutenberg-First Themes
Establishing a Robust CI/CD Pipeline for FSE Block Themes
The advent of Full Site Editing (FSE) and the increasing reliance on the `theme.json` file for global styles and theme configuration necessitate a sophisticated CI/CD strategy. This isn’t merely about deploying code; it’s about ensuring theme integrity, style consistency, and performance across diverse environments. For premium Gutenberg-first themes, a broken CI/CD pipeline can lead to significant regressions, impacting user experience and brand perception. This guide outlines a production-ready CI/CD workflow, focusing on automated testing, linting, and deployment for FSE block themes.
Core Components of the CI/CD Workflow
Our CI/CD pipeline will be built around several key stages, each with specific tools and objectives:
- Version Control: Git (e.g., GitHub, GitLab, Bitbucket)
- CI/CD Platform: GitHub Actions (demonstrated here), GitLab CI, Jenkins
- Dependency Management: Composer, npm/yarn
- Linting & Formatting: ESLint, Stylelint, PHP_CodeSniffer (with WordPress Coding Standards), Prettier
- Testing: PHPUnit (for PHP logic), Jest/Vitest (for JavaScript components), Playwright/Cypress (for end-to-end visual regression and functional testing)
- Build & Packaging: Webpack/Vite (for JS/CSS compilation), `wp-cli` (for theme packaging)
- Deployment: SSH/SFTP, rsync, cloud provider CLI tools (AWS CLI, gcloud), managed WordPress hosting APIs
Setting Up the Development Environment and Local Workflow
Before automating, a solid local development setup is paramount. This ensures developers can catch issues early and that the CI environment accurately reflects local conditions.
Local Development Stack
A containerized environment using Docker is highly recommended. This provides consistency and isolates dependencies.
`docker-compose.yml` Example
version: '3.8'
services:
wordpress:
image: wordpress:latest
ports:
- "8080:80"
volumes:
- ./wordpress/htdocs:/var/www/html
- ./wordpress/plugins:/usr/local/wordpress/wp-content/plugins
- ./wordpress/themes:/usr/local/wordpress/wp-content/themes
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: password
WORDPRESS_DB_NAME: wordpress
WORDPRESS_TABLE_PREFIX: wp_
depends_on:
- db
networks:
- wp-network
db:
image: mysql:8.0
ports:
- "33060:3306"
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: password
networks:
- wp-network
volumes:
db_data:
networks:
wp-network:
With this setup, running docker-compose up -d will spin up a WordPress instance accessible at http://localhost:8080. Your theme code should be mounted into the ./wordpress/themes directory.
Local Scripting for Development Tasks
Create a scripts/ directory in your theme’s root for common development tasks.
`scripts/setup-dev.sh`
#!/bin/bash
# Ensure Docker is running
if ! docker info >& /dev/null; then
echo "Docker is not running. Please start Docker and try again."
exit 1
fi
# Check if docker-compose is available
if ! command -v docker-compose >& /dev/null; then
echo "docker-compose could not be found. Please install it."
exit 1
fi
echo "Starting WordPress development environment..."
docker-compose up -d
echo "Waiting for WordPress and MySQL to be ready..."
# Basic check, more robust checks might be needed
sleep 30
echo "Installing WordPress and theme via WP-CLI..."
# Mount WP-CLI into the container for execution
docker-compose exec wordpress bash -c "wp core install --url=http://localhost:8080 --title='Dev Theme Site' --admin_user=admin --admin_password=password [email protected] --skip-email"
docker-compose exec wordpress bash -c "wp theme activate your-theme-slug --allow-root" # Replace 'your-theme-slug'
echo "Installing Composer dependencies..."
docker-compose exec wordpress bash -c "cd /var/www/html/wp-content/themes/your-theme-slug && composer install" # Adjust path if needed
echo "Installing Node.js dependencies..."
cd wordpress/themes/your-theme-slug && npm install && cd ../../..
echo "Development environment setup complete. Access at http://localhost:8080"
echo "Admin: admin / password"
Make this script executable: chmod +x scripts/setup-dev.sh. Running ./scripts/setup-dev.sh will set up your local environment.
Automating Linting and Formatting
Consistency in code style is crucial for maintainability and collaboration. We’ll integrate linters for PHP, JavaScript, and CSS.
PHP Linting with PHP_CodeSniffer
Install PHP_CodeSniffer and the WordPress Coding Standards.
composer require --dev squizlabs/php_codesniffer wp-coding-standards/wpcs phpcs --config-set installed_paths vendor/wp-coding-standards/wpcs
Create a phpcs.xml configuration file in your theme’s root:
<?xml version="1.0"?>
<ruleset name="WordPress Theme">
<description>Coding standard for WordPress themes.</description>
<file>./</file>
<exclude-pattern>./vendor/</exclude-pattern>
<exclude-pattern>./node_modules/</exclude-pattern>
<exclude-pattern>./build/</exclude-pattern>
<exclude-pattern>./dist/</exclude-pattern>
<rule ref="WordPress"/>
<rule ref="WordPress-Core"/>
<rule ref="WordPress-Extra"/>
<rule ref="WordPress-VIP"/>
<!-- For FSE themes, we might need to adjust rules related to PHP template files -->
<!-- Example: Allowing certain constructs in block templates -->
<rule ref="WordPress.Functions.discouraged_functions">
<exclude-pattern>*.php</exclude-pattern> <!-- Adjust as needed -->
</rule>
<!-- Specific rules for theme.json validation if needed, though often handled by JS -->
</ruleset>
Add a script to your package.json:
"scripts": {
// ... other scripts
"lint:php": "phpcs --standard=phpcs.xml ."
}
JavaScript/CSS Linting and Formatting with ESLint & Prettier
Ensure your JavaScript and CSS adhere to modern standards. Prettier handles auto-formatting.
npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-prettier @wordpress/eslint-plugin
Create an .eslintrc.js file:
module.exports = {
root: true,
parser: '@babel/eslint-parser', // Or '@typescript-eslint/parser' if using TypeScript
extends: [
'eslint:recommended',
'plugin:@wordpress/eslint-plugin/recommended', // WordPress JS standards
'prettier', // Use Prettier to override ESLint rules it handles
],
plugins: ['prettier', '@wordpress/eslint-plugin'],
env: {
browser: true,
node: true,
es6: true,
'shared-node-environment': true, // For WP-CLI and build tools
},
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
requireConfigFile: false, // If not using Babel config file
},
rules: {
// Add or override specific rules here
'prettier/prettier': 'error',
'@wordpress/no-unsafe-i18n-placeholders': 'warn', // Example: Be cautious with i18n
// Add rules for React/JSX if using them in blocks
},
settings: {
// If using React
react: {
version: 'detect',
},
},
};
Create a .prettierrc.js file:
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 80,
tabWidth: 2,
useTabs: false,
// Add specific WordPress/Gutenberg related prettier options if available/needed
};
Add scripts to package.json:
"scripts": {
// ... other scripts
"lint:js": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:css": "stylelint 'style.css' 'assets/css/**/*.css'", // Adjust path
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,html,yml,yaml,md}\""
}
Automated Testing Strategies
Comprehensive testing is non-negotiable. We’ll cover unit tests for PHP logic, JavaScript component tests, and end-to-end visual regression tests.
PHP Unit Tests
For any custom PHP functions, classes, or complex logic within your theme (e.g., custom block render callbacks, utility functions), PHPUnit is essential. Ensure you have a phpunit.xml configured for WordPress.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Theme Unit Tests">
<directory>./tests/php/</directory>
</testsuite>
</testsuites>
<php>
<!-- Define constants or global variables needed by WordPress -->
<const name="WP_TESTS_DIR" value="vendor/wordpress-tests-lib/includes"/>
<const name="WP_PLUGIN_DIR" value="vendor/wordpress/wordpress/wp-content/plugins"/>
<const name="WP_CONTENT_DIR" value="vendor/wordpress/wordpress/wp-content"/>
<const name="ABSPATH" value="vendor/wordpress/wordpress/"/>
</php>
</phpunit>
The tests/bootstrap.php file typically sets up the WordPress test environment. You’ll need to install the WordPress test suite via Composer:
composer require --dev phpunit/phpunit --prefer-dist composer require --dev wp-testing/wp-browser --prefer-dist # For WP Browser integration # Manually clone or use a script to get wordpress-tests-lib # e.g., git clone https://github.com/WordPress/wordpress-develop.git vendor/wordpress # mv vendor/wordpress/tests/phpunit vendor/wordpress-tests-lib
Add a script to package.json:
"scripts": {
// ... other scripts
"test:php": "phpunit"
}
JavaScript Unit/Integration Tests (Jest/Vitest)
For JavaScript blocks and components, Jest or Vitest provide excellent testing frameworks. Vitest is often faster and integrates well with Vite build tools.
npm install --save-dev vitest @vitejs/plugin-react # If using React # Or for Jest: # npm install --save-dev jest @babel/preset-react @babel/preset-env babel-jest
Create a vitest.config.js (or jest.config.js):
// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react'; // If using React
export default defineConfig({
plugins: [react()], // If using React
test: {
globals: true, // Make global functions like describe, it available
environment: 'jsdom', // Simulate a browser environment
setupFiles: ['./tests/vitest.setup.js'], // Optional setup file
alias: {
// Map paths for easier imports, e.g., '@components': './src/components'
},
},
});
Create a tests/vitest.setup.js if needed:
// tests/vitest.setup.js
// Mock WordPress global objects if necessary
// global.wp = { ... };
// global.wpApiSettings = { ... };
// import '@testing-library/jest-dom'; // If using testing-library
Add a script to package.json:
"scripts": {
// ... other scripts
"test:js": "vitest run" // or "jest"
}
End-to-End (E2E) Visual Regression Testing (Playwright)
For FSE themes, visual consistency is paramount. E2E tests with visual regression capabilities ensure that changes don’t break the UI unexpectedly. Playwright is a powerful choice.
npm install --save-dev @playwright/test npx playwright install
Create a playwright.config.js:
// playwright.config.js
const { defineConfig } = require('@playwright/test');
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: './tests/e2e', // Directory for your E2E tests
timeout: 60 * 1000, // 60 seconds timeout per test
expect: {
timeout: 5000, // Timeout for expect assertions
},
fullyParallel: true, // Run tests in parallel
forbidOnly: !!process.env.CI, // Fail build if tests are marked as only
retries: process.env.CI ? 2 : 0, // Retry on CI
workers: process.env.CI ? 1 : undefined, // Use 1 worker on CI for stability
reporter: 'html', // Generate HTML report
use: {
// Base URL for your site
baseURL: 'http://localhost:8080', // Ensure this matches your dev environment
/* base URL for all tests */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' },
},
// Add other browsers if needed: { name: 'firefox', use: { browserName: 'firefox' } },
// { name: 'webkit', use: { browserName: 'webkit' } },
],
// Optional: Define global setup/teardown for E2E tests
// globalSetup: require.resolve('./tests/e2e/global-setup.js'),
// globalTeardown: require.resolve('./tests/e2e/global-teardown.js'),
});
Create a sample E2E test file (e.g., tests/e2e/theme-settings.spec.js):
// tests/e2e/theme-settings.spec.js
import { test, expect } from '@playwright/test';
test.describe('FSE Theme Settings', () => {
test('should load and save global styles', async ({ page }) => {
// Navigate to the site editor or a specific page where styles are applied
await page.goto('/wp-admin/site-editor.php'); // Adjust URL as needed
// Log in if necessary (often handled by global setup or separate login test)
// await page.fill('#username', 'admin');
// await page.fill('#password', 'password');
// await page.click('button[type="submit"]');
// Wait for the editor to load
await page.waitForSelector('.edit-site-layout');
// Example: Interact with a style control (this is highly theme-dependent)
// This is a placeholder; actual selectors will vary greatly.
// You might need to inspect the DOM of the site editor.
// await page.click('button:has-text("Styles")');
// await page.click('button:has-text("Colors")');
// await page.click('button:has-text("Primary")');
// await page.fill('input[type="color"]', '#ff0000'); // Set to red
// Take a screenshot for visual regression
await expect(page).toHaveScreenshot('global-styles-primary-color.png', {
fullPage: true,
// You might need to mask dynamic elements or specific areas
// mask: [page.locator('.some-dynamic-element')]
});
// Save changes (if applicable and testable)
// await page.click('button:has-text("Save")');
// await expect(page.locator('.components-notice.is-success')).toBeVisible();
});
test('should render a specific block correctly', async ({ page }) => {
// Navigate to a page where a custom block is used
await page.goto('/sample-page/'); // Assuming a sample page exists
// Wait for the block to render
await page.waitForSelector('.wp-block-your-theme-custom-block'); // Replace with your block's selector
// Take a screenshot of the block
await expect(page.locator('.wp-block-your-theme-custom-block')).toHaveScreenshot('custom-block-rendering.png');
});
});
Add a script to package.json:
"scripts": {
// ... other scripts
"test:e2e": "playwright test"
}
Building and Packaging the Theme
The build process typically involves compiling JavaScript and CSS, optimizing assets, and potentially creating a distributable zip file.
Asset Compilation (Vite/Webpack)
If your theme uses modern JS/CSS features or requires bundling, a build tool is necessary. Vite is a modern, fast alternative to Webpack.
npm install --save-dev vite @vitejs/plugin-legacy # For older browser support
Create a vite.config.js:
// vite.config.js
import { defineConfig, splitVendorChunkPlugin } from 'vite';
import legacy from '@vitejs/plugin-legacy';
// Determine the theme directory relative to the build process
// This might need adjustment based on your project structure
const themeDir = './'; // Assuming vite.config.js is in the theme root
export default defineConfig({
plugins: [
// splitVendorChunkPlugin(), // Splits vendor code into a separate chunk
legacy({
targets: ['defaults', 'not IE 11'], // Target modern browsers + IE 11
}),
],
root: themeDir, // Set the root directory for Vite
base: '/wp-content/themes/your-theme-slug/assets/', // Base path for assets in WordPress
build: {
outDir: themeDir + 'assets/dist', // Output directory for built assets
assetsDir: '.', // Keep assets in the root of outDir
manifest: true, // Generate manifest.json for asset mapping
rollupOptions: {
input: {
// Entry points for your JS and CSS
main: themeDir + 'assets/js/main.js',
editor: themeDir + 'assets/js/editor.js', // For block editor specific JS
style: themeDir + 'style.scss', // Main theme stylesheet
// block_styles: themeDir + 'assets/css/block-styles.scss', // Example for block specific styles
},
output: {
entryFileNames: '[name]-[hash].js',
chunkFileNames: 'chunks/[name]-[hash].js',
assetFileNames: '[name]-[hash].[ext]',
},
},
emptyOutDir: true, // Clean the output directory before building
},
server: {
// Configure the development server if needed, e.g., for hot module replacement
// This is less critical if you're primarily using Docker for local dev
// hmr: {
// protocol: 'ws',
// host: 'localhost',
// },
},
resolve: {
alias: {
// Define aliases for easier imports
'@': themeDir + 'assets/js/',
},
},
});
Add scripts to package.json:
"scripts": {
// ... other scripts
"dev": "vite", // For local development server
"build": "vite build"
}
Theme Packaging (`wp-cli`)
For distribution (e.g., to the WordPress.org theme repository or for client delivery), packaging the theme into a zip file is standard. `wp-cli` can automate this.
# Ensure wp-cli is installed and accessible # You might need to run this within your Docker container or have it installed globally # Example command assuming you are in the theme's parent directory and wp-cli is available: wp theme package your-theme-slug --version=1.2.3 --output-dir=./packages
Add a script to package.json:
"scripts": {
// ... other scripts
"package": "wp theme package your-theme-slug --version=$(node -p "require('./package.json').version") --output-dir=./packages"
}
GitHub Actions Workflow Configuration
Now, let’s orchestrate these steps into a GitHub Actions workflow. Create a file named .github/workflows/ci.yml in your repository.
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
build_and_test:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.0', '8.1', '8.2']
wordpress-version: ['latest', '6.2', '6.3'] # Test against specific WP versions
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: gd, mbstring, xml, zip, intl
tools: composer, phpcs, phpunit
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18' # Or your preferred Node.js version
cache: 'npm' # Cache npm dependencies
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Install WordPress test environment
run: |
mkdir -p vendor/wordpress-tests-lib/includes
wget -qO- https://github.com/WordPress/wordpress-develop/archive/refs/tags/6.3.zip | jar xf - -C vendor/wordpress-develop --strip-components=1
mv vendor/wordpress-develop/tests/phpunit vendor/wordpress-tests-lib/includes
rm -rf vendor/wordpress-develop # Clean up
- name: Setup WordPress for PHPUnit
run: |
# Create a dummy wp-config.php for PHPUnit
cp vendor/wordpress-tests-lib/includes/wp-config-sample.php vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'DB_NAME', 'test_db' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'DB_USER', 'test_user' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'DB_PASSWORD', 'test_pass' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'DB_HOST', 'localhost' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_TESTS_DOMAIN', 'example.com' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_TESTS_EMAIL', '[email protected]' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_TESTS_TITLE', 'Test Blog' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_MEMORY_LIMIT', '256M' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_DEBUG', true );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_DEBUG_LOG', false );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_DEBUG_DISPLAY', false );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'SAVEQUERIES', true );" >> vendor/wordpress-tests-lib/includes/wp-config.php
echo "define( 'WP_LANG_DIR', '' );" >> vendor/wordpress-tests-lib/includes/wp-config.php
# Create dummy WordPress core files if not using a Composer package for WP core
# This is a simplified approach; consider using a dedicated WP Docker image for tests
mkdir -p vendor/wordpress/wordpress/wp-includes
touch vendor/wordpress/wordpress/wp-load.php
touch vendor/wordpress/wordpress/wp-settings.php
# ... more dummy files might be needed depending on your PHPUnit setup
- name: Run PHP Linting
run: composer run lint:php
- name: Run PHPUnit Tests
run: vendor/bin/phpunit --configuration phpunit.xml --teamcity # Use --teamcity for better CI output
- name: Install Node.js dependencies
run: npm install
- name: Run JS Linting
run: npm run lint:js
- name: Run CSS Linting
run: npm run lint:css
- name: Build Assets
run: npm run build
env:
NODE_ENV: production
# E2E tests require a running WordPress instance.
# This is more complex and often involves spinning up a temporary Docker environment
# or using a dedicated testing service. For simplicity, we'll skip E2E in this basic example,
# but in a real-world scenario, you'd integrate Playwright here.
# Example:
# - name: Setup Docker Compose for E2E
# uses: isbang/compose-action@v1
# with:
# compose-file: "docker-compose.yml" # Your compose file
# - name: Run E2E Tests
# run: npm run test:e2e
# env:
# PLAYWRIGHT_BROWSERS_PATH: 0 # Ensure browsers are downloaded
- name: Upload Build Artifacts (Optional)
uses: actions/upload-artifact@v3
with:
name: theme-assets
path: assets/dist/ # Path to your built assets
- name: Package Theme (Optional, for releases)
if: github.ref == 'refs/heads/main' && github.event_name == 'push' # Only on push to main branch
run: |
npm run package
# Upload the generated zip file as an artifact
# You might also want to deploy it here
echo "Theme packaged successfully."
env:
WP_CLI_VERSION: 2.7.1 # Ensure WP-CLI is available or install it
# Add any necessary environment variables for WP-CLI if it needs them
Important Considerations for CI/CD: