Zero Version Pin Strategy¶
Always use the latest compatible versions of everything, with zero version pins anywhere β while maintaining 100% reproducible builds and failing fast in TEST/DEV.
This is the pattern used internally by Shopify, GitHub, GitLab, and most large Jekyll/Docker teams that want bleeding-edge dependencies with zero version drift.
Core Philosophy¶
- Never pin any version in code (no
ruby '3.3', nogem 'jekyll', '~> 4.3', noFROM ruby:3.3, noBUNDLER_VERSION=β¦) - Let Bundler + Docker Hub resolve the latest compatible versions at build time
- Fail immediately and loudly in CI (TEST) if the latest versions are incompatible
- Production always gets exactly what passed TEST (via immutable image tags, never
:latest)
How It Works¶
Dependency Resolution Flow¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ZERO PIN DEPENDENCY FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. DEVELOP/TEST (Build with --no-cache) β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Dockerfile βββββΆβ Bundler βββββΆβ Gemfile.lockβ β
β β (no pins) β β resolves β β (auto-gen) β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Latest compatible versions for ALL gems β β
β β β Tests run against these exact versions β β
β β β If incompatible: BUILD FAILS β β β
β β β If compatible: Tests proceed β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β 2. CI/TEST PASS β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Create immutable image tag: β β
β β bamr87/zer0-mistakes:20251128-1420-a1b2c3d β β
β β (date + time + commit hash) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β 3. PRODUCTION (Uses immutable tag) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β image: bamr87/zer0-mistakes:20251128-1420-... β β
β β β Exact same image that passed tests β β
β β β 100% reproducible β β
β β β NEVER uses :latest in production β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
File Structure¶
zer0-mistakes/
βββ docker/
β βββ Dockerfile # Multi-stage, zero version pins
β βββ config/
β βββ production.yml # Jekyll production config
β βββ development.yml # Jekyll development config
βββ Gemfile # Zero version constraints
βββ Gemfile.lock # Auto-generated, tracks exact versions
βββ jekyll-theme-zer0.gemspec # Minimal version requirements
βββ docker-compose.yml # Development environment
βββ docker-compose.test.yml # CI testing overlay
βββ docker-compose.prod.yml # Production (immutable tags only)
βββ .github/workflows/
βββ test-latest.yml # Fail-fast CI with latest deps
Key Files Explained¶
Dockerfile (docker/Dockerfile)¶
# Zero version pins - always latest
FROM ruby:slim AS base
# Install dependencies without version pins
RUN apt-get update -qq && \
apt-get install -y build-essential libyaml-dev ...
# Install latest Bundler
RUN gem install bundler
# Let Bundler resolve latest compatible
COPY Gemfile Gemfile.lock* ./
RUN bundle install
Gemfile¶
source "https://rubygems.org"
gemspec
# NO version constraints β always latest compatible
gem "github-pages", group: :jekyll_plugins
gem "webrick"
gem "ffi"
gem "commonmarker"
docker-compose.prod.yml¶
services:
jekyll:
# NEVER use :latest in production
# Always use immutable tag from CI
image: bamr87/zer0-mistakes:${IMAGE_TAG:-20251128-1420-a1b2c3d}
Usage¶
Development¶
# Start development server (builds with latest deps)
docker compose up
# Rebuild with fresh dependencies
docker compose up --build
# Access container shell
docker compose exec jekyll bash
Testing (CI)¶
# Build with no cache to ensure latest dependencies
docker compose -f docker-compose.yml -f docker-compose.test.yml build --no-cache
# Run tests
docker compose -f docker-compose.yml -f docker-compose.test.yml run jekyll
# Validate only
docker compose -f docker-compose.yml -f docker-compose.test.yml run validate
Production Deployment¶
# Get the latest successful tag from CI artifact
IMAGE_TAG=$(cat LATEST_SUCCESSFUL_TAG)
# Deploy with immutable tag
IMAGE_TAG=$IMAGE_TAG docker compose -f docker-compose.prod.yml up -d
# Or set in environment file
echo "IMAGE_TAG=$IMAGE_TAG" > .env.prod
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
Benefits Summary¶
| Goal | How It's Achieved |
|---|---|
| Always use latest dependencies | Zero version pins anywhere |
| Auto-resolve compatible set | Bundler + lockfile does it on every build |
| Incompatibilities caught early | Build fails loudly in TEST/CI β PR blocked |
| Production never breaks | Only images that passed TEST are promoted |
| Zero version maintenance | No one ever has to bump versions manually |
| Full reproducibility | Immutable tags (date+commit) lock the exact set |
Handling Breaking Changes¶
When the CI build fails due to an upstream breaking change:
- Check the CI logs - The
debug-failurejob shows exactly what versions were attempted - Identify the culprit - Usually one gem released a breaking change
- Options:
- Wait for upstream fix (often resolved within hours)
- Open issue upstream
- Last resort: Temporarily pin the problematic gem
- Temporary pins should be documented and removed ASAP
Example: Temporary Pin (Last Resort)¶
# Gemfile
# TEMPORARY: commonmarker 0.24.0 breaks our build
# Issue: https://github.com/github/commonmarker/issues/XXX
# TODO: Remove this pin when issue is resolved
gem "commonmarker", "< 0.24.0"
CI Workflow¶
The .github/workflows/test-latest.yml workflow:
- Builds Docker image with
--no-cache(latest everything) - Documents resolved versions in workflow summary
- Tests Jekyll build, RSpec, HTML validation
- Tags successful images with immutable tag (date+commit)
- Publishes to Docker Hub (only on main branch)
- Debug info on failure (shows what versions were attempted)
Migration from Pinned Versions¶
If you're coming from a project with version pins:
- Remove all version constraints from
Gemfile - Remove all version constraints from
gemspec - Update
Dockerfileto useruby:slim(no version) - Delete
Gemfile.lock(let it regenerate) - Run
docker compose up --build - Fix any compatibility issues that surface
- Commit the new
Gemfile.lock