code-quality-gates

SKILL.md

Code Quality Gates — Expert Decisions

Expert decision frameworks for quality gate choices. Claude knows linting tools — this skill provides judgment calls for threshold calibration and rule selection.


Decision Trees

Which Quality Gates for Your Project

Project stage?
├─ Greenfield (new project)
│  └─ Enable ALL gates from day 1
│     • SwiftLint (strict)
│     • SwiftFormat
│     • SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
│     • Coverage > 80%
├─ Brownfield (legacy, no gates)
│  └─ Adopt incrementally:
│     1. SwiftLint with --baseline (ignore existing)
│     2. Format new files only
│     3. Gradually increase thresholds
└─ Existing project with some gates
   └─ Team size?
      ├─ Solo/Small (1-3) → Lint + Format sufficient
      └─ Medium+ (4+) → Full pipeline
         • Add coverage gates
         • Add static analysis
         • Add security scanning

Coverage Threshold Selection

What type of code?
├─ Domain/Business Logic
│  └─ 90%+ coverage required
│     • Business rules must be tested
│     • Silent bugs are expensive
├─ Data Layer (Repositories, Mappers)
│  └─ 85% coverage
│     • Test mapping edge cases
│     • Test error handling
├─ Presentation (ViewModels)
│  └─ 70-80% coverage
│     • Test state transitions
│     • Skip trivial bindings
└─ Views (SwiftUI)
   └─ Don't measure coverage
      • Snapshot tests or manual QA
      • Unit testing views has poor ROI

SwiftLint Rule Strategy

Starting fresh?
├─ YES → Enable opt_in_rules aggressively
│  └─ Easier to disable than enable later
└─ NO → Adopting on existing codebase
   └─ Use baseline approach:
      1. Run: swiftlint lint --reporter json > baseline.json
      2. Configure: baseline_path: baseline.json
      3. New violations fail, existing ignored
      4. Chip away at baseline over time

NEVER Do

Threshold Anti-Patterns

NEVER set coverage to 100%:

# ❌ Blocks legitimate PRs, encourages gaming
MIN_COVERAGE: 100

# ✅ Realistic threshold with room for edge cases
MIN_COVERAGE: 80

NEVER use zero-tolerance for warnings initially on legacy code:

# ❌ 500 warnings = blocked pipeline forever
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES  # On day 1 of legacy project

# ✅ Incremental adoption
# 1. First: just report warnings
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO

# 2. Then: require no NEW warnings
swiftlint lint --baseline existing-violations.json

# 3. Finally: zero tolerance (after cleanup)
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES

NEVER skip gates on "urgent" PRs:

# ❌ Creates precedent, gates become meaningless
if: github.event.pull_request.labels.contains('urgent')
  run: echo "Skipping quality gates"

# ✅ Gates are non-negotiable
# If code can't pass gates, it shouldn't ship

SwiftLint Anti-Patterns

NEVER disable rules project-wide without documenting why:

# ❌ Mystery disabled rules
disabled_rules:
  - force_unwrapping
  - force_cast
  - line_length

# ✅ Document the reasoning
disabled_rules:
  # line_length: Configured separately with custom thresholds
  # force_unwrapping: Using force_unwrapping opt-in rule instead (stricter)

NEVER use inline disables without explanation:

// ❌ No context
// swiftlint:disable force_unwrapping
let value = optionalValue!
// swiftlint:enable force_unwrapping

// ✅ Explain why exception is valid
// swiftlint:disable force_unwrapping
// Reason: fatalError path for corrupted bundle resources that should crash
let value = optionalValue!
// swiftlint:enable force_unwrapping

NEVER silence all warnings in a file:

// ❌ Nuclear option hides real issues
// swiftlint:disable all

// ✅ Disable specific rules for specific lines
// swiftlint:disable:next identifier_name
let x = calculateX()  // Math convention

CI Anti-Patterns

NEVER run expensive gates first:

# ❌ Slow feedback — tests run even if lint fails
jobs:
  test:  # 10 minutes
    ...
  lint:  # 30 seconds
    ...

# ✅ Fast feedback — fail fast on cheap checks
jobs:
  lint:
    runs-on: macos-14
    steps: [swiftlint, swiftformat]

  build:
    needs: lint  # Only if lint passes
    ...

  test:
    needs: build  # Only if build passes
    ...

NEVER run quality gates only on PR:

# ❌ Main branch can have violations
on:
  pull_request:
    branches: [main]

# ✅ Protect main branch too
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

Essential Configurations

Minimal SwiftLint (Start Here)

# .swiftlint.yml — Essential rules only

excluded:
  - Pods
  - .build
  - DerivedData

# Most impactful opt-in rules
opt_in_rules:
  - force_unwrapping          # Catches crashes
  - implicitly_unwrapped_optional
  - empty_count               # Performance
  - first_where               # Performance
  - contains_over_first_not_nil
  - fatal_error_message       # Debugging

# Sensible limits
line_length:
  warning: 120
  error: 200
  ignores_urls: true
  ignores_function_declarations: true

