From 48305f6c6a33dac79291006510564818165145d6 Mon Sep 17 00:00:00 2001 From: ebembi-crdb Date: Wed, 10 Dec 2025 19:36:19 +0530 Subject: [PATCH] Add hermetic Docker build environment for documentation This adds a Docker-based build system for consistent Jekyll documentation builds across all developers and environments. Changes: - Add Dockerfile with pinned versions (Ruby 3.4, Bundler 4.0, Jekyll 4.3.4) - Add .dockerignore to exclude build artifacts - Add ci/README.md with usage documentation and troubleshooting - Add ci/cloudbuild.yaml for GCP Cloud Build multi-arch builds - Update Makefile with docker-build, docker-serve, docker-shell targets - Update Gemfile with Docker rebuild instructions - Fix _plugins/versions/release_info.rb for Docker path compatibility Image published to: us-docker.pkg.dev/release-notes-automation-stag/docs-builder/docs-builder:latest Usage: make docker-build # Build image locally make docker-serve # Serve docs at http://localhost:4000/docs/ make docker-pull # Pull pre-built image from GCP --- src/current/.dockerignore | 35 ++++ src/current/Dockerfile | 66 ++++++ src/current/Gemfile | 4 + src/current/Makefile | 67 ++++++ src/current/_plugins/versions/release_info.rb | 7 + src/current/ci/README.md | 195 ++++++++++++++++++ src/current/ci/cloudbuild.yaml | 75 +++++++ 7 files changed, 449 insertions(+) create mode 100644 src/current/.dockerignore create mode 100644 src/current/Dockerfile create mode 100644 src/current/ci/README.md create mode 100644 src/current/ci/cloudbuild.yaml diff --git a/src/current/.dockerignore b/src/current/.dockerignore new file mode 100644 index 00000000000..c279631d7fa --- /dev/null +++ b/src/current/.dockerignore @@ -0,0 +1,35 @@ +# Git +.git +.gitignore + +# Jekyll build outputs +_site +.jekyll-cache +.jekyll-metadata +.sass-cache + +# Ruby vendor dependencies (will be installed in image) +vendor + +# Node modules +node_modules + +# Logs +*.log +build_*.log + +# OS files +.DS_Store +Thumbs.db + +# IDE +.idea +.vscode +*.swp +*.swo + +# Algolia state (generated during indexing) +algolia_state + +# Temporary files +tmp diff --git a/src/current/Dockerfile b/src/current/Dockerfile new file mode 100644 index 00000000000..e89d40139fc --- /dev/null +++ b/src/current/Dockerfile @@ -0,0 +1,66 @@ +# Hermetic Jekyll Documentation Build Image +# +# This Dockerfile creates a consistent build environment for the CockroachDB +# documentation site. It pins all Ruby, Python, and Node.js dependencies to +# ensure reproducible builds across all developers and CI environments. +# +# For build and publish instructions, see ci/README.md + +FROM ruby:3.4-slim + +# Version labels +LABEL org.opencontainers.image.title="CockroachDB Docs Builder" +LABEL org.opencontainers.image.description="Hermetic build environment for CockroachDB documentation" +LABEL org.opencontainers.image.source="https://github.com/cockroachdb/docs" +LABEL ruby.version="3.4.0" +LABEL bundler.version="4.0.0" +LABEL jekyll.version="4.3.4" + +# Install build and runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + python3 \ + python3-pip \ + curl \ + libxml2-dev \ + libxslt1-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js 20 LTS for Jest tests +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /docs + +# Install specific bundler version (must match Gemfile.lock BUNDLED WITH) +RUN gem install bundler:4.0.0 --no-document + +# Copy Gemfile and local gem for dependency installation +# The jekyll-algolia-dev directory contains a local gem that must be present +COPY Gemfile Gemfile.lock ./ +COPY jekyll-algolia-dev ./jekyll-algolia-dev/ + +# Configure bundler and install gems to /usr/local/bundle (default location) +RUN bundle config set --local jobs 4 \ + && bundle config set --local without development:test \ + && bundle install + +# Install Python dependencies for Algolia indexing +RUN pip3 install --break-system-packages --no-cache-dir \ + pyyaml \ + "algoliasearch>=3.0,<4.0" \ + beautifulsoup4 \ + lxml \ + tqdm + +# Set environment variables +ENV JEKYLL_ENV=development +ENV BUNDLE_FROZEN=true + +# Expose Jekyll server port +EXPOSE 4000 + +# Default command - serve the documentation locally +CMD ["bundle", "exec", "jekyll", "serve", "--host", "0.0.0.0", "--port", "4000"] diff --git a/src/current/Gemfile b/src/current/Gemfile index 208f25f007c..68e9b8dbb99 100644 --- a/src/current/Gemfile +++ b/src/current/Gemfile @@ -5,6 +5,10 @@ source "https://rubygems.org" # If you add to this file, it is recommended to run `make vendor` # (`gem install bundler && bundle install`) before running your next local build. # It may fail without at least running `bundle install`. +# +# If you modify this file, you'll need to rebuild the docs-builder Docker +# image to ensure CI and local builds use the updated dependencies. +# See ci/README.md for build and publish instructions. gem "jekyll", "4.3.4" gem "liquid-c", "~> 4.0.0" gem "redcarpet", "~> 3.6" diff --git a/src/current/Makefile b/src/current/Makefile index 063711deaa8..f9ee8cddc49 100644 --- a/src/current/Makefile +++ b/src/current/Makefile @@ -104,3 +104,70 @@ clean-site: clean-cache: rm -rf .jekyll-cache + +# ============================================================================= +# Docker-based builds +# ============================================================================= +# These targets use the hermetic docs-builder Docker image for consistent builds +# across all environments. See ci/README.md for more details. + +DOCKER_REGISTRY := us-docker.pkg.dev/release-notes-automation-stag/docs-builder +DOCKER_IMAGE := docs-builder +DOCKER_TAG := latest +# Run as current user to avoid permission issues with mounted volumes +DOCKER_USER := $(shell id -u):$(shell id -g) + +# Build the Docker image locally +.PHONY: docker-build +docker-build: + docker build -t $(DOCKER_IMAGE):local . + +# Serve documentation using Docker (with live reload) +.PHONY: docker-serve +docker-serve: + docker run -it --rm \ + --dns 8.8.8.8 \ + --user $(DOCKER_USER) \ + -p 4000:4000 \ + -v "$(CURDIR)":/docs \ + -e JEKYLL_ENV=development \ + -e HOME=/tmp \ + $(DOCKER_IMAGE):local \ + bundle exec jekyll serve --host 0.0.0.0 --port 4000 --incremental --trace \ + --config _config_base.yml,_config_cockroachdb.yml,_config_cockroachdb_local.yml + +# Build documentation using Docker (no serve) +.PHONY: docker-build-site +docker-build-site: + docker run -it --rm \ + --dns 8.8.8.8 \ + --user $(DOCKER_USER) \ + -v "$(CURDIR)":/docs \ + -e JEKYLL_ENV=production \ + -e HOME=/tmp \ + $(DOCKER_IMAGE):local \ + bundle exec jekyll build --trace \ + --config _config_base.yml,_config_cockroachdb.yml + +# Interactive shell in the Docker container +.PHONY: docker-shell +docker-shell: + docker run -it --rm \ + --dns 8.8.8.8 \ + --user $(DOCKER_USER) \ + -v "$(CURDIR)":/docs \ + -e HOME=/tmp \ + $(DOCKER_IMAGE):local \ + /bin/bash + +# Pull the latest pre-built image from GCP Artifact Registry +.PHONY: docker-pull +docker-pull: + docker pull $(DOCKER_REGISTRY)/$(DOCKER_IMAGE):$(DOCKER_TAG) + docker tag $(DOCKER_REGISTRY)/$(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_IMAGE):local + +# Push image to GCP Artifact Registry (requires gcloud auth) +.PHONY: docker-push +docker-push: + docker tag $(DOCKER_IMAGE):local $(DOCKER_REGISTRY)/$(DOCKER_IMAGE):$(DOCKER_TAG) + docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE):$(DOCKER_TAG) diff --git a/src/current/_plugins/versions/release_info.rb b/src/current/_plugins/versions/release_info.rb index c7a8c9e9d90..ce69cb26c71 100644 --- a/src/current/_plugins/versions/release_info.rb +++ b/src/current/_plugins/versions/release_info.rb @@ -11,9 +11,16 @@ def generate(site) parent_dir = File.expand_path('..', site.source) # Step 2: Construct the paths to versions.csv and releases.yml + # Try parent/current/_data first (standard layout), fall back to site.source/_data (Docker) versions_path = File.join(parent_dir, "current/_data/versions.csv") releases_path = File.join(parent_dir, "current/_data/releases.yml") + # Fall back to site.source if parent path doesn't exist (e.g., in Docker container) + unless File.exist?(versions_path) + versions_path = File.join(site.source, "_data/versions.csv") + releases_path = File.join(site.source, "_data/releases.yml") + end + # Load versions and releases data versions_data = CSV.read(versions_path, headers: true) releases_data = YAML.load_file(releases_path) diff --git a/src/current/ci/README.md b/src/current/ci/README.md new file mode 100644 index 00000000000..c90251e1577 --- /dev/null +++ b/src/current/ci/README.md @@ -0,0 +1,195 @@ +# Documentation Build Docker Image + +This directory contains configuration for building and publishing the hermetic Docker image used for CockroachDB documentation builds. + +## Quick Start (Local Development) + +```bash +# 1. Build the Docker image (first time only) +make docker-build + +# 2. Serve docs locally with live reload +make docker-serve + +# 3. Open http://localhost:4000/docs/ in your browser +``` + +That's it! No Ruby, Bundler, or gem installation required on your machine. + +**Other useful commands:** +- `make docker-build-site` - Build without serving +- `make docker-shell` - Interactive shell for debugging +- `make docker-pull` - Pull pre-built image from GCP (instead of building locally) + +## Overview + +The `docs-builder` Docker image provides a consistent build environment with pinned versions of: + +| Component | Version | +|-----------|---------| +| Base Image | ruby:3.4-slim (Debian Trixie) | +| Ruby | 3.4.0 | +| Bundler | 4.0.0 | +| Jekyll | 4.3.4 | +| Python | 3.13+ | +| Node.js | 20 LTS | + +## Prerequisites + +- Docker installed locally +- For publishing: `gcloud` CLI configured with appropriate permissions + +## Building the Image Locally + +```bash +# From the repository root +docker build -t docs-builder:local . + +# Or use the Makefile target +make docker-build +``` + +## Running Locally with Docker + +### Serve documentation with live reload + +```bash +make docker-serve +# Then open http://localhost:4000 +``` + +### Interactive shell in the container + +```bash +make docker-shell +``` + +### Build without serving + +```bash +docker run -it --rm \ + -v "$(pwd)":/docs \ + docs-builder:local \ + bundle exec jekyll build --trace \ + --config _config_base.yml,_config_cockroachdb.yml +``` + +## Publishing to GCP Artifact Registry + +### Manual Publishing + +1. Authenticate with GCP: + ```bash + gcloud auth login + gcloud auth configure-docker us-docker.pkg.dev + ``` + +2. Build and push the image: + ```bash + # Set the image tag (use date-based versioning) + export IMAGE_TAG=$(date +%Y-%m-%d) + export REGISTRY=us-docker.pkg.dev/release-notes-automation-stag/docs-builder + + # Build for multi-architecture + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag ${REGISTRY}/docs-builder:${IMAGE_TAG} \ + --tag ${REGISTRY}/docs-builder:latest \ + --push \ + . + ``` + +### Automated Publishing with Cloud Build + +Trigger a Cloud Build to build and publish the image: + +```bash +gcloud builds submit --config ci/cloudbuild.yaml . +``` + +## Image Tagging Convention + +| Tag | Description | +|-----|-------------| +| `latest` | Most recent build | +| `YYYY-MM-DD` | Date-based version (e.g., `2025-01-15`) | +| `ruby3.4-bundler4.0` | Version-based for explicit compatibility | + +## When to Rebuild the Image + +Rebuild and publish a new image when: + +1. `Gemfile` or `Gemfile.lock` changes +2. Python dependencies change in build scripts +3. Ruby or Node.js version needs updating +4. Security updates are required for base image + +## Environment Variables + +The following environment variables can be passed to the container: + +| Variable | Description | Default | +|----------|-------------|---------| +| `JEKYLL_ENV` | Build environment (`development`, `production`, `preview`) | `development` | +| `ALGOLIA_API_KEY` | Algolia write API key for indexing | - | +| `PROD_ALGOLIA_API_KEY` | Production Algolia API key | - | + +## Volume Mounts + +| Mount Point | Purpose | +|-------------|---------| +| `/docs` | Mount the documentation source directory here | + +## Exposed Ports + +| Port | Service | +|------|---------| +| 4000 | Jekyll development server | + +## Troubleshooting + +### Permission errors with mounted volumes + +If you encounter permission errors (e.g., with `.jekyll-cache`, `_site`, `.jekyll-metadata`), run the container as your current user: + +```bash +docker run -it --rm \ + -v "$(pwd)":/docs \ + -u "$(id -u):$(id -g)" \ + -e HOME=/tmp \ + docs-builder:local \ + bundle exec jekyll build +``` + +If files were created by root in a previous run, remove them first: + +```bash +sudo rm -rf .jekyll-cache _site .jekyll-metadata +``` + +### DNS resolution errors + +If you see errors like "Failed to open TCP connection" or "getaddrinfo: Temporary failure in name resolution", add explicit DNS: + +```bash +docker run -it --rm \ + --dns 8.8.8.8 \ + -v "$(pwd)":/docs \ + docs-builder:local \ + bundle exec jekyll build +``` + +### Native gem compilation issues + +The image uses `ruby:3.4-slim` which includes build tools for native extensions. If you encounter issues: + +1. Ensure you're using the multi-arch image matching your platform +2. Try pulling a fresh image: `docker pull ${REGISTRY}/docs-builder:latest` + +### Bundle path issues + +The image pre-installs gems to `/usr/local/bundle`. When mounting your local directory, ensure you don't have a conflicting local `vendor/bundle` directory. + +To work around this, you can either: +- Clear your local vendor directory: `rm -rf vendor` +- Or use a different bundle path in the container diff --git a/src/current/ci/cloudbuild.yaml b/src/current/ci/cloudbuild.yaml new file mode 100644 index 00000000000..8745fdbf1d3 --- /dev/null +++ b/src/current/ci/cloudbuild.yaml @@ -0,0 +1,75 @@ +# Cloud Build configuration for multi-arch docs-builder image +# +# This builds the documentation Docker image for both AMD64 and ARM64 +# architectures and pushes to GCP Artifact Registry. +# +# Usage: +# gcloud builds submit --config ci/cloudbuild.yaml . +# +# Or trigger manually: +# gcloud builds submit --config ci/cloudbuild.yaml --substitutions=_IMAGE_TAG=2025-01-15 . + +substitutions: + _REGISTRY: us-docker.pkg.dev/release-notes-automation-stag/docs-builder + _IMAGE_NAME: docs-builder + # Default tag is date-based + _IMAGE_TAG: ${_DATE} + +options: + # Use a larger machine for faster builds + machineType: 'E2_HIGHCPU_8' + # Enable Kaniko caching for faster builds + logging: CLOUD_LOGGING_ONLY + +steps: + # Step 1: Set up Docker buildx for multi-architecture builds + - name: 'gcr.io/cloud-builders/docker' + id: 'setup-buildx' + entrypoint: 'bash' + args: + - '-c' + - | + docker buildx create --name multiarch --driver docker-container --use + docker buildx inspect --bootstrap + + # Step 2: Build and push multi-arch image with date tag + - name: 'gcr.io/cloud-builders/docker' + id: 'build-push-dated' + entrypoint: 'bash' + args: + - '-c' + - | + DATE_TAG=$(date +%Y-%m-%d) + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag ${_REGISTRY}/${_IMAGE_NAME}:$${DATE_TAG} \ + --tag ${_REGISTRY}/${_IMAGE_NAME}:latest \ + --tag ${_REGISTRY}/${_IMAGE_NAME}:ruby3.4-bundler4.0 \ + --push \ + --cache-from type=registry,ref=${_REGISTRY}/${_IMAGE_NAME}:cache \ + --cache-to type=registry,ref=${_REGISTRY}/${_IMAGE_NAME}:cache,mode=max \ + . + waitFor: ['setup-buildx'] + + # Step 3: Verify the image was pushed successfully + - name: 'gcr.io/cloud-builders/docker' + id: 'verify' + entrypoint: 'bash' + args: + - '-c' + - | + echo "Verifying image availability..." + docker manifest inspect ${_REGISTRY}/${_IMAGE_NAME}:latest + echo "Image published successfully!" + echo "" + echo "Available tags:" + echo " - ${_REGISTRY}/${_IMAGE_NAME}:latest" + echo " - ${_REGISTRY}/${_IMAGE_NAME}:$(date +%Y-%m-%d)" + echo " - ${_REGISTRY}/${_IMAGE_NAME}:ruby3.4-bundler4.0" + waitFor: ['build-push-dated'] + +# Images to be available after build +images: + - '${_REGISTRY}/${_IMAGE_NAME}:latest' + +timeout: '1800s' # 30 minutes max