Phil
penny
96x96
naturalmohican By Phillip David Penny
« Home  /  Blog

Dec 20, 2025

Setting up CI/CD for Jekyll

It is time I gave my personal space the same professional discipline I bring to my day job.

806 words (Approximately a 4 minute read)

From Notepad to CI/CD

This website has been a constant companion for over twenty years. It started as a hobby project, hand-coded in Notepad during the early days of the web and uploaded manually with FileZilla. Over the decades, it evolved into my professional portfolio, but like many personal projects, it often became an afterthought—lacking the proper care, attention, and rigorous engineering standards I dedicate to client work.

Recently, I decided it was time to change that. I wanted to treat this personal space with the same professional discipline I bring to my day job, bringing it in line with modern development workflows. That meant moving away from manual FTP uploads and ad-hoc fixes to a robust, automated pipeline.

The Challenge

My Jekyll site uses:

However, automating deployments came with a significant risk. My server hosts over twenty years of accumulated artifacts—files, projects, and data that exist alongside the main site but aren’t part of the Git repository. A misconfigured pipeline could easily wipe out decades of history in seconds, so ensuring the safety of these legacy files was paramount.

I needed a deployment pipeline that would:

  1. Build the site correctly with both Ruby (Jekyll) and Node.js (Tailwind/PostCSS) dependencies
  2. Deploy via SFTP to my hosting provider
  3. Provide visibility into what would be deleted before actual deployment
  4. Protect important files and directories from accidental deletion

The Solution: GitHub Actions Workflow

Build Process

The workflow handles two separate build processes that need to run in sequence:

# 1. Setup Ruby for Jekyll
- name: 💎 Setup Ruby
  uses: ruby/setup-ruby@v1
  with:
    ruby-version: '3.2' # Compatible with Jekyll 4.1.1

# 2. Setup Node.js for Tailwind/PostCSS
- name: 🟢 Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

Key learning: Ruby 3.3 had compatibility issues with Jekyll 4.1.1’s logger initialization, so I downgraded to Ruby 3.2 which works reliably.

Tailwind CSS v4 Considerations

Migrating to Tailwind v4 introduced several challenges:

  1. PostCSS Configuration: The @import "tailwindcss" directive requires @tailwindcss/postcss plugin, not the old tailwindcss PostCSS plugin
  2. SCSS Parsing: PostCSS needs postcss-scss parser to handle SCSS syntax before Tailwind processes it
  3. Custom Utilities: Font sizes defined in @theme weren’t generating utilities correctly, so I moved them to the JS config file

The final PostCSS config:

module.exports = {
  plugins: [
    require('@tailwindcss/postcss'),
    // cssnano disabled due to CSS optimization warnings
    // process.env.NODE_ENV === 'production' && require('cssnano')({ preset: 'default' }),
  ].filter(Boolean),
};

And the build command uses the SCSS parser:

"css:build": "NODE_ENV=production postcss _sass/main.scss --parser postcss-scss --output assets/css/style.css"

My hosting uses SFTP on a non-standard port, which ruled out most GitHub Actions that only support FTP/FTPS. I used lftp, a command-line FTP/SFTP client that supports password authentication and recursive directory syncing.

- name: 📂 Install SFTP tools
  run: sudo apt-get update && sudo apt-get install -y lftp

- name: 🚀 Deploy to server via SFTP
  run: |
    echo "🚀 Deploying site to production..."
    lftp -c "
    set sftp:auto-confirm yes
    set sftp:connect-program 'ssh -a -x -oStrictHostKeyChecking=no'
    set cmd:verbose no
    open -u $,$ -p $ sftp://$
    lcd _site
    cd public_html
    mirror --reverse --delete --parallel=4 --only-newer \
      --exclude-glob .git* \
      --exclude-glob .github* \
      # ... many more excludes
    quit
    "

Safety Features

Safe Deployment Strategy: While lftp supports dry runs, I’ve opted for a direct deployment strategy that relies on a comprehensive exclude list to prevent accidental deletions.

Exclude list: A comprehensive list of files and directories that should never be deleted, including:

Trade-offs: The Cost of Convenience

While this setup is powerful, it’s important to acknowledge that using GitHub Actions does come with some “lock-in”:

  1. Platform Lock-in: The .yml configuration is specific to GitHub. You cannot simply copy-paste this pipeline to GitLab, Bitbucket, or Jenkins.
  2. Proprietary Actions: Relying on actions like ruby/setup-ruby ties you further to the ecosystem.
  3. Debugging: Debugging often involves a slow “commit-push-fail” loop, whereas local scripts can be tested instantly.

For a personal portfolio, the convenience of having code and CI/CD in one place usually outweighs these cons. However, if portability is a major concern, there are strategies to mitigate this, which I will cover in a follow-up post.

Benefits

  1. Automated deployments: Every push to master triggers a build and deployment.
  2. Data Preservation: Strict exclude patterns protect twenty years of legacy artifacts.
  3. Simplicity: Direct mirroring reduces pipeline complexity while maintaining safety.
  4. Reliability: Consistent build environment in GitHub Actions.
  5. Version control: The entire deployment process is versioned and reviewable.

Conclusion

This setup provides a robust, safe, and transparent deployment pipeline that gives me confidence when pushing changes to production, finally bringing my personal portfolio up to the professional standards I value in my work.

^ Top