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.

In This Guide

CI/CD Pipeline Architecture and Design Patterns

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

Advanced Ruby Scripting for Build Automation

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 with Matrix Strategies

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

Comprehensive Testing Strategies with Custom Ruby Tests

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

Multi-environment Deployment to Cloudflare Pages

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

Build Performance Monitoring and Optimization

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.