FLX-ENG-RFC-005 — Objective CI Gate Configuration · 10 Automated Quality Gates¶
| Field | Value |
|---|---|
| RFC ID | FLX-ENG-RFC-005 |
| Status | Active — Weeks 1–2 (configuration); Week 3 (enforcement) |
| Author | Arun Singh, Senior Distinguished Engineer / Architect (Consulting) |
| Reviewers | Rahul (Eng Lead), Tushar, Shrikant |
| Scope | 10 automated CI quality gates for DMS — build, coverage, lint, complexity, SAST, CVE, secrets, duplication, dead code, size |
| Parent Epic | GitHub Issue #6 — [EPIC] Objective Code Review CI Configuration |
| Priority | P0-CRITICAL |
| Related Issues | #20 (build), #21 (coverage), #22 (lint), #23 (complexity), #24 (SAST), #25 (CVE), #26 (secrets), #27 (duplication), #28 (dead code), #29 (size) |
| Blocks | RFC-PoC-1.3 (#56) — all gates run inside GitHub Actions build.yml |
TL;DR¶
10 automated quality gates run on every PR to main. Each gate has a pass/fail threshold calibrated to the current DMS codebase baseline. Gates start informational (Week 2) and become merge-blocking (Week 3). This RFC defines what each gate checks, the tool used, the initial threshold, and the step-by-step configuration.
Pre-condition: .NET 8 port (RFC-PoC-1.1 #53) must complete before gates can run reliably on CI.
1. Gate Architecture¶
PR opened on feature/* → GitHub Actions trigger
│
▼
[build] ──────────────────── dotnet build -c Release
│
▼ (on build success)
[test+coverage] ──────────── dotnet test + reportgenerator
│
├──[lint] ─────────────── dotnet format --verify-no-changes
├──[complexity] ────────── dotnet-metrics or NDepend
├──[sast] ─────────────── semgrep p/csharp
├──[cve-scan] ─────────── trivy fs
├──[secrets] ──────────── gitleaks
├──[duplication] ───────── jscpd
├──[dead-code] ─────────── roslyn unused symbol analysis
└──[size-gate] ─────────── custom PR diff size check
│
▼
All required gates green? → PR can be merged
2. Gate Definitions & Configuration¶
Gate 1 · Build (GitHub Issue #20)¶
Priority: P0 | Tool: dotnet build | Phase: Blocking from Day 1
Configuration:
# .github/workflows/build.yml
- name: Build
run: dotnet build src/distribution-management-server-layered/ -c Release --no-restore
Pass condition: Exit code 0, zero errors
Warning condition: Compiler warnings (CS0xxx) — log but don't fail in Week 1
Fail condition: Any build error (CS8xxx, CS0246, etc.)
Step-by-step setup: 1. Create .github/workflows/build.yml (see RFC-PoC-1.3 #56 for full workflow) 2. Add dotnet restore step before build (cache NuGet packages) 3. Run dotnet build -c Release — capture exit code 4. Upload build log as artifact on failure 5. Calibrate: run locally first, ensure baseline build passes before enabling CI gate
Gate 2 · Test Coverage (GitHub Issue #21)¶
Priority: P0 | Tool: dotnet test + reportgenerator | Phase: Informational Week 1, Blocking Week 3
Configuration:
- name: Test + Coverage
run: |
dotnet test DistributionServerUnitTest/ \
--collect:"XPlat Code Coverage" \
--results-directory ./coverage \
--no-build -c Release
dotnet tool run reportgenerator \
-reports:"coverage/**/*.xml" \
-targetdir:"coverage/report" \
-reporttypes:"Html;Cobertura"
- name: Coverage Gate
run: |
COVERAGE=$(python3 scripts/parse-coverage.py coverage/report/Cobertura.xml)
echo "Coverage: $COVERAGE%"
if [ "$COVERAGE" -lt "$MIN_COVERAGE" ]; then
echo "FAIL: Coverage $COVERAGE% < minimum $MIN_COVERAGE%"
exit 1
fi
env:
MIN_COVERAGE: 60 # calibrated from baseline in Week 2 pair session
Step-by-step setup: 1. Run dotnet test locally with coverage: dotnet test --collect:"XPlat Code Coverage" 2. Check current coverage percentage (Week 1 baseline scan) 3. Set MIN_COVERAGE = (current actual% - 5%) rounded down to nearest 5 4. In Week 3: raise threshold by 5 percentage points 5. Create scripts/parse-coverage.py to extract line coverage from Cobertura XML 6. Add reportgenerator as dotnet tool: dotnet tool install --global dotnet-reportgenerator-globaltool 7. Exclude generated files from coverage:
# .runsettings
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector uri="datacollector://Microsoft/CodeCoverage/2.0">
<Configuration>
<Exclude>
<ModulePath>.*Migrations.*</ModulePath>
<ModulePath>.*Program.*</ModulePath>
</Exclude>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
Gate 3 · Linter / Format (GitHub Issue #22)¶
Priority: P1 | Tool: dotnet format | Phase: Blocking from Week 2
Note: Issue #22 mentions Pylint/ruff which are Python linters. DMS is C#. The correct tool is
dotnet formatfor C# formatting enforcement. Issue #22 should be updated to reflectdotnet formatnot Pylint.
Configuration:
- name: Format Check
run: dotnet format src/distribution-management-server-layered/ --verify-no-changes --verbosity diagnostic
Pass condition: No formatting changes needed (exit 0)
Fail condition: Any unformatted file detected (exit non-zero)
Step-by-step setup: 1. Ensure .editorconfig exists at repo root with C# formatting rules:
[*.cs]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
dotnet format src/ --verify-no-changes locally first 3. If it fails: run dotnet format src/ (without --verify) to fix all files in one commit 4. Then add to CI as blocking gate 5. Add pre-commit hook (optional, Week 3): dotnet format on staged C# files Gate 4 · Cyclomatic Complexity (GitHub Issue #23)¶
Priority: P2 | Tool: dotnet-metrics or Roslyn analyzer | Phase: Informational Week 2, Blocking Week 3
Configuration:
- name: Complexity Analysis
run: |
dotnet build src/ -c Release -p:EnableRoslynAnalyzers=true
# CA1502: AvoidExcessiveComplexity is a built-in Roslyn rule
# Add to .editorconfig:
# dotnet_diagnostic.CA1502.severity = error
OR use lizard for cross-language complexity:
Pass condition: No method with cyclomatic complexity > 10
Warning: Complexity 8–10
Fail: Complexity > 10 (any single method)
Step-by-step setup: 1. Run lizard src/ -l cs locally — identify current max complexity 2. List the top 10 most complex methods (likely in service layer or sorting logic) 3. Set initial threshold to max(current highest, 15) — don't fail everything on Day 1 4. In Week 3: lower threshold to 10 (per ITIL best practice for maintainability) 5. For existing violations: create separate refactoring issues, don't block this gate
Gate 5 · SAST — Static Application Security Testing (GitHub Issue #24)¶
Priority: P0 | Tool: Semgrep (C# ruleset) | Phase: Blocking from Day 1 (Critical), Informational (High)
Configuration:
- name: SAST (Semgrep)
uses: semgrep/semgrep-action@v1
with:
config: >-
p/csharp
p/owasp-top-ten
p/secrets
generateSarif: true
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} # optional for CI
- name: Upload SAST SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
Pass condition: Zero Critical severity findings
Fail condition: Any Critical finding; >3 High findings
Warning condition: High findings (1–3) — logged, not blocking in Week 1
Step-by-step setup: 1. Run Semgrep locally: semgrep --config "p/csharp" src/ --json > semgrep-results.json 2. Review findings: cat semgrep-results.json | jq '.results[] | {severity:.extra.severity, rule:.check_id, path:.path}' 3. For each High/Critical: decide suppress or fix 4. Create .semgrepignore for false positives (document each with reason) 5. Add semgrep.sarif upload to GitHub Code Scanning (visible in Security tab) 6. Set Semgrep to fail CI on Critical: semgrep --error --severity=error
Common DMS findings to expect: - SQL injection via string interpolation in raw queries - Missing input validation on API parameters - Hardcoded credentials in test files (false positive if using test constants)
Gate 6 · Dependency CVE Scan (GitHub Issue #25)¶
Priority: P0 | Tool: Trivy | Phase: Blocking (Critical CVEs) from Day 1
Configuration:
- name: CVE Scan (Trivy)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
exit-code: '1'
severity: 'CRITICAL'
format: 'sarif'
output: 'trivy.sarif'
- name: Upload CVE SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
Pass condition: Zero CRITICAL CVEs
Fail condition: Any CRITICAL CVE
Warning condition: HIGH CVEs ≥5 fails; <5 is warning only
Step-by-step setup: 1. Install Trivy: brew install trivy 2. Run locally: trivy fs . --severity CRITICAL,HIGH 3. Review findings: note package name, CVE ID, current version, fixed version 4. For each CRITICAL: update package to fixed version immediately 5. For HIGH: document in docs/security/cve-exceptions.md with: - CVE ID - Package - Why it's acceptable for now - Planned fix date 6. Configure trivy.yaml for suppressed findings:
Gate 7 · Secret Scan (GitHub Issue #26)¶
Priority: P0 | Tool: gitleaks | Phase: Blocking from Day 1
Configuration:
- name: Secret Scan (gitleaks)
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_COMMENTS: false
Pass condition: Zero secrets detected
Fail condition: Any secret detected (API key, password, token, private key)
No exceptions: This gate is never suppressed for secrets. If a false positive is found: add to .gitleaks.toml with documented justification.
Step-by-step setup: 1. Create .gitleaks.toml at repo root:
title = "Flexli DMS Gitleaks Configuration"
[allowlist]
description = "Allowlisted false positives"
paths = [
'''DistributionServerUnitTest/.*TestConstants.*\.cs'''
]
regexes = [
'''EXAMPLE_KEY''', # test fixture constants
]
gitleaks detect --source . --log-opts="HEAD~100..HEAD" 3. If any real secrets found in history: rotate the credential FIRST, then clean git history 4. Confirm appsettings.Development.json, appsettings.Production.json are gitignored (they are per existing .gitignore) 5. Add to pre-commit hook: gitleaks protect --staged Gate 8 · Code Duplication (GitHub Issue #27)¶
Priority: P2 | Tool: jscpd | Phase: Informational Week 2, Blocking Week 3
Configuration:
- name: Duplication Check (jscpd)
run: |
npx jscpd src/ --min-lines 10 --min-tokens 50 \
--languages csharp \
--threshold 5 \
--reporters json \
--output /tmp/jscpd-report
DUPE_PERCENT=$(cat /tmp/jscpd-report/jscpd-report.json | jq '.statistics.total.percentage')
echo "Duplication: $DUPE_PERCENT%"
if (( $(echo "$DUPE_PERCENT > 5" | bc -l) )); then
echo "FAIL: Duplication $DUPE_PERCENT% > 5% threshold"
exit 1
fi
Pass condition: < 5% code duplication
Fail condition: ≥ 5% duplication
Step-by-step setup: 1. Run npx jscpd src/ --languages csharp locally 2. Note current duplication percentage (likely higher than 5% in Week 1) 3. Set initial threshold to (current + 2%) — don't fail everything on Day 1 4. In Week 3: lower to 5% after addressing most obvious duplications 5. Review jscpd report: identify top duplicated blocks → candidates for extraction to shared utilities
Gate 9 · Dead Code Detection (GitHub Issue #28)¶
Priority: P2 | Tool: Roslyn unused symbol analysis | Phase: Informational only
Configuration:
- name: Dead Code Analysis
run: |
dotnet build src/ -c Release \
-p:TreatWarningsAsErrors=false \
-p:EnableRoslynAnalyzers=true \
2>&1 | grep -E "CS0649|CS0169|CS8618|IDE0051|IDE0052" \
| tee /tmp/dead-code.txt
DEAD_COUNT=$(wc -l < /tmp/dead-code.txt)
echo "Dead code indicators: $DEAD_COUNT"
# Week 1-2: informational only
# Week 3: fail if > 10 indicators
Relevant Roslyn rules: - CS0649: Field is never assigned - CS0169: Field is never used - IDE0051: Remove unused private members - IDE0052: Remove unread private members
Step-by-step setup: 1. Add to .editorconfig:
dotnet build and count warning output for these rules 3. Document current dead code count as baseline 4. Create separate cleanup issues for any dead code found in critical paths Gate 10 · File and Function Size (GitHub Issue #29)¶
Priority: P2 | Tool: Custom Python script | Phase: Informational Week 2, Blocking Week 3
Configuration:
# scripts/size-gate.py
import os, re, sys, argparse
def check_sizes(src_dir, max_file_lines, max_method_lines):
violations = []
for root, _, files in os.walk(src_dir):
for f in files:
if not f.endswith('.cs'):
continue
path = os.path.join(root, f)
lines = open(path).readlines()
if len(lines) > max_file_lines:
violations.append(f"FILE TOO LARGE: {path} ({len(lines)} lines > {max_file_lines})")
return violations
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('src_dir')
parser.add_argument('--max-file-lines', type=int, default=400)
parser.add_argument('--max-method-lines', type=int, default=50)
args = parser.parse_args()
violations = check_sizes(args.src_dir, args.max_file_lines, args.max_method_lines)
for v in violations:
print(v)
sys.exit(1 if violations else 0)
Thresholds: - File: ≤ 400 lines (week 1: set to actual max + 50% as informational threshold) - Method: ≤ 50 lines
3. Gate Rollout Timeline¶
| Gate | Tool | Week 1 | Week 2 | Week 3 |
|---|---|---|---|---|
| Build | dotnet build | Blocking | Blocking | Blocking |
| Coverage | dotnet test | Informational | Informational | Blocking |
| Lint | dotnet format | Blocking | Blocking | Blocking |
| Complexity | lizard/roslyn | Off | Informational | Blocking |
| SAST | semgrep | Blocking (Critical) | Blocking (Critical) | Blocking (High+) |
| CVE | trivy | Blocking (Critical) | Blocking (Critical) | Blocking (Critical) |
| Secrets | gitleaks | Blocking | Blocking | Blocking |
| Duplication | jscpd | Off | Informational | Blocking |
| Dead Code | roslyn | Off | Informational | Informational |
| Size | custom | Off | Informational | Informational |
4. Dependencies¶
| Dependency | Required for |
|---|---|
| .NET 8 port (RFC-PoC-1.1 #53) | All gates — cannot run reliably on unsupported .NET 6 SDK |
| GitHub Actions CI (RFC-PoC-1.3 #56) | Gate execution engine |
| Week 2 pair session (US-2.2 #18) | Threshold calibration for coverage and complexity |
5. Success Criteria¶
- All 10 gates defined in
build.ymlwith correct tools and thresholds - Gates 1, 5, 6, 7 blocking from Week 1
- All 10 gates blocking by Week 3
- First-pass scan results committed to
docs/developer-guide/week1-scans/ - No suppressions without documented justification in
docs/security/