- CI
- DevEx
- TDD
Zero Drift CI: Same Rules Everywhere, from Hook to Pipeline
Local hooks pass but CI fails? Never again. Multi-CI Zero Drift ensures identical validation at every stage.
Alexandre Mallet2 min read
You know the drill. You write code locally. Your editor says it's clean. You commit, push, and open a PR. Twenty minutes later, CI fails on a rule you've never seen enforced locally.
This gap between local validation and CI validation is drift, and it's one of the most common sources of developer frustration.
The Drift Problem
Drift happens when your local tooling and your CI pipeline diverge:
- CI runs PHPStan level 9, but your local hook only runs level 6
- CI checks architecture boundaries, but locally nobody runs that check
- CI enforces coding standards your editor plugin ignores
- A new rule was added to CI last week, but nobody updated the local config
The result: developers stop trusting local feedback. They push and wait. The feedback loop stretches from seconds to minutes. Velocity drops. Frustration rises.
One Config, Every Stage
AI Craftsman Superpowers solves this with a single source of truth: .craft-config.yml. The same rules file drives both your local hooks (Claude Code hooks that validate on every write) and your CI pipeline.
# .craft-config.yml: single source of truth
v: 4
stack: symfony
strictness: strict
rules:
PHP003: block # setters refused, not only reported
TS002: ignore # default exports allowed in this projectLocally, this config powers the quality gate on every file write. In CI, the exact same config and the same rules engine feed the pipeline, so a rule cannot mean one thing on your machine and another in the pipeline.
GitHub Actions Integration
Setting up CI is a single workflow file:
# .github/workflows/craftsman.yml, from ci/templates/craftsman-quality-gate.yml
name: Craftsman Quality Gate
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Run craftsman-ci quality gate
env:
GH_TOKEN: ${{ github.token }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
CRAFTSMAN_SCOPE: ${{ github.event_name == 'pull_request' && '--changed-only' || '' }}
run: bash ci/craftsman-ci.sh ci --provider github $CRAFTSMAN_SCOPEThe --changed-only flag keeps CI fast: only files touched in the PR are validated. The --provider github option selects the GitHub adapter, which writes the findings as check annotations on the PR diff and posts a PR comment.
Beyond GitHub
The same pattern works across providers. The plugin ships with starter configs for GitLab CI, Bitbucket Pipelines, and Jenkins: each using the same .craft-config.yml as the source of truth. Switch providers without rewriting rules.
# GitLab CI, from ci/templates/.gitlab-ci.craftsman.yml
craftsman-quality-gate:
stage: test
image: python:3.12-slim
variables:
GIT_DEPTH: 0
rules:
- if: $CI_MERGE_REQUEST_IID
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- if [ -n "$CI_MERGE_REQUEST_IID" ]; then git fetch origin "+refs/heads/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME:refs/remotes/origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; SCOPE="--changed-only"; else SCOPE=""; fi
- bash ci/craftsman-ci.sh ci --provider gitlab $SCOPE
artifacts:
reports:
codequality: gl-code-quality-report.json
when: alwaysThe Feedback Loop
Zero drift means zero surprises:
- You write code → the pre-write hook judges the file before it reaches disk, using
.craft-config.yml - You push → CI validates using the same
.craft-config.yml - PR opens → violations appear as inline annotations, identical to what you saw locally
If it passes locally, it passes in CI. If it fails in CI, it would have failed locally. The feedback loop is tight, consistent, and trustworthy.
Developers who trust their local tooling commit with confidence. Developers who don't trust it push and pray. Zero drift turns prayers into guarantees.