Modern Jekyll development requires robust CI/CD pipelines that automate testing, building, and deployment while ensuring quality and performance. By combining GitHub Actions with custom Ruby scripting and Cloudflare Pages, you can create enterprise-grade deployment pipelines that handle complex build processes, run comprehensive tests, and deploy with zero downtime. This guide explores advanced pipeline patterns that leverage Ruby's power for custom build logic, GitHub Actions for orchestration, and Cloudflare for global deployment.
A sophisticated CI/CD pipeline for Jekyll involves multiple stages that ensure code quality, build reliability, and deployment safety. The architecture separates concerns while maintaining efficient execution flow from code commit to production deployment.
The pipeline comprises parallel testing stages, conditional build processes, and progressive deployment strategies. Ruby scripts handle complex logic like dynamic configuration, content validation, and build optimization. GitHub Actions orchestrates the entire process with matrix builds for different environments, while Cloudflare Pages provides the deployment platform with built-in rollback capabilities and global CDN distribution.
# Pipeline Architecture:
# 1. Code Push → GitHub Actions Trigger
# 2. Parallel Stages:
# - Unit Tests (Ruby RSpec)
# - Integration Tests (Custom Ruby)
# - Security Scanning (Ruby scripts)
# - Performance Testing (Lighthouse CI)
# 3. Build Stage:
# - Dynamic Configuration (Ruby)
# - Content Processing (Jekyll + Ruby plugins)
# - Asset Optimization (Ruby pipelines)
# 4. Deployment Stages:
# - Staging → Cloudflare Pages (Preview)
# - Production → Cloudflare Pages (Production)
# - Rollback Automation (Ruby + GitHub API)
# Required GitHub Secrets:
# - CLOUDFLARE_API_TOKEN
# - CLOUDFLARE_ACCOUNT_ID
# - RUBY_GEMS_TOKEN
# - CUSTOM_BUILD_SECRETS
Ruby scripts provide the intelligence for complex build processes, handling tasks that exceed Jekyll's native capabilities. These scripts manage dynamic configuration, content validation, and build optimization.
Here's a comprehensive Ruby build automation script:
#!/usr/bin/env ruby
# scripts/advanced_build.rb
require 'fileutils'
require 'yaml'
require 'json'
require 'net/http'
require 'time'
class JekyllBuildOrchestrator
def initialize(branch, environment)
@branch = branch
@environment = environment
@build_start = Time.now
@metrics = {}
end
def execute
log "Starting build for #{@branch} in #{@environment} environment"
# Pre-build validation
validate_environment
validate_content
# Dynamic configuration
generate_environment_config
process_external_data
# Optimized build process
run_jekyll_build
# Post-build processing
optimize_assets
generate_build_manifest
deploy_to_cloudflare
log "Build completed successfully in #{Time.now - @build_start} seconds"
rescue => e
log "Build failed: #{e.message}"
exit 1
end
private
def validate_environment
log "Validating build environment..."
# Check required tools
%w[jekyll ruby node].each do |tool|
unless system("which #{tool} > /dev/null 2>&1")
raise "Required tool #{tool} not found"
end
end
# Verify configuration files
required_configs = ['_config.yml', 'Gemfile']
required_configs.each do |config|
unless File.exist?(config)
raise "Required configuration file #{config} not found"
end
end
@metrics[:environment_validation] = Time.now - @build_start
end
def validate_content
log "Validating content structure..."
# Validate front matter in all posts
posts_dir = '_posts'
if File.directory?(posts_dir)
Dir.glob(File.join(posts_dir, '**/*.md')).each do |post_path|
validate_post_front_matter(post_path)
end
end
# Validate data files
data_dir = '_data'
if File.directory?(data_dir)
Dir.glob(File.join(data_dir, '**/*.{yml,yaml,json}')).each do |data_file|
validate_data_file(data_file)
end
end
@metrics[:content_validation] = Time.now - @build_start - @metrics[:environment_validation]
end
def validate_post_front_matter(post_path)
content = File.read(post_path)
if content =~ /^---\s*\n(.*?)\n---\s*\n/m
front_matter = YAML.safe_load($1)
required_fields = ['title', 'date']
required_fields.each do |field|
unless front_matter&.key?(field)
raise "Post #{post_path} missing required field: #{field}"
end
end
# Validate date format
if front_matter['date']
begin
Date.parse(front_matter['date'].to_s)
rescue ArgumentError
raise "Invalid date format in #{post_path}: #{front_matter['date']}"
end
end
else
raise "Invalid front matter in #{post_path}"
end
end
def generate_environment_config
log "Generating environment-specific configuration..."
base_config = YAML.load_file('_config.yml')
# Environment-specific overrides
env_config = {
'url' => environment_url,
'google_analytics' => environment_analytics_id,
'build_time' => @build_start.iso8601,
'environment' => @environment,
'branch' => @branch
}
# Merge configurations
final_config = base_config.merge(env_config)
# Write merged configuration
File.write('_config.build.yml', final_config.to_yaml)
@metrics[:config_generation] = Time.now - @build_start - @metrics[:content_validation]
end
def environment_url
case @environment
when 'production'
'https://yourdomain.com'
when 'staging'
"https://#{@branch}.yourdomain.pages.dev"
else
'http://localhost:4000'
end
end
def run_jekyll_build
log "Running Jekyll build..."
build_command = "bundle exec jekyll build --config _config.yml,_config.build.yml --trace"
unless system(build_command)
raise "Jekyll build failed"
end
@metrics[:jekyll_build] = Time.now - @build_start - @metrics[:config_generation]
end
def optimize_assets
log "Optimizing build assets..."
# Optimize images
optimize_images
# Compress HTML, CSS, JS
compress_assets
# Generate brotli compressed versions
generate_compressed_versions
@metrics[:asset_optimization] = Time.now - @build_start - @metrics[:jekyll_build]
end
def deploy_to_cloudflare
return if @environment == 'development'
log "Deploying to Cloudflare Pages..."
# Use Wrangler for deployment
deploy_command = "npx wrangler pages publish _site --project-name=your-project --branch=#{@branch}"
unless system(deploy_command)
raise "Cloudflare Pages deployment failed"
end
@metrics[:deployment] = Time.now - @build_start - @metrics[:asset_optimization]
end
def generate_build_manifest
manifest = {
build_id: ENV['GITHUB_RUN_ID'] || 'local',
timestamp: @build_start.iso8601,
environment: @environment,
branch: @branch,
metrics: @metrics,
commit: ENV['GITHUB_SHA'] || `git rev-parse HEAD`.chomp
}
File.write('_site/build-manifest.json', JSON.pretty_generate(manifest))
end
def log(message)
puts "[#{Time.now.strftime('%H:%M:%S')}] #{message}"
end
end
# Execute build
if __FILE__ == $0
branch = ARGV[0] || 'main'
environment = ARGV[1] || 'production'
orchestrator = JekyllBuildOrchestrator.new(branch, environment)
orchestrator.execute
end
GitHub Actions workflows orchestrate the entire CI/CD process using matrix strategies for parallel testing and conditional deployments. The workflows integrate Ruby scripts and handle complex deployment scenarios.
# .github/workflows/ci-cd.yml
name: Jekyll CI/CD Pipeline
on:
push:
branches: [ main, develop, feature/* ]
pull_request:
branches: [ main ]
env:
RUBY_VERSION: '3.1'
NODE_VERSION: '18'
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.0', '3.1']
node: ['16', '18']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: $
bundler-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: $
cache: 'npm'
- name: Install dependencies
run: |
bundle install
npm ci
- name: Run Ruby tests
run: |
bundle exec rspec spec/
- name: Run custom Ruby validations
run: |
ruby scripts/validate_content.rb
ruby scripts/check_links.rb
- name: Security scan
run: |
bundle audit check --update
ruby scripts/security_scan.rb
build:
name: Build and Test
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: $
bundler-cache: true
- name: Run advanced build script
run: |
chmod +x scripts/advanced_build.rb
ruby scripts/advanced_build.rb $ staging
env:
CLOUDFLARE_API_TOKEN: $
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v10
with:
uploadArtifacts: true
temporaryPublicStorage: true
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: jekyll-build-$
path: _site/
retention-days: 7
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: jekyll-build-$
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: $
accountId: $
projectName: 'your-jekyll-site'
directory: '_site'
branch: $
- name: Run smoke tests
run: |
ruby scripts/smoke_tests.rb https://$.your-site.pages.dev
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
if: github.ref == 'refs/heads/main'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: jekyll-build-$
- name: Final validation
run: |
ruby scripts/final_validation.rb _site
- name: Deploy to Production
uses: cloudflare/pages-action@v1
with:
apiToken: $
accountId: $
projectName: 'your-jekyll-site'
directory: '_site'
branch: 'main'
# Enable rollback on failure
failOnError: true
Custom Ruby tests provide validation beyond standard unit tests, covering content quality, link integrity, and performance benchmarks.
# spec/content_validator_spec.rb
require 'rspec'
require 'yaml'
require 'nokogiri'
describe 'Content Validation' do
before(:all) do
@posts_dir = '_posts'
@pages_dir = ''
end
describe 'Post front matter' do
it 'validates all posts have required fields' do
Dir.glob(File.join(@posts_dir, '**/*.md')).each do |post_path|
content = File.read(post_path)
if content =~ /^---\s*\n(.*?)\n---\s*\n/m
front_matter = YAML.safe_load($1)
expect(front_matter).to have_key('title'), "Missing title in #{post_path}"
expect(front_matter).to have_key('date'), "Missing date in #{post_path}"
expect(front_matter['date']).to be_a(Date), "Invalid date in #{post_path}"
end
end
end
end
end
# scripts/link_checker.rb
#!/usr/bin/env ruby
require 'net/http'
require 'uri'
require 'nokogiri'
class LinkChecker
def initialize(site_directory)
@site_directory = site_directory
@broken_links = []
end
def check
html_files = Dir.glob(File.join(@site_directory, '**/*.html'))
html_files.each do |html_file|
check_file_links(html_file)
end
report_results
end
private
def check_file_links(html_file)
doc = File.open(html_file) { |f| Nokogiri::HTML(f) }
doc.css('a[href]').each do |link|
href = link['href']
next if skip_link?(href)
if external_link?(href)
check_external_link(href, html_file)
else
check_internal_link(href, html_file)
end
end
end
def check_external_link(url, source_file)
uri = URI.parse(url)
begin
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(Net::HTTP::Head.new(uri))
end
unless response.is_a?(Net::HTTPSuccess)
@broken_links << {
url: url,
source: source_file,
status: response.code,
type: 'external'
}
end
rescue => e
@broken_links << {
url: url,
source: source_file,
error: e.message,
type: 'external'
}
end
end
def report_results
if @broken_links.any?
puts "Found #{@broken_links.size} broken links:"
@broken_links.each do |link|
puts " - #{link[:url]} in #{link[:source]}"
end
exit 1
else
puts "All links are valid!"
end
end
end
LinkChecker.new('_site').check if __FILE__ == $0
Cloudflare Pages supports sophisticated deployment patterns with preview deployments for branches and automatic production deployments from main. Ruby scripts enhance this with custom routing and environment configuration.
# scripts/cloudflare_deploy.rb
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'fileutils'
class CloudflareDeployer
def initialize(api_token, account_id, project_name)
@api_token = api_token
@account_id = account_id
@project_name = project_name
@base_url = "https://api.cloudflare.com/client/v4/accounts/#{@account_id}/pages/projects/#{@project_name}"
end
def deploy(directory, branch, environment = 'production')
# Create deployment
deployment_id = create_deployment(directory, branch)
# Wait for deployment to complete
wait_for_deployment(deployment_id)
# Configure environment-specific settings
configure_environment(deployment_id, environment)
deployment_id
end
def create_deployment(directory, branch)
# Upload directory to Cloudflare Pages
puts "Creating deployment for branch #{branch}..."
# Use Wrangler CLI for deployment
result = `npx wrangler pages publish #{directory} --project-name=#{@project_name} --branch=#{branch} --json`
deployment_data = JSON.parse(result)
deployment_data['id']
end
def configure_environment(deployment_id, environment)
# Set environment variables and headers
env_vars = environment_variables(environment)
env_vars.each do |key, value|
set_environment_variable(deployment_id, key, value)
end
end
def environment_variables(environment)
case environment
when 'production'
{
'ENVIRONMENT' => 'production',
'GOOGLE_ANALYTICS_ID' => ENV['PROD_GA_ID'],
'API_BASE_URL' => 'https://api.yourdomain.com'
}
when 'staging'
{
'ENVIRONMENT' => 'staging',
'GOOGLE_ANALYTICS_ID' => ENV['STAGING_GA_ID'],
'API_BASE_URL' => 'https://staging-api.yourdomain.com'
}
else
{
'ENVIRONMENT' => environment,
'API_BASE_URL' => 'https://dev-api.yourdomain.com'
}
end
end
end
Monitoring build performance helps identify bottlenecks and optimize the CI/CD pipeline. Ruby scripts collect metrics and generate reports for continuous improvement.
# scripts/performance_monitor.rb
#!/usr/bin/env ruby
require 'benchmark'
require 'json'
require 'fileutils'
class BuildPerformanceMonitor
def initialize
@metrics = {
build_times: [],
asset_sizes: {},
step_durations: {}
}
@current_build = {}
end
def track_build
@current_build[:start_time] = Time.now
yield
@current_build[:end_time] = Time.now
@current_build[:duration] = @current_build[:end_time] - @current_build[:start_time]
record_build_metrics
generate_report
end
def track_step(step_name)
start_time = Time.now
result = yield
duration = Time.now - start_time
@current_build[:steps] ||= {}
@current_build[:steps][step_name] = duration
result
end
private
def record_build_metrics
@metrics[:build_times] << @current_build[:duration]
# Keep only last 100 builds
@metrics[:build_times] = @metrics[:build_times].last(100)
# Record asset sizes
if Dir.exist?('_site')
@current_build[:asset_sizes] = calculate_asset_sizes
end
# Save metrics to file
save_metrics
end
def calculate_asset_sizes
sizes = {}
%w[css js images].each do |asset_type|
dir = "_site/assets/#{asset_type}"
if Dir.exist?(dir)
total_size = Dir[File.join(dir, '**', '*')].sum { |f| File.size(f) }
sizes[asset_type] = total_size
end
end
sizes
end
def generate_report
report = {
current_build: @current_build,
historical_metrics: @metrics,
recommendations: generate_recommendations
}
File.write('build-performance.json', JSON.pretty_generate(report))
puts "Build Performance Report:"
puts " Total duration: #{@current_build[:duration].round(2)}s"
puts " Steps:"
@current_build[:steps]&.each do |step, duration|
puts " #{step}: #{duration.round(2)}s"
end
end
def generate_recommendations
recommendations = []
avg_build_time = @metrics[:build_times].sum / @metrics[:build_times].size
if @current_build[:duration] > avg_build_time * 1.2
recommendations << "Build time increased by #{((@current_build[:duration] / avg_build_time - 1) * 100).round(2)}%"
end
# Check for large assets
@current_build[:asset_sizes]&.each do |type, size|
if size > 5_000_000 # 5MB
recommendations << "Large #{type} assets: #{(size / 1_000_000.0).round(2)}MB - consider optimization"
end
end
recommendations
end
def save_metrics
FileUtils.mkdir_p('_data')
File.write('_data/build_metrics.json', JSON.pretty_generate(@metrics))
end
end
# Usage in build script
monitor = BuildPerformanceMonitor.new
monitor.track_build do
monitor.track_step('content_validation') { validate_content }
monitor.track_step('jekyll_build') { run_jekyll_build }
monitor.track_step('asset_optimization') { optimize_assets }
end
This advanced CI/CD pipeline transforms Jekyll development with enterprise-grade automation, comprehensive testing, and reliable deployments. By combining Ruby's scripting power, GitHub Actions' orchestration capabilities, and Cloudflare's global platform, you achieve rapid, safe, and efficient deployments for any scale of Jekyll project.