function_body_length:
  warning: 50
  error: 80

cyclomatic_complexity:
  warning: 10
  error: 15

type_body_length:
  warning: 300
  error: 400

file_length:
  warning: 500
  error: 800

Minimal SwiftFormat

# .swiftformat — Essentials only

--swiftversion 5.9
--exclude Pods,.build,DerivedData

--indent 4
--maxwidth 120
--wraparguments before-first
--wrapparameters before-first

# Non-controversial rules
--enable sortedImports
--enable trailingCommas
--enable redundantSelf
--enable redundantReturn
--enable blankLinesAtEndOfScope

# Disable controversial rules
--disable acronyms
--disable wrapMultilineStatementBraces

Xcode Build Settings (Quality Enforced)

# QualityGates.xcconfig

// Fail on warnings
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
GCC_TREAT_WARNINGS_AS_ERRORS = YES

// Strict concurrency (Swift 6 prep)
SWIFT_STRICT_CONCURRENCY = complete

// Static analyzer
RUN_CLANG_STATIC_ANALYZER = YES
CLANG_STATIC_ANALYZER_MODE = deep

CI Patterns

GitHub Actions (Minimal Effective)

name: Quality Gates

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  # Fast gates first
  lint:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - run: brew install swiftlint swiftformat
      - run: swiftlint lint --strict --reporter github-actions-logging
      - run: swiftformat . --lint

  # Build only if lint passes
  build:
    needs: lint
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Build with strict warnings
        run: |
          xcodebuild build \
            -scheme App \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            SWIFT_TREAT_WARNINGS_AS_ERRORS=YES

  # Test only if build passes
  test:
    needs: build
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Test with coverage
        run: |
          xcodebuild test \
            -scheme App \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -enableCodeCoverage YES \
            -resultBundlePath TestResults.xcresult

      - name: Check coverage threshold
        run: |
          COVERAGE=$(xcrun xccov view --report --json TestResults.xcresult | \
            jq '.targets[] | select(.name | contains("App")) | .lineCoverage * 100')
          echo "Coverage: ${COVERAGE}%"
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "❌ Coverage below 80%"
            exit 1
          fi

Pre-commit Hook (Local Enforcement)

#!/bin/bash
# .git/hooks/pre-commit

echo "Running pre-commit quality gates..."

# SwiftLint (fast)
if ! swiftlint lint --strict --quiet 2>/dev/null; then
    echo "❌ SwiftLint failed"
    exit 1
fi

# SwiftFormat check (fast)
if ! swiftformat . --lint 2>/dev/null; then
    echo "❌ Code formatting issues. Run: swiftformat ."
    exit 1
fi

echo "✅ Pre-commit checks passed"

Threshold Calibration

Finding the Right Coverage Threshold

Step 1: Measure current coverage
$ xcodebuild test -enableCodeCoverage YES ...
$ xcrun xccov view --report TestResults.xcresult

Step 2: Set threshold slightly below current
Current: 73% → Set threshold: 70%
Prevents regression without blocking

Step 3: Ratchet up over time
Week 1: 70%
Week 4: 75%
Week 8: 80%
Stop at: 80-85% (diminishing returns above)

SwiftLint Warning Budget

# Start permissive, tighten over time
# Week 1
MAX_WARNINGS: 100

# Week 4
MAX_WARNINGS: 50

# Week 8
MAX_WARNINGS: 20

# Target (after cleanup sprint)
MAX_WARNINGS: 0

Quick Reference

Gate Priority Order

Priority Gate Time ROI
1 SwiftLint ~30s High — catches bugs
2 SwiftFormat ~15s Medium — consistency
3 Build (0 warnings) ~2-5m High — compiler catches issues
4 Unit Tests ~5-15m High — catches regressions
5 Coverage Check ~1m Medium — enforces testing
6 Static Analysis ~5-10m Medium — deep issues

Red Flags

Smell Problem Fix
Gates disabled for "urgent" PR Culture problem Gates are non-negotiable
100% coverage requirement Gaming metrics 80-85% is optimal
All SwiftLint rules enabled Too noisy Curate impactful rules
Pre-commit takes > 30s Devs skip it Only fast checks locally
Different rules in CI vs local Surprises Same config everywhere

SwiftLint Rules Worth Enabling

Rule Why
force_unwrapping Prevents crashes
implicitly_unwrapped_optional Prevents crashes
empty_count Performance (O(1) vs O(n))
first_where Performance
fatal_error_message Better crash logs
unowned_variable_capture Prevents crashes
unused_closure_parameter Code hygiene

SwiftLint Rules to Avoid

Rule Why Avoid
explicit_type_interface Swift has inference for a reason
file_types_order Team preference varies
prefixed_toplevel_constant Outdated convention
extension_access_modifier Rarely adds value
Weekly Installs
15
GitHub Stars
6
First Seen
Jan 25, 2026
Installed on
opencode13
claude-code12
gemini-cli12
codex12
github-copilot11
antigravity10