The CI checkout
CI environments check out your code afresh on every job. The choices you make about how they check out determine cache hits, build speed, and security. The defaults are rarely optimal.
Shallow clone for speed
git clone --depth=1 https://example.com/repo.git
# GitHub Actions:
- uses: actions/checkout@v4
with:
fetch-depth: 1
A depth-1 clone fetches only the tip commit - sufficient for build-and-test on modest repos. For tools that need history (versioning, blame, changelogs), unshallow on demand:
git fetch --unshallow
Sparse checkout for monorepos
git sparse-checkout init --cone
git sparse-checkout set apps/web shared/ui
Combine with shallow clone: only the directories you need, only the latest commit.
Caching .git across jobs
The .git directory is the most cacheable artefact in a pipeline. Most CI systems support persistent workspaces or runner caches:
cache:
key: git-$CI_COMMIT_REF_SLUG
paths:
- .git/
Authenticated cloning
Use HTTPS with a token, not stored SSH keys, in ephemeral runners. GitHub provides GITHUB_TOKEN; GitLab provides CI_JOB_TOKEN. Never bake long-lived credentials into images.
git clone https://oauth2:${TOKEN}@gitlab.com/group/repo.git
Reproducible builds
Pin everything by SHA, not branch. Lockfiles (package-lock.json, poetry.lock, Cargo.lock) belong in Git. CI should fail if the lockfile drifts:
npm ci # fails if lockfile is out of date
cargo build --locked
Detecting changed files
Run only relevant jobs by diffing against the merge base:
BASE=$(git merge-base origin/main HEAD)
CHANGED=$(git diff --name-only $BASE)
if echo "$CHANGED" | grep -q '^apps/web/'; then
./scripts/test-web.sh
fi
Tag-driven workflows
Separate "build on every push" from "publish on tag". Branch pipelines run tests; tag pipelines build, sign, and publish. This keeps responsibility clean.
Secrets out of Git
Never commit secrets to be "decoded by CI". Use the host's secret store. Run gitleaks in CI to fail builds containing leaked secrets:
- uses: gitleaks/gitleaks-action@v2
Tagging the build
Bake the commit SHA and build number into your artefacts:
SHA=$(git rev-parse --short HEAD)
docker build -t myapp:$SHA -t myapp:latest .
This makes "what is running in production?" answerable with docker inspect.
The checklist
- Shallow clone unless you need history.
- Cache
.gitand language caches. - Use ephemeral, scoped tokens.
- Run secret scanning in CI, not just on commit.
- Tag artefacts with the SHA they were built from.