Skip to content

Commit fa11cb4

Browse files
committed
docs: add CI and container delegation guide with GHCR upgrade path
Document the current CI approach (apt-get + CI=true skip), explain why Docker delegation is skipped in CI (path mismatch prevention), and provide step-by-step instructions for upgrading to GHCR container images as an alternative for production projects.
1 parent 3eb176b commit fa11cb4

2 files changed

Lines changed: 148 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1717

1818
Pre-commit hooks run on **pre-push** (not pre-commit): `pre-commit install --hook-type pre-push`
1919

20-
All scripts auto-delegate to Docker when run outside the container (`docker run --rm`). The delegation logic lives in `scripts/docker/exec.sh`, sourced by each script via `scripts/env.sh`. Inside the container, scripts run directly with no overhead.
20+
All scripts auto-delegate to Docker when run outside the container (`docker run --rm`). The delegation logic lives in `scripts/docker/exec.sh`, sourced by each script via `scripts/env.sh`. Inside the container or CI (`CI=true`), scripts run directly with no overhead. See `docs/ci-container-delegation.md` for details on the CI strategy and a GHCR upgrade path.
2121

2222
## Architecture
2323

docs/ci-container-delegation.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# CI and Container Delegation
2+
3+
## How Script Delegation Works
4+
5+
All project scripts (`build.sh`, `test.sh`, `format.sh`, `lint.sh`, etc.) auto-delegate to Docker when run outside the container. The delegation logic in `scripts/docker/exec.sh` checks three conditions in order:
6+
7+
1. **Inside a container** (`/.dockerenv` exists) — run directly, no delegation
8+
2. **CI environment** (`CI=true`) — run directly, tools provided by the runner
9+
3. **Developer host** — delegate to Docker via `docker run --rm`
10+
11+
```
12+
Developer host Container / CI runner
13+
-------------- ---------------------
14+
./scripts/build.sh
15+
source env.sh
16+
source docker/exec.sh
17+
delegate_to_container
18+
/.dockerenv? No
19+
CI=true? No
20+
docker run --rm ... build.sh --> ./scripts/build.sh
21+
exit $? delegate_to_container
22+
/.dockerenv? Yes
23+
return 0
24+
cmake / make / ...
25+
<-- exits
26+
```
27+
28+
## Current CI Approach
29+
30+
The CI workflow installs tools directly on the GitHub Actions runner and skips Docker delegation:
31+
32+
```yaml
33+
# .github/workflows/ci.yml
34+
steps:
35+
- uses: actions/checkout@v4
36+
- name: Install deps
37+
run: >
38+
sudo apt-get update &&
39+
sudo apt-get install -y cmake clang-format clang-tidy
40+
- name: Format check
41+
run: ./scripts/format.sh --check
42+
- name: Build
43+
run: ./scripts/build.sh
44+
- name: Lint check
45+
run: ./scripts/lint.sh
46+
- name: Test
47+
run: ./scripts/test.sh
48+
```
49+
50+
GitHub Actions sets `CI=true` automatically, so `delegate_to_container` returns immediately and scripts run directly on the runner. This is fast, requires no Docker setup, and works out-of-the-box for anyone who forks the template.
51+
52+
### Why not Docker in CI?
53+
54+
An earlier approach ran some CI steps directly on the host and others via Docker delegation. This caused path mismatches: `compile_commands.json` generated on the host contained runner paths (`/home/runner/work/...`), but `clang-tidy` ran inside a Docker container with different paths (`/workspaces/...`), causing crashes. The current approach avoids this by running everything in the same context.
55+
56+
## Alternative: GHCR Container Image
57+
58+
For production projects that require identical toolchains in CI and local development, you can publish the dev container image to GitHub Container Registry (GHCR) and use it as the CI job container.
59+
60+
GHCR is free for public repositories (unlimited storage and bandwidth).
61+
62+
### Setup
63+
64+
**1. Add a workflow to build and push the image** (`.github/workflows/docker-image.yml`):
65+
66+
```yaml
67+
name: Docker Image
68+
on:
69+
push:
70+
branches: [main]
71+
paths:
72+
- 'Dockerfile'
73+
- 'scripts/docker/entrypoint.sh'
74+
- '.github/workflows/docker-image.yml'
75+
workflow_dispatch:
76+
77+
env:
78+
REGISTRY: ghcr.io
79+
IMAGE_NAME: ${{ github.repository }}/cpp-dev
80+
81+
jobs:
82+
build-and-push:
83+
runs-on: ubuntu-24.04
84+
permissions:
85+
contents: read
86+
packages: write
87+
steps:
88+
- uses: actions/checkout@v4
89+
- name: Log in to GHCR
90+
uses: docker/login-action@v3
91+
with:
92+
registry: ghcr.io
93+
username: ${{ github.actor }}
94+
password: ${{ secrets.GITHUB_TOKEN }}
95+
- name: Build and push
96+
uses: docker/build-push-action@v6
97+
with:
98+
context: .
99+
file: docker/Dockerfile
100+
push: true
101+
tags: |
102+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
103+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
104+
```
105+
106+
**2. Update the CI workflow** to use the published image:
107+
108+
```yaml
109+
name: CI
110+
on: [push, pull_request]
111+
jobs:
112+
build:
113+
runs-on: ubuntu-24.04
114+
container:
115+
image: ghcr.io/${{ github.repository }}/cpp-dev:latest
116+
steps:
117+
- uses: actions/checkout@v4
118+
- name: Format check
119+
run: ./scripts/format.sh --check
120+
- name: Build
121+
run: ./scripts/build.sh
122+
- name: Lint check
123+
run: ./scripts/lint.sh
124+
- name: Test
125+
run: ./scripts/test.sh
126+
```
127+
128+
### Why this works without code changes
129+
130+
When GitHub Actions runs a job with `container:`, it creates `/.dockerenv` inside the container. The first check in `delegate_to_container` detects this and skips delegation. The `CI=true` check is never reached, so both guards coexist without conflict.
131+
132+
### Trade-offs
133+
134+
| | apt-get (current) | GHCR container |
135+
|---|---|---|
136+
| CI speed | Fast | Fast (pre-built image) |
137+
| Tool consistency | Runner versions (minor drift possible) | Identical to local dev |
138+
| Fork setup | Zero — works immediately | Must trigger image build first |
139+
| Maintenance | 1 workflow | 2 workflows |
140+
141+
### First-time setup for GHCR
142+
143+
1. Push the `docker-image.yml` workflow to `main`
144+
2. Go to Actions tab and manually trigger "Docker Image" (`workflow_dispatch`)
145+
3. Go to Packages tab and ensure the image visibility matches the repo (public/private)
146+
4. Update `ci.yml` to use `container:` as shown above
147+
5. Remove the `apt-get install` step (tools come from the image)

0 commit comments

Comments
 (0)