Build Stage
What the Build Stage Does
The build stage transforms source code into a deployable artifact. It must be fast, deterministic, and reproducible.
Same code + same dependencies = same output. Always.
Build Process Steps
1. Dependency Installation
Install all project dependencies. Cache aggressively — this is the slowest step.
# GitHub Actions — Python with uv
- uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ hashFiles('uv.lock') }}
- run: uv sync --frozen
--frozen ensures the exact lockfile is used. No silent upgrades in CI.
2. Compilation / Transpilation
For compiled languages (Go, Rust, C++), the build stage produces a binary. Python skips compilation but still validates the package:
# Python — validate package builds correctly
- run: uv build # build wheel + sdist
- run: uv run python -c "import myapp" # verify importability
3. Linting & Type Checking
Fast static analysis catches issues before running tests.
- run: uv run ruff check . # lint
- run: uv run ruff format --check # format check
- run: uv run mypy . # type check
Fail here before wasting test compute on fundamentally broken code.
Caching Strategy
Caching dependencies is the single most effective build optimisation.
| What to Cache | Cache Key |
|---|---|
| Python packages (uv) | hash(uv.lock) |
| Python packages (pip) | hash(requirements.txt) |
| Docker layer cache | Layer hash |
| Go modules | hash(go.sum) |
- uses: actions/cache@v4
with:
path: |
~/.cache/uv
~/.cache/pip
key: deps-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
deps-${{ runner.os }}-
restore-keys provides a fallback: if the exact key misses, use the most recent partial match.
Incremental Builds
Only rebuild what changed. For monorepos:
# Use paths filter — only run service A pipeline when service A changes
on:
push:
paths:
- 'services/service-a/**'
- 'shared/utils/**'
For Docker: - Layer caching ensures only changed layers rebuild - Multi-stage builds minimise the final image size
Docker Build in CI
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
type=gha uses GitHub Actions cache for Docker layers. Typically cuts build time by 60–80%.
Build Reproducibility
A build is reproducible when:
- Dependency versions are pinned (uv.lock, Cargo.lock, go.sum)
- Base Docker image is pinned by digest, not tag
- No network calls during build (dependencies pre-installed)
- Timestamps and random values are not embedded in artifacts
# Pinned by digest — immune to tag mutation
FROM python:3.12-slim@sha256:abc123...
WORKDIR /app
COPY uv.lock pyproject.toml ./
RUN uv sync --frozen --no-dev
COPY src/ ./src/