My baseline git template. It ships a shared git configuration and a set of pre-commit hooks so every repo started from this template gets consistent commit hygiene and commit-message formatting out of the box.
-
pre-commitinstalled and on yourPATH:pipx install pre-commit # or: brew install pre-commit -
A Python 3.10 interpreter installed on your system (required by the commitizen and sync-pre-commit-deps hooks). Install instructions per platform:
Platform Install command macOS (Homebrew) brew install python@3.10Linux (Debian/Ubuntu) sudo apt install python3.10Linux (any, via pyenv) pyenv install 3.10Windows Download from python.org (the installer registers it with the pylauncher aspy -3.10).Why exactly 3.10? Some hooks require Python
>=3.10, but pre-commit otherwise uses each hook's own default interpreter (python3in their manifests) — which on many systems (notably macOS) is an older 3.9 that fails to build the hook environments withrequires a different Python. The template therefore setslanguage_version: python3.10explicitly on the affected hooks in.pre-commit-config.yaml. (A top-leveldefault_language_versionwould not work here: pre-commit only applies it to hooks that don't already declare their ownlanguage_version, and these hooks do.)python3.10is a version request that pre-commit resolves to a real interpreter on Windows, Linux, and macOS (on Windows through thepylauncher). A Python 3.10 interpreter therefore needs to be installed, though it does not need to be your defaultpython3. To standardize on a newer version, change thelanguage_versionvalues in.pre-commit-config.yaml(see Troubleshooting).
From the repository root:
make setupThis runs:
git config --local include.path ../.gitconfigwhich makes the repo's local config include the committed .gitconfig.
That config sets core.hooksPath = .githooks/, activating the committed hook
scripts. Because the hooks live in .githooks/ and are wired up through
include.path, pre-commit install is not required.
Run make help (or just make) to list the available targets.
| File | Purpose |
|---|---|
.gitconfig |
Sets core.hooksPath = .githooks/ so the committed hooks are used. |
.githooks/pre-commit |
Runs the pre-commit-stage hooks (formatting, secret detection, etc.). |
.githooks/commit-msg |
Runs commitizen to enforce Conventional Commits message format. |
.pre-commit-config.yaml |
Declares the hook repos and versions, and pins the Python interpreter (language_version: python3.10) on the Python hooks that require it. |
- pre-commit-hooks: trailing whitespace, end-of-file fixer, YAML checks, large-file guard (blocks files over 500 kB by default), case-conflict detection (catches filename collisions on case-insensitive filesystems like macOS/Windows), illegal Windows names, merge-conflict markers, private-key detection, byte-order-marker fix, and mixed line endings.
- gitleaks: scans for hardcoded secrets.
- commitizen (
commit-msgstage): validates commit messages follow the Conventional Commits format, e.g.:feat: add user login fix(api): handle null response chore: bump dependencies - sync-pre-commit-deps: keeps hook dependency versions in sync.
This template is designed so that wiring it into any CI/CD platform is simple and deterministic. It follows the industry-standard thin wrapper pattern (see Martin Fowler on Continuous Integration): all the actual check logic lives in the repository behind a single command, and each CI platform's config does nothing more than check out the code, install prerequisites, and run that one command.
make ci ← single source of truth (runs locally too)
└── pre-commit run --all-files
└── hooks in .pre-commit-config.yaml
.github/workflows/ci.yml ← thin stub: checkout → setup → `make ci`
azure-pipelines.yml ← thin stub: checkout → setup → `make ci`
The same make ci a developer runs on their laptop is exactly what runs on
GitHub Actions and Azure DevOps. To change what CI does, edit the
Makefile and .pre-commit-config.yaml
— not the platform YAML.
| File | Purpose |
|---|---|
Makefile (make ci) |
The portable entrypoint. All check logic lives here. Extend it with your project's build/test commands. |
.github/workflows/ci.yml |
Thin GitHub Actions stub that runs make ci. |
azure-pipelines.yml |
Thin Azure DevOps stub that runs make ci. |
Add your build/test steps to the ci target in the Makefile. For
example, for a .NET project:
ci: check-pre-commit lint test ## Run the full CI check suite
test: ## Run the test suite
dotnet testBecause the logic is in the Makefile, those steps run identically locally and on every CI platform — no YAML changes required.
The thin-wrapper pattern minimizes platform-specific config but cannot eliminate it. Each platform requires its own small YAML stub, and a few concerns are inherently platform-specific and cannot be pushed into a portable script:
-
The stub file itself — GitHub needs
.github/workflows/*.yml; Azure DevOps needsazure-pipelines.yml. The template ships both, pre-wired tomake ci. -
Triggers (which branches/events run CI) — expressed differently on each platform. Both stubs ship pre-configured to run CI on:
- pushes to
mainanddevelop, and - pull requests targeting
develop.
Adjust the
on/trigger/prsections in.github/workflows/ci.ymlandazure-pipelines.ymlto change this. - pushes to
-
Secrets, service connections, OIDC, and permissions — managed in each platform's settings/YAML, never in the repo.
-
Runner/agent image and prerequisite installation (Python,
pre-commit).
Everything else — the actual checks — is shared via make ci.
- Hook versions are pinned via
revin.pre-commit-config.yaml. - The Python interpreter is pinned to 3.10 in both CI stubs (matching the
per-hook
language_version; see Requirements). - Both stubs cache pre-commit hook environments keyed on the config file, so unchanged hooks are not rebuilt.
Bump these versions deliberately when you want to upgrade.
| Target | Description |
|---|---|
make help |
Show available targets (default when running make). |
make setup |
Configure the repo to use the shared git config and pre-commit hooks. Runs the preflight checks first. |
make ci |
Run the full CI check suite — the single command CI/CD pipelines invoke. Runs identically locally. |
make lint |
Run all pre-commit hooks against all files. |
make check-pre-commit |
Verify the pre-commit tool is installed. |
make check-python |
Verify a Python 3.10 interpreter is available for the hooks. |
`pre-commit` not found— install the tool (see Requirements).- commitizen / sync-pre-commit-deps fails to build /
requires a different Python— a Python 3.10 interpreter could not be found. Install it (see Requirements) and re-runmake check-pythonto confirm it is detected. The interpreter is pinned per-hook vialanguage_version: python3.10in.pre-commit-config.yaml; to standardize on a different version, change thoselanguage_versionvalues (e.g. topython3.11) and make sure that interpreter is installed. - Hook is ignored / not running — confirm
make setuphas been run (git config --get include.pathshould print../.gitconfig) and that the hook scripts in.githooks/are executable.