Quality Gates
Quality gates are automated checks that enforce code standards in your CI pipeline. CKB provides multiple gate types that can warn, block, or annotate PRs based on configurable thresholds.
Ready-to-use workflows: See Workflow Examples for complete GitHub Actions templates.
Integration guide: See CI-CD-Integration for installation and CLI usage.
Overview
What Quality Gates Do
| Mode | Behavior | Use Case |
|---|---|---|
| Warn | Post comment, annotate files, continue | Early adoption, informational |
| Fail | Block merge if threshold exceeded | Enforce standards |
| Annotate | Inline warnings in PR diff | Precise feedback |
Available Gates
| Gate | What It Checks | CLI Command |
|---|---|---|
| Complexity | Cyclomatic/cognitive complexity | ckb complexity |
| Risk | Overall change risk level | ckb pr-summary, ckb impact diff |
| Coupling | Missing co-changed files | ckb coupling |
| Coverage | Documentation coverage | ckb docs coverage |
| Contract | API boundary changes | File pattern matching |
| Dead Code | Unused code detection | ckb dead-code |
| Eval | Search quality regression | ckb eval |
Complexity Gate
Block PRs that introduce overly complex code. High cyclomatic complexity correlates with bugs and maintenance burden.
Thresholds
| Metric | Recommended | Strict | Description |
|---|---|---|---|
| Cyclomatic | 15 | 10 | Number of independent paths through code |
| Cognitive | 20 | 15 | Mental effort to understand code |
| File Total | 100 | 50 | Sum of all function complexities |
Implementation
env:
MAX_CYCLOMATIC: 15
MAX_COGNITIVE: 20
steps:
- name: Complexity Check
id: complexity
run: |
VIOLATIONS=0
for file in $(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(go|ts|js|py)$'); do
[ -f "$file" ] || continue
RESULT=$(ckb complexity "$file" --format=json 2>/dev/null || echo '{}')
CYCLO=$(echo "$RESULT" | jq '.summary.maxCyclomatic // 0')
COGNITIVE=$(echo "$RESULT" | jq '.summary.maxCognitive // 0')
if [ "$CYCLO" -gt "$MAX_CYCLOMATIC" ]; then
echo "::warning file=$file::Cyclomatic complexity $CYCLO exceeds $MAX_CYCLOMATIC"
VIOLATIONS=$((VIOLATIONS + 1))
fi
if [ "$COGNITIVE" -gt "$MAX_COGNITIVE" ]; then
echo "::warning file=$file::Cognitive complexity $COGNITIVE exceeds $MAX_COGNITIVE"
VIOLATIONS=$((VIOLATIONS + 1))
fi
done
echo "violations=$VIOLATIONS" >> $GITHUB_OUTPUT
# Warn mode (comment only)
- name: Complexity Warning
if: steps.complexity.outputs.violations > 0
run: echo "::warning::${{ steps.complexity.outputs.violations }} complexity violations found"
# Fail mode (block merge)
- name: Complexity Gate
if: steps.complexity.outputs.violations > 0
run: |
echo "::error::Complexity gate failed with ${{ steps.complexity.outputs.violations }} violations"
exit 1
Inline Annotations
Add warnings directly in the PR diff:
- name: Annotate Complex Functions
run: |
for file in $(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(go|ts|js|py)$'); do
[ -f "$file" ] || continue
ckb complexity "$file" --format=json 2>/dev/null | \
jq -r --arg max "$MAX_CYCLOMATIC" \
'.functions[] | select(.cyclomatic > ($max | tonumber)) |
"::warning file=\(.file),line=\(.line)::Function \(.name) has cyclomatic complexity \(.cyclomatic)"'
done
Risk Gate
Block high-risk changes that could have significant downstream impact.
Risk Levels
| Level | Score | Typical Triggers |
|---|---|---|
low |
0-0.3 | Small changes, single module |
medium |
0.3-0.6 | Multi-module, some hotspots |
high |
0.6-0.8 | Many modules, API changes |
critical |
0.8-1.0 | Core changes, breaking APIs |
Implementation
env:
FAIL_ON_RISK: critical # Options: low, medium, high, critical
steps:
- name: Analyze Risk
id: risk
run: |
# Using pr-summary
ckb pr-summary --base=origin/${{ github.base_ref }} --format=json > analysis.json
echo "level=$(jq -r '.riskAssessment.level // "unknown"' analysis.json)" >> $GITHUB_OUTPUT
echo "score=$(jq -r '.riskAssessment.score // 0' analysis.json)" >> $GITHUB_OUTPUT
# Or using impact diff
ckb impact diff --base=origin/${{ github.base_ref }} --format=json > impact.json
echo "impact_risk=$(jq -r '.summary.estimatedRisk // "unknown"' impact.json)" >> $GITHUB_OUTPUT
- name: Risk Gate
run: |
RISK="${{ steps.risk.outputs.level }}"
THRESHOLD="${{ env.FAIL_ON_RISK }}"
# Map to numbers: low=1, medium=2, high=3, critical=4
risk_num() {
case "$1" in
low) echo 1;;
medium) echo 2;;
high) echo 3;;
critical) echo 4;;
*) echo 0;;
esac
}
if [ "$(risk_num "$RISK")" -ge "$(risk_num "$THRESHOLD")" ]; then
echo "::error::Risk level '$RISK' meets or exceeds threshold '$THRESHOLD'"
exit 1
fi
Using Impact Analysis
For more granular risk assessment:
- name: Impact Risk Gate
run: |
RISK=$(jq -r '.summary.estimatedRisk // "unknown"' impact.json)
AFFECTED=$(jq '.summary.transitivelyAffected // 0' impact.json)
MODULES=$(jq '.blastRadius.moduleCount // 0' impact.json)
# Custom risk logic
if [ "$RISK" = "critical" ]; then
echo "::error::Critical risk: $AFFECTED symbols affected across $MODULES modules"
exit 1
elif [ "$RISK" = "high" ] && [ "$MODULES" -gt 5 ]; then
echo "::error::High risk spanning $MODULES modules requires additional review"
exit 1
fi
Coupling Gate
Warn when files that frequently change together are modified independently.
Thresholds
| Parameter | Recommended | Description |
|---|---|---|
min-correlation |
0.7 | Minimum correlation to consider coupled |
min-cochanges |
5 | Minimum times files changed together |
Implementation
env:
COUPLING_THRESHOLD: 0.7
steps:
- name: Coupling Check
id: coupling
run: |
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(go|ts|js|py)$' || true)
MISSING=0
for file in $(echo "$CHANGED" | head -10); do
[ -f "$file" ] || continue
RESULT=$(ckb coupling "$file" --min-correlation=$COUPLING_THRESHOLD --format=json 2>/dev/null || echo '{}')
# Check if coupled files are missing from PR
for coupled in $(echo "$RESULT" | jq -r '.correlations[]?.file // empty'); do
if ! echo "$CHANGED" | grep -q "^$coupled$"; then
echo "::warning::$file is often changed with $coupled (not in PR)"
MISSING=$((MISSING + 1))
fi
done
done
echo "missing=$MISSING" >> $GITHUB_OUTPUT
# Warn mode
- name: Coupling Warning
if: steps.coupling.outputs.missing > 0
run: |
echo "::warning::${{ steps.coupling.outputs.missing }} coupled file(s) may be missing from this PR"
# Fail mode (optional - usually just warn)
- name: Coupling Gate
if: steps.coupling.outputs.missing > 5
run: |
echo "::error::Too many coupled files missing. Review related changes."
exit 1
Coverage Gate
Enforce documentation coverage thresholds.
Implementation
env:
DOC_COVERAGE_MIN: 70
steps:
- name: Doc Coverage
id: docs
run: |
ckb docs index 2>/dev/null || true
ckb docs coverage --format=json > docs-coverage.json
COVERAGE=$(jq '.coveragePercent // 0' docs-coverage.json)
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
- name: Coverage Gate
run: |
COVERAGE="${{ steps.docs.outputs.coverage }}"
THRESHOLD="${{ env.DOC_COVERAGE_MIN }}"
if [ "$COVERAGE" -lt "$THRESHOLD" ]; then
echo "::error::Documentation coverage $COVERAGE% is below threshold $THRESHOLD%"
exit 1
fi
Stale Reference Detection
- name: Stale Docs Check
id: stale
run: |
ckb docs stale --all --format=json > docs-stale.json
STALE=$(jq '.totalStale // 0' docs-stale.json)
echo "stale=$STALE" >> $GITHUB_OUTPUT
- name: Stale Docs Gate
if: steps.stale.outputs.stale > 0
run: |
echo "::warning::${{ steps.stale.outputs.stale }} stale documentation references found"
# Optionally fail:
# exit 1
Contract Gate
Flag changes to API boundaries (protobuf, OpenAPI, GraphQL).
Implementation
steps:
- name: Contract Check
id: contracts
run: |
CONTRACTS=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | \
grep -E '\.(proto|graphql|gql|openapi\.ya?ml|swagger\.ya?ml)$' || true)
if [ -n "$CONTRACTS" ]; then
echo "found=true" >> $GITHUB_OUTPUT
echo "count=$(echo "$CONTRACTS" | wc -l)" >> $GITHUB_OUTPUT
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$CONTRACTS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "found=false" >> $GITHUB_OUTPUT
echo "count=0" >> $GITHUB_OUTPUT
fi
- name: Contract Warning
if: steps.contracts.outputs.found == 'true'
run: |
echo "::warning::API contract files changed: ${{ steps.contracts.outputs.count }} file(s)"
echo "Files: ${{ steps.contracts.outputs.files }}"
# Require additional approval for contract changes
- name: Contract Gate
if: steps.contracts.outputs.found == 'true'
run: |
echo "::notice::Contract changes require @api-team review"
# Could also auto-request reviewers:
# gh pr edit ${{ github.event.pull_request.number }} --add-reviewer api-team
Breaking Change Detection
- name: Breaking Change Check
if: steps.contracts.outputs.found == 'true'
run: |
# For protobuf, use buf
if echo "${{ steps.contracts.outputs.files }}" | grep -q '\.proto$'; then
buf breaking --against '.git#branch=origin/${{ github.base_ref }}' || {
echo "::error::Breaking protobuf changes detected"
exit 1
}
fi
Dead Code Gate
Fail if dead code confidence is too high (indicates forgotten cleanup).
Implementation
env:
DEAD_CODE_THRESHOLD: 0.95
steps:
- name: Dead Code Check
id: deadcode
run: |
ckb dead-code --min-confidence=$DEAD_CODE_THRESHOLD --limit=20 --format=json > deadcode.json
COUNT=$(jq '.candidates | length' deadcode.json)
echo "count=$COUNT" >> $GITHUB_OUTPUT
- name: Dead Code Warning
if: steps.deadcode.outputs.count > 0
run: |
echo "::warning::${{ steps.deadcode.outputs.count }} high-confidence dead code candidates found"
jq -r '.candidates[] | "- \(.name) in \(.path)"' deadcode.json
# Fail if new dead code introduced
- name: Dead Code Gate
if: steps.deadcode.outputs.count > 10
run: |
echo "::error::Too much dead code detected. Please clean up unused symbols."
exit 1
Eval Gate
Ensure search quality doesn't regress.
Implementation
env:
EVAL_PASS_RATE: 90
steps:
- name: Run Eval Suite
id: eval
run: |
if [ -d ".ckb/fixtures" ]; then
ckb eval --fixtures=.ckb/fixtures --format=json > eval.json
PASSED=$(jq '.passedTests // 0' eval.json)
TOTAL=$(jq '.totalTests // 0' eval.json)
if [ "$TOTAL" -gt 0 ]; then
RATE=$((PASSED * 100 / TOTAL))
else
RATE=100
fi
echo "passed=$PASSED" >> $GITHUB_OUTPUT
echo "total=$TOTAL" >> $GITHUB_OUTPUT
echo "rate=$RATE" >> $GITHUB_OUTPUT
else
echo "rate=100" >> $GITHUB_OUTPUT
echo "total=0" >> $GITHUB_OUTPUT
fi
- name: Eval Gate
if: steps.eval.outputs.total > 0
run: |
RATE="${{ steps.eval.outputs.rate }}"
THRESHOLD="${{ env.EVAL_PASS_RATE }}"
if [ "$RATE" -lt "$THRESHOLD" ]; then
echo "::error::Eval pass rate $RATE% is below threshold $THRESHOLD%"
jq -r '.results[] | select(.passed == false) | "- \(.id): \(.reason)"' eval.json
exit 1
fi
Combining Gates
Starter Profile (Lenient)
For projects just starting with quality gates:
env:
# Complexity - warn only
MAX_CYCLOMATIC: 20
MAX_COGNITIVE: 25
COMPLEXITY_GATE_ENABLED: 'false' # Warn only
# Risk - fail on critical only
FAIL_ON_RISK: critical
# Coupling - informational
COUPLING_THRESHOLD: 0.8
# Coverage - no enforcement
DOC_COVERAGE_MIN: 0
Standard Profile
For established projects:
env:
# Complexity - warn, no fail
MAX_CYCLOMATIC: 15
MAX_COGNITIVE: 20
COMPLEXITY_GATE_ENABLED: 'warn'
# Risk - fail on high and critical
FAIL_ON_RISK: high
# Coupling - warn when files missing
COUPLING_THRESHOLD: 0.7
# Coverage - enforce minimum
DOC_COVERAGE_MIN: 60
Strict Profile
For mature projects with high quality bar:
env:
# Complexity - enforce
MAX_CYCLOMATIC: 10
MAX_COGNITIVE: 15
COMPLEXITY_GATE_ENABLED: 'true'
# Risk - fail on medium and above
FAIL_ON_RISK: medium
# Coupling - stricter threshold
COUPLING_THRESHOLD: 0.6
# Coverage - high bar
DOC_COVERAGE_MIN: 80
# Additional gates
DEAD_CODE_THRESHOLD: 0.9
EVAL_PASS_RATE: 95
Check Run Annotations
Instead of (or in addition to) comments, use GitHub Check Runs for inline annotations:
- name: Create Check Run
uses: actions/github-script@v7
with:
script: |
const violations = parseInt('${{ steps.complexity.outputs.violations }}');
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Complexity Gate',
head_sha: context.sha,
status: 'completed',
conclusion: violations > 0 ? 'failure' : 'success',
output: {
title: violations > 0 ? `${violations} complexity violations` : 'All checks passed',
summary: `Found ${violations} file(s) exceeding complexity thresholds.`,
annotations: [
// Add file-level annotations here
]
}
});
Rollout Strategy
-
Week 1-2: Informational
- Enable all gates in warn mode
- Post comments but don't block
- Gather baseline data
-
Week 3-4: Soft Enforcement
- Enable fail mode for critical risk only
- Complexity warnings become more visible
- Team discusses thresholds
-
Month 2: Standard Enforcement
- Enable complexity gate
- Risk gate at high level
- Coupling warnings active
-
Month 3+: Full Enforcement
- All gates active
- Thresholds tightened based on team feedback
- Add eval suite if applicable
Troubleshooting
Gate Too Strict
# Temporarily bypass for urgent fixes
- name: Check Override Label
id: override
run: |
LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
if echo "$LABELS" | grep -q "bypass-gates"; then
echo "bypass=true" >> $GITHUB_OUTPUT
else
echo "bypass=false" >> $GITHUB_OUTPUT
fi
- name: Complexity Gate
if: steps.override.outputs.bypass != 'true' && steps.complexity.outputs.violations > 0
run: exit 1
False Positives
# Exclude generated files
- name: Get Changed Files
run: |
git diff --name-only origin/${{ github.base_ref }}...HEAD | \
grep -E '\.(go|ts|js|py)$' | \
grep -v '_generated\.' | \
grep -v '\.pb\.go$' | \
grep -v 'vendor/' > changed-files.txt
Slow Gates
# Limit files analyzed
- name: Complexity Check (Limited)
run: |
# Only check first 20 files to avoid timeout
for file in $(cat changed-files.txt | head -20); do
# ...
done
Related Pages
- Workflow Examples — Complete workflow templates using these gates
- CI-CD-Integration — Full CI/CD integration guide
- Impact-Analysis — Understanding risk scoring
- Configuration — Global configuration options