Fixing Jekyll Builds That Break After a Ruby Upgrade
Jekyll itself changes slowly. Ruby does not: a new minor version every December, each with library moves and deprecations. A Jekyll site that has not been touched in two years usually still has the same Gemfile — and a CI image, a Homebrew install or a colleague's laptop now running a Ruby three versions newer. The resulting failures look unrelated to each other: cannot load such file -- webrick, a C compiler error from sassc, a wall of warnings about csv and base64, and a Liquid template that renders differently.
This guide fixes those failures on a company engineering blog — Jekyll 4.2, eleven plugins, built on Ruby 2.7 since 2021 — when its CI image moved to Ruby 3.4. It covers each error, its cause and fix, then pins Ruby so the next upgrade is deliberate. It belongs to Jekyll Plugin Ecosystem; the general upgrade process is in Upgrading Jekyll and Ruby Versions Safely.
What Broke
CI used ruby/setup-ruby with ruby-version: '3', which resolved to the latest 3.x. When 3.4 became the latest, the next build failed. The errors, in the order they appeared once each was fixed:
Error 1: sassc Fails to Compile
Installing sassc 2.4.0 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
...libsass/src/... error: no member named 'unary_function' in namespace 'std'
Cause. sassc wraps the deprecated LibSass C++ library, which does not compile with recent compilers and is no longer maintained. Jekyll 4.2's jekyll-sass-converter 2.x depends on it.
Fix. Upgrade to Jekyll 4.3 or later, whose jekyll-sass-converter 3.x uses sass-embedded (Dart Sass, distributed as a binary — nothing to compile). Dart Sass is stricter than LibSass: the blog's SCSS produced 14 deprecation warnings for / as division, fixed with math.div or calc(). One @import of a theme partial also needed its path corrected, because Dart Sass resolves load paths more strictly.
Error 2: webrick Cannot Be Loaded
jekyll 4.2.2 | Error: cannot load such file -- webrick
Cause. Ruby 3.0 stopped shipping webrick as a default gem; jekyll serve needs it.
Fix. Jekyll 4.3 declares it, so the upgrade in Error 1 fixed this too. On older Jekyll, add gem "webrick" to the Gemfile.
Error 3: Standard Library Gems Moved Out
warning: csv was loaded from the standard library, but is not part of the default gems starting from Ruby 3.4.0.
You can add csv to your Gemfile or gemspec to silence this warning.
...
LoadError: cannot load such file -- base64
Cause. Ruby 3.4 turned several default gems into bundled gems. Under bundle exec, only gems in the lockfile can be loaded, so code that requires csv, base64, bigdecimal or logger without declaring them fails. Two of the blog's plugins required csv (for a data import) and base64 (for an image inliner) without listing them.
Fix. Add them to the Gemfile:
gem "csv"
gem "base64"
gem "bigdecimal"
gem "logger"
Then check whether the plugins have newer versions that declare their own dependencies, which is the better long-term fix. One did; the other was a local plugin in _plugins/, which simply needed the require to resolve — the Gemfile entry was enough.
Error 4: A Plugin Calls a Removed Method
NoMethodError: undefined method `exists?' for class File
Cause. File.exists? was deprecated for years and removed in Ruby 3.2. A local plugin used it.
Fix. File.exist?. A grep across _plugins/ and the installed gems found two more removed methods (Dir.exists? and URI.escape), both in the same local plugin; URI.escape became URI::DEFAULT_PARSER.escape for the one place it was used.
Error 5: Output That Changed Without an Error
With the build passing, the output was compared against a build from the old setup (kept in a Ruby 2.7 Docker image for exactly this purpose):
docker run --rm -v "$PWD":/srv -w /srv ruby:2.7 bash -c 'bundle _2.4.22_ install && bundle exec jekyll build -d /srv/_site-old'
bundle exec jekyll build -d _site-new
diff -rq _site-old _site-new | wc -l
Most differences were in CSS (Dart Sass output formatting). One mattered: post dates in the archive page rendered in UTC instead of the site's timezone on 41 posts published late in the evening, shifting them to the next day. The cause was the timezone setting interacting with Ruby's updated Time parsing for front matter dates without an offset. Setting timezone: Europe/Berlin explicitly in _config.yml — it had been relying on the CI machine's TZ variable, which the new image did not set — fixed all 41.
Pin Ruby Everywhere
The incident happened because nothing pinned Ruby. Now:
# .ruby-version
3.4.1
# CI
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
setup-ruby reads .ruby-version; rbenv, chruby, asdf and mise do too on developers' machines. Gemfile.lock lists the platforms the site builds on (bundle lock --add-platform x86_64-linux arm64-darwin) so the same resolution is used locally and in CI. For contributors who do not want a Ruby toolchain at all, Running Jekyll in Docker for Reproducible Builds gives a one-command alternative with the same pinned version.
Upgrade on a Schedule
Pinning postpones upgrades; it should not prevent them. Dependabot opens pull requests for gem updates weekly, and a reminder each January bumps .ruby-version to the new Ruby release on a branch, where CI runs with RUBYOPT=-W:deprecated so deprecation warnings appear in the log a year before they become errors. The output diff against main is reviewed before merging.
Measured Impact
| Measure | Before | After | Tool |
|---|---|---|---|
| Ruby in CI | '3' (floating) | 3.4.1 pinned | Workflow |
| Build time | 38 s (Ruby 2.7, LibSass) | 31 s (Ruby 3.4, Dart Sass) | hyperfine |
bundle install, cold | 2 min 10 s (compiling sassc) | 24 s | CI logs |
| Gems with native extensions | 6 | 2 | bundle list |
| Posts with wrong dates | 41 | 0 | Output diff |
Pitfalls & Rollback
- Floating Ruby versions in CI.
ruby-version: '3'changes under you; pin from.ruby-version. - Fixing only the first error. Each fix reveals the next; budget for the chain.
- Relying on the machine's timezone. Set
timezonein_config.yml. - Skipping the output diff. Date and Sass changes do not fail the build.
- Rollback: pin
.ruby-versionback to the old Ruby and restore the oldGemfile.lock; the Docker image of the old setup builds the old output exactly.
Conclusion
Jekyll breaks on new Ruby versions through its dependencies, not its own code: a native Sass compiler that no longer builds, webrick and other standard libraries moved out of Ruby, removed methods in local plugins, and subtle changes such as timezone handling. Upgrading to Jekyll 4.3 with Dart Sass, declaring moved gems, fixing removed calls, diffing the output and pinning Ruby turned a day-long incident into a yearly 40-minute task.
FAQ
Why does Jekyll fail with cannot load such file webrick?
Ruby 3.0 removed webrick from its default gems, and jekyll serve depends on it. Jekyll 4.3 and later list it as a dependency, but older Jekyll versions do not. Adding gem webrick to the Gemfile, or upgrading Jekyll, fixes it. The build command jekyll build does not need webrick, so the error appears only for local serving.
What do warnings about gems no longer being part of the default gems mean?
Ruby 3.4 moved several libraries, including csv, base64, bigdecimal and logger, from default gems to bundled gems. Code that requires them without declaring them in the Gemfile warns on 3.3 and fails on 3.4 under Bundler. Plugins and older Jekyll dependencies are the usual source; adding the gems to the Gemfile or upgrading the plugins fixes it.
Why do native extension gems fail to install after upgrading Ruby?
Gems with C extensions, such as sassc, eventmachine or older versions of ffi, are compiled against a specific Ruby version. Old releases may not compile on new Ruby or new compilers. Upgrading the gem to a version that supports the new Ruby, or replacing it, as with sassc replaced by sass-embedded in Jekyll 4.3, resolves it.
How do I keep Jekyll builds reproducible across machines?
Pin the Ruby version in a .ruby-version file, commit Gemfile.lock with the platforms you build on, use the same version in CI through setup-ruby reading .ruby-version, and run builds through bundle exec. A Docker image with a pinned Ruby gives the strongest guarantee.
Related
- Parent: Jekyll Plugin Ecosystem — Jekyll's plugins and their maintenance.
- Upgrading Jekyll and Ruby Versions Safely — a planned upgrade process.
- Running Jekyll in Docker for Reproducible Builds — the pinned environment.
- Fixing Hugo Builds That Fail After a Version Upgrade — the same failure mode in Hugo.
- Speeding Up Slow Jekyll Builds — what else to fix while you are in the Gemfile.