Skip to content

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', no gem 'jekyll', '~> 4.3', no FROM ruby:3.3, no BUNDLER_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:

  1. Check the CI logs - The debug-failure job shows exactly what versions were attempted
  2. Identify the culprit - Usually one gem released a breaking change
  3. Options:
  4. Wait for upstream fix (often resolved within hours)
  5. Open issue upstream
  6. Last resort: Temporarily pin the problematic gem
  7. 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:

  1. Builds Docker image with --no-cache (latest everything)
  2. Documents resolved versions in workflow summary
  3. Tests Jekyll build, RSpec, HTML validation
  4. Tags successful images with immutable tag (date+commit)
  5. Publishes to Docker Hub (only on main branch)
  6. Debug info on failure (shows what versions were attempted)

Migration from Pinned Versions

If you're coming from a project with version pins:

  1. Remove all version constraints from Gemfile
  2. Remove all version constraints from gemspec
  3. Update Dockerfile to use ruby:slim (no version)
  4. Delete Gemfile.lock (let it regenerate)
  5. Run docker compose up --build
  6. Fix any compatibility issues that surface
  7. Commit the new Gemfile.lock