Skip to content

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

  1. Week 1-2: Informational

    • Enable all gates in warn mode
    • Post comments but don't block
    • Gather baseline data
  2. Week 3-4: Soft Enforcement

    • Enable fail mode for critical risk only
    • Complexity warnings become more visible
    • Team discusses thresholds
  3. Month 2: Standard Enforcement

    • Enable complexity gate
    • Risk gate at high level
    • Coupling warnings active
  4. 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