|
| 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