Skip to content

Instantly share code, notes, and snippets.

@SolomonHD
Last active July 21, 2025 01:49
Show Gist options
  • Save SolomonHD/3450847185c4749b7d7612d9725cef44 to your computer and use it in GitHub Desktop.
Save SolomonHD/3450847185c4749b7d7612d9725cef44 to your computer and use it in GitHub Desktop.
Jekyll Deploy Script
#!/bin/bash
# jekyll-ghpages-deploy
# Script to build Jekyll site locally and deploy to gh-pages branch
# Run this script from the root of your repository
#
# Documentation: https://gist.github.com/SolomonHD/1391d973b86c0aeb3e901cceeb34650f
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Configuration
readonly SOURCE_BRANCH="main"
readonly DEPLOY_BRANCH="gh-pages"
readonly DOCS_FOLDER="docs"
readonly BUILD_DIR="_site"
readonly SOURCE_DIR="src"
readonly REMOTE_NAME="origin"
readonly COMMIT_MESSAGE_PREFIX="Deploy"
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly NC='\033[0m' # No Color
# Global variables
FORCE_BUILD=false
SKIP_CHECKS=false
FORCE_REBUILD=false
CURRENT_BRANCH=""
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${CYAN}[STEP]${NC} $1"
}
# Show help message
show_help() {
echo "Usage: $0 [OPTIONS]"
echo
echo "Options:"
echo " -f, --force Force build even with uncommitted changes"
echo " --skip-checks Skip prerequisite checks"
echo " --rebuild Force Pages rebuild after deployment"
echo " -h, --help Show this help message"
echo
echo "This script builds your Jekyll site and deploys it to the gh-pages branch."
echo "Make sure you have run jekyll-ghpages-setup first to create the deployment branch."
}
# Parse command line arguments
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
--force|-f)
FORCE_BUILD=true
shift
;;
--skip-checks)
SKIP_CHECKS=true
shift
;;
--rebuild)
FORCE_REBUILD=true
shift
;;
--help|-h)
show_help
exit 0
;;
*)
log_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
}
# Check prerequisites
check_prerequisites() {
log_step "Checking prerequisites..."
# Check if in git repo
if ! git rev-parse --git-dir > /dev/null 2>&1; then
log_error "Not in a git repository"
exit 1
fi
# Check if Jekyll is installed
if ! command -v jekyll &> /dev/null; then
log_error "Jekyll is not installed. Please install with: gem install jekyll bundler"
exit 1
fi
# Check if bundle is available
if ! command -v bundle &> /dev/null; then
log_error "Bundler is not installed. Please install with: gem install bundler"
exit 1
fi
# Check if source directory exists
if [[ ! -d "$SOURCE_DIR" ]]; then
log_error "Source directory '$SOURCE_DIR' not found"
log_info "Expected structure: $SOURCE_DIR/_config.yml, $SOURCE_DIR/index.md, etc."
exit 1
fi
# Check if Gemfile exists
if [[ ! -f "$SOURCE_DIR/Gemfile" ]]; then
log_warning "No Gemfile found in $SOURCE_DIR. Creating basic Gemfile..."
create_gemfile
fi
log_success "All prerequisites met"
}
# Create basic Gemfile if it doesn't exist
create_gemfile() {
cat > "$SOURCE_DIR/Gemfile" << 'EOF'
source "https://rubygems.org"
gem "jekyll", "~> 4.3"
gem "kramdown-parser-gfm"
group :jekyll_plugins do
gem "jekyll-feed"
gem "jekyll-sitemap"
end
EOF
log_info "Created basic Gemfile in $SOURCE_DIR"
}
# Check working directory status
check_working_directory() {
log_step "Checking working directory status..."
if ! git diff-index --quiet HEAD --; then
log_warning "You have uncommitted changes in your working directory"
echo
git status --porcelain
echo
if [[ "$FORCE_BUILD" == false ]]; then
read -p "Continue with deployment? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Deployment aborted by user"
exit 0
fi
else
log_info "Forcing deployment with uncommitted changes"
fi
fi
# Get current branch
CURRENT_BRANCH=$(git branch --show-current)
log_info "Current branch: $CURRENT_BRANCH"
}
# Install dependencies and build Jekyll site
build_site() {
log_step "Building Jekyll site..."
cd "$SOURCE_DIR"
# Install dependencies
log_info "Installing dependencies..."
if ! bundle install --quiet; then
log_error "Failed to install dependencies"
exit 1
fi
# Clean previous build
if [[ -d "../$BUILD_DIR" ]]; then
log_info "Cleaning previous build..."
rm -rf "../$BUILD_DIR"
fi
# Build site
log_info "Building site with Jekyll..."
if ! bundle exec jekyll build --destination "../$BUILD_DIR" --quiet; then
log_error "Jekyll build failed"
exit 1
fi
cd ..
# Verify build output
if [[ ! -d "$BUILD_DIR" ]] || [[ ! -f "$BUILD_DIR/index.html" ]]; then
log_error "Build failed - no output generated"
exit 1
fi
log_success "Site built successfully"
# Show build stats
local build_size file_count
build_size=$(du -sh "$BUILD_DIR" | cut -f1)
file_count=$(find "$BUILD_DIR" -type f | wc -l)
log_info "Build size: $build_size ($file_count files)"
}
# Deploy to gh-pages branch
deploy_to_ghpages() {
log_step "Deploying to $DEPLOY_BRANCH branch..."
# Fetch latest remote changes
git fetch "$REMOTE_NAME" > /dev/null 2>&1 || true
# Check if deploy branch exists on remote
if ! git ls-remote --heads "$REMOTE_NAME" "$DEPLOY_BRANCH" | grep -q "$DEPLOY_BRANCH"; then
log_error "Branch '$DEPLOY_BRANCH' does not exist on remote"
log_info "Please run jekyll-ghpages-setup first to create the branch"
exit 1
fi
# Create worktree for deployment
local deploy_dir="deploy-$(date +%s)"
log_info "Creating deployment worktree: $deploy_dir"
if ! git worktree add "$deploy_dir" "$DEPLOY_BRANCH"; then
log_error "Failed to create worktree for $DEPLOY_BRANCH"
exit 1
fi
# Function to handle deployment in subshell
deploy_content() {
cd "$deploy_dir"
# Clear existing docs content
if [[ -d "$DOCS_FOLDER" ]]; then
log_info "Clearing existing content in $DOCS_FOLDER..."
rm -rf "${DOCS_FOLDER:?}"/*
else
log_info "Creating $DOCS_FOLDER directory..."
mkdir -p "$DOCS_FOLDER"
fi
# Copy built site to docs folder
log_info "Copying built site to $DOCS_FOLDER..."
cp -r "../$BUILD_DIR"/* "$DOCS_FOLDER/"
# Check if there are changes to commit
if git diff --quiet && git diff --cached --quiet; then
log_warning "No changes detected - site is already up to date"
return 0
fi
# Stage changes
git add "$DOCS_FOLDER/"
# Create commit message
local commit_hash current_branch timestamp commit_msg
commit_hash=$(git rev-parse --short HEAD)
current_branch=$CURRENT_BRANCH
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
commit_msg="$COMMIT_MESSAGE_PREFIX: $timestamp (from $current_branch@$commit_hash)"
# Commit changes
log_info "Committing changes..."
git commit -m "$commit_msg"
# Push to remote
log_info "Pushing to remote..."
if git push "$REMOTE_NAME" "$DEPLOY_BRANCH"; then
log_success "Successfully deployed to $DEPLOY_BRANCH"
return 0
else
log_error "Failed to push to remote"
return 1
fi
}
# Execute deployment in subshell and capture result
if deploy_content; then
# Cleanup on success
git worktree remove "$deploy_dir" --force
else
# Cleanup on failure
git worktree remove "$deploy_dir" --force
exit 1
fi
}
# Check rebuild status after triggering
check_rebuild_status() {
local owner="$1"
local repo="$2"
local method="$3"
local token="${4:-}"
local ghes_host="${5:-}"
log_info "Checking Pages rebuild status..."
# Wait a moment for the build to register
sleep 2
local build_status=""
local pages_url=""
# Try to get build status
if [[ "$method" == "gh" ]] && command -v gh &> /dev/null; then
# Determine GHES hostname if needed
local gh_api_cmd="gh api"
if [[ -n "$ghes_host" ]]; then
gh_api_cmd="gh api --hostname $ghes_host"
fi
# Get latest build status via GitHub CLI
local build_info
build_info=$($gh_api_cmd "repos/$owner/$repo/pages/builds?per_page=1" 2>/dev/null || echo "")
if [[ -n "$build_info" ]] && [[ "$build_info" != "[]" ]]; then
build_status=$(echo "$build_info" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "unknown")
log_info "Latest build status: $build_status"
fi
# Get Pages URL
local pages_info
pages_info=$($gh_api_cmd "repos/$owner/$repo/pages" 2>/dev/null || echo "")
if [[ -n "$pages_info" ]]; then
pages_url=$(echo "$pages_info" | grep -o '"html_url":"[^"]*"' | cut -d'"' -f4 || echo "")
# If no html_url, construct it based on repo URL for GHES
if [[ -z "$pages_url" ]]; then
local repo_url
repo_url=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || echo "")
if [[ $repo_url =~ ([^/:]+)[:/]([^/]+)/([^/]+)(\.git)?$ ]] && [[ ! $repo_url =~ github\.com ]]; then
local ghes_host="${BASH_REMATCH[1]}"
pages_url="https://$ghes_host/pages/$owner/$repo"
fi
fi
fi
elif [[ "$method" == "api" ]] && [[ -n "$token" ]]; then
# Get latest build status via API
local api_base="https://api.github.com"
# Determine API base from git remote URL
local repo_url
repo_url=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || echo "")
if [[ $repo_url =~ github\.com[:/] ]]; then
api_base="https://api.github.com"
elif [[ $repo_url =~ ([^/:]+)[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
# For GHES instances, extract the host
local ghes_host="${BASH_REMATCH[1]}"
api_base="https://$ghes_host/api/v3"
fi
local build_response
build_response=$(curl -s \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $token" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$api_base/repos/$owner/$repo/pages/builds?per_page=1" 2>/dev/null || echo "")
if [[ -n "$build_response" ]] && [[ "$build_response" != "[]" ]]; then
build_status=$(echo "$build_response" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "unknown")
log_info "Latest build status: $build_status"
fi
fi
# Display rebuild confirmation
echo
log_success "πŸ”„ Pages rebuild has been triggered!"
echo
if [[ -n "$build_status" ]]; then
case "$build_status" in
"building"|"queued")
log_info "βœ… Build is in progress (status: $build_status)"
log_info "The rebuild typically takes 1-3 minutes to complete"
;;
"built")
log_success "βœ… A recent build completed successfully"
log_info "Your new rebuild should start shortly"
;;
"errored")
log_warning "⚠️ The last build had errors"
log_info "Check Settings β†’ Pages for error details"
;;
*)
log_info "Build status: $build_status"
;;
esac
else
log_info "πŸ“Š Could not retrieve build status (this is normal)"
log_info "The rebuild has been triggered and should complete in 1-3 minutes"
fi
if [[ -n "$pages_url" ]]; then
echo
log_info "🌐 Your Pages site URL: $pages_url"
log_info "Changes should be visible there once the rebuild completes"
fi
echo
log_info "πŸ’‘ To verify rebuild completion:"
echo " 1. Check your Pages URL in a few minutes"
echo " 2. Visit Settings β†’ Pages to see build status"
echo " 3. Look for the deployment timestamp on your site"
echo
}
# Force rebuild GitHub Pages via API
force_pages_rebuild() {
log_step "Attempting to force Pages rebuild via API..."
# Ensure we're in the git repository root
local repo_root
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
cd "$repo_root"
# Get repository info from remote URL
local repo_url repo_owner repo_name api_base
repo_url=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || {
log_error "Could not get remote URL"
return 1
})
# Parse repository owner and name from different URL formats
if [[ $repo_url =~ github\.com[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
repo_owner="${BASH_REMATCH[1]}"
repo_name="${BASH_REMATCH[2]%.git}"
api_base="https://api.github.com"
elif [[ $repo_url =~ ([^/:]+)[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
# For GHES instances
local ghes_host="${BASH_REMATCH[1]}"
repo_owner="${BASH_REMATCH[2]}"
repo_name="${BASH_REMATCH[3]%.git}"
api_base="https://$ghes_host/api/v3"
else
log_warning "Could not parse repository URL for API rebuild"
return 1
fi
log_info "Repository: $repo_owner/$repo_name"
# Try with GitHub CLI first
if command -v gh &> /dev/null && gh auth status &> /dev/null; then
log_info "Using GitHub CLI to trigger rebuild..."
# Determine if we need to specify hostname for GHES
local gh_api_cmd="gh api"
if [[ "$api_base" != "https://api.github.com" ]] && [[ -n "${ghes_host:-}" ]]; then
gh_api_cmd="gh api --hostname $ghes_host"
fi
if $gh_api_cmd "repos/$repo_owner/$repo_name/pages/builds" --method POST; then
log_success "Pages rebuild triggered via GitHub CLI"
check_rebuild_status "$repo_owner" "$repo_name" "gh" "" "${ghes_host:-}"
return 0
else
log_warning "GitHub CLI rebuild failed, trying other methods..."
fi
fi
# Try with curl and token
local token=""
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
token="$GITHUB_TOKEN"
elif [[ -n "${GH_TOKEN:-}" ]]; then
token="$GH_TOKEN"
else
log_info "No GitHub token found - trying empty commit method"
force_rebuild_empty_commit
return $?
fi
log_info "Using API to trigger rebuild..."
local response http_code
response=$(curl -s -w "%{http_code}" \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $token" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$api_base/repos/$repo_owner/$repo_name/pages/builds" 2>/dev/null)
http_code="${response: -3}"
if [[ "$http_code" =~ ^(200|201)$ ]]; then
log_success "Pages rebuild triggered via API"
check_rebuild_status "$repo_owner" "$repo_name" "api" "$token" "${ghes_host:-}"
return 0
else
log_warning "API rebuild failed (HTTP $http_code) - trying empty commit"
force_rebuild_empty_commit
return $?
fi
}
# Force rebuild using empty commit
force_rebuild_empty_commit() {
log_info "Triggering rebuild with empty commit..."
# Ensure we're in the git repository root
local repo_root
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
cd "$repo_root"
# Create worktree for rebuild
local rebuild_dir="rebuild-$(date +%s)"
if git worktree add "$rebuild_dir" "$DEPLOY_BRANCH" &> /dev/null; then
(
cd "$rebuild_dir" || {
log_error "Failed to enter rebuild directory"
return 1
}
# Make empty commit
if git commit --allow-empty -m "Force Pages rebuild - $(date '+%Y-%m-%d %H:%M:%S')" &> /dev/null; then
if git push "$REMOTE_NAME" "$DEPLOY_BRANCH" &> /dev/null; then
log_success "Rebuild triggered with empty commit"
# Note: Can't check status with empty commit method
log_info "Empty commit pushed - Pages should rebuild within a few minutes"
log_info "Check your Pages URL or repository Settings β†’ Pages for status"
return 0
else
log_error "Failed to push empty commit"
return 1
fi
else
log_error "Failed to create empty commit"
return 1
fi
)
local result=$?
# Clean up from the repository root
cd "$repo_root"
git worktree remove "$rebuild_dir" --force &> /dev/null || {
log_warning "Failed to remove worktree cleanly, removing directory manually"
rm -rf "$rebuild_dir" 2>/dev/null || true
}
return $result
else
log_error "Failed to create worktree for rebuild"
return 1
fi
}
# Cleanup deployment artifacts
cleanup_deployment() {
log_step "Cleaning up..."
# Remove build directory
if [[ -d "$BUILD_DIR" ]]; then
rm -rf "$BUILD_DIR"
log_info "Removed build directory"
fi
# Remove any leftover deployment worktrees
local deploy_dirs
deploy_dirs=$(find . -maxdepth 1 -name "deploy-*" -type d 2>/dev/null || true)
if [[ -n "$deploy_dirs" ]]; then
echo "$deploy_dirs" | while read -r dir; do
if [[ -n "$dir" ]]; then
git worktree remove "$dir" --force 2>/dev/null || rm -rf "$dir"
log_info "Cleaned up deployment directory: $dir"
fi
done
fi
local rebuild_dirs
rebuild_dirs=$(find . -maxdepth 1 -name "rebuild-*" -type d 2>/dev/null || true)
if [[ -n "$rebuild_dirs" ]]; then
echo "$rebuild_dirs" | while read -r dir; do
if [[ -n "$dir" ]]; then
git worktree remove "$dir" --force 2>/dev/null || rm -rf "$dir"
log_info "Cleaned up rebuild directory: $dir"
fi
done
fi
}
# Return to original branch
return_to_original_branch() {
local current_branch
current_branch=$(git branch --show-current)
if [[ "$CURRENT_BRANCH" != "$current_branch" ]]; then
log_info "Returning to original branch: $CURRENT_BRANCH"
git checkout "$CURRENT_BRANCH"
fi
}
# Get git host info from remote URL
get_git_host_info() {
local repo_url
repo_url=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || echo "")
if [[ -z "$repo_url" ]]; then
echo "ERROR: No remote URL"
return 1
fi
# GitHub.com
if [[ $repo_url =~ github\.com[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
echo "TYPE=github"
echo "HOST=github.com"
echo "OWNER=${BASH_REMATCH[1]}"
echo "REPO=${BASH_REMATCH[2]%.git}"
echo "API_BASE=https://api.github.com"
echo "PAGES_URL_PREFIX=https://${BASH_REMATCH[1]}.github.io"
# GHES or other git hosts
elif [[ $repo_url =~ ([^/:]+)[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
local host="${BASH_REMATCH[1]}"
local owner="${BASH_REMATCH[2]}"
local repo="${BASH_REMATCH[3]%.git}"
echo "TYPE=ghes"
echo "HOST=$host"
echo "OWNER=$owner"
echo "REPO=$repo"
echo "API_BASE=https://$host/api/v3"
echo "PAGES_URL_PREFIX=https://$host/pages/$owner"
else
echo "ERROR: Could not parse repository URL: $repo_url"
return 1
fi
}
# Display deployment information
show_deployment_info() {
echo
log_success "πŸš€ Deployment completed successfully!"
echo
# Get repository URL for Pages URL construction
local repo_url
repo_url=$(git remote get-url "$REMOTE_NAME")
if [[ $repo_url =~ github\.com[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
local user_org="${BASH_REMATCH[1]}"
local repo_name="${BASH_REMATCH[2]%.git}"
echo -e "${BLUE}🌐 Your site should be available at:${NC}"
echo " https://$user_org.github.io/$repo_name"
elif [[ $repo_url =~ ([^/:]+)[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
# For GHES instances
local ghes_host="${BASH_REMATCH[1]}"
local user_org="${BASH_REMATCH[2]}"
local repo_name="${BASH_REMATCH[3]%.git}"
echo -e "${BLUE}🌐 Your site should be available at:${NC}"
echo " https://$ghes_host/pages/$user_org/$repo_name"
fi
echo
echo -e "${BLUE}πŸ“Š Deployment Details:${NC}"
echo " Branch: $DEPLOY_BRANCH"
echo " Folder: /$DOCS_FOLDER"
echo " Time: $(date '+%Y-%m-%d %H:%M:%S')"
echo
echo -e "${BLUE}πŸ’‘ Next time:${NC}"
echo " Run 'jekyll-ghpages-deploy' to deploy again"
echo " Use '--rebuild' flag to force Pages rebuild if you get 503 errors"
echo " Or set up Jenkins CI/CD for automatic deployments"
echo
}
# Handle script interruption
cleanup_on_exit() {
log_warning "Script interrupted"
cleanup_deployment
return_to_original_branch
exit 1
}
# Set trap for cleanup on exit
trap cleanup_on_exit INT TERM
# Main execution
main() {
echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}"
echo -e "${CYAN}β•‘ Jekyll Deployment Script β•‘${NC}"
echo -e "${CYAN}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}"
echo
# Parse command line arguments
parse_arguments "$@"
# Run checks unless skipped
if [[ "$SKIP_CHECKS" == false ]]; then
check_prerequisites
check_working_directory
fi
# Build and deploy
build_site
deploy_to_ghpages
# Force rebuild if requested (before cleanup, from main directory)
if [[ "$FORCE_REBUILD" == true ]]; then
# Ensure we're in the main repository directory
cd "$(git rev-parse --show-toplevel)"
force_pages_rebuild
fi
cleanup_deployment
return_to_original_branch
show_deployment_info
}
# Run main function
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment