-
-
Notifications
You must be signed in to change notification settings - Fork 4
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.
| 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 |
| 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 |
Block PRs that introduce overly complex code. High cyclomatic complexity correlates with bugs and maintenance burden.
| 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 |
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 1Add 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)"'
doneBlock high-risk changes that could have significant downstream impact.
| 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 |
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
fiFor 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
fiWarn when files that frequently change together are modified independently.
| Parameter | Recommended | Description |
|---|---|---|
min-correlation |
0.7 | Minimum correlation to consider coupled |
min-cochanges |
5 | Minimum times files changed together |
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 1Enforce documentation coverage thresholds.
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- 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 1Flag changes to API boundaries (protobuf, OpenAPI, GraphQL).
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- 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
}
fiFail if dead code confidence is too high (indicates forgotten cleanup).
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 1Ensure search quality doesn't regress.
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
fiFor 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: 0For 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: 60For 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: 95Instead 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
]
}
});-
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
# 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# 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# 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- 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