Files
RepoPulse-Public/backend/app/release_gate.py
T

148 lines
5.5 KiB
Python

from __future__ import annotations
from .models import (
DependencySignals,
Finding,
GateCheck,
GateState,
ReleaseGate,
RepositoryMetrics,
RepositoryPolicy,
ScanCoverage,
Severity,
TestSignals,
)
def build_release_gate(
*,
health_score: int,
findings: list[Finding],
metrics: RepositoryMetrics,
tests: TestSignals,
dependencies: DependencySignals,
coverage: ScanCoverage | None = None,
policy: RepositoryPolicy | None = None,
) -> ReleaseGate:
policy = policy or RepositoryPolicy()
active_findings = [item for item in findings if not item.suppressed]
blockers = [item for item in active_findings if item.severity == Severity.BLOCKER]
warnings = [item for item in active_findings if item.severity == Severity.WARNING]
dependency_issues = dependencies.unbounded_dependencies + dependencies.unpinned_dependencies
checks = [
GateCheck(
key="security",
label="Security basics",
state=GateState.BLOCK if len(blockers) > policy.max_blockers else GateState.PASS,
detail=(
f"{len(blockers)} blocker finding(s), policy allows {policy.max_blockers}"
if blockers
else "No blocker-level secret/environment findings"
),
),
GateCheck(
key="tests",
label="Tests" + (" (required)" if policy.require_tests else ""),
state=(
GateState.BLOCK
if policy.require_tests and not metrics.test_files
else GateState.PASS
if metrics.test_files
else GateState.WARN
),
detail=(
f"{metrics.test_files} test file(s) detected"
if metrics.test_files
else "Tests are required by repository policy"
if policy.require_tests
else "No test files detected"
),
),
GateCheck(
key="ci",
label="Continuous integration" + (" (required)" if policy.require_ci else ""),
state=(
GateState.BLOCK
if policy.require_ci and not tests.ci_validation_workflows
else GateState.PASS
if tests.ci_validation_workflows
else GateState.WARN
),
detail=(
f"{len(tests.ci_validation_workflows)} validation workflow(s) detected"
if tests.ci_validation_workflows
else f"{len(tests.ci_deploy_only_workflows)} deploy-only workflow(s) do not provide validation evidence"
if tests.ci_deploy_only_workflows
else "CI is required by repository policy"
if policy.require_ci
else "No validation workflow detected"
),
),
GateCheck(
key="coverage",
label="Coverage evidence",
state=(
GateState.PASS
if tests.coverage_evidence
else GateState.WARN
if metrics.test_files
else GateState.PASS
),
detail=(
f"{len(tests.coverage_evidence)} coverage evidence source(s) detected"
if tests.coverage_evidence
else "Tests exist but no coverage-producing command or retained coverage output was detected"
if metrics.test_files
else "No test suite is present to produce coverage evidence"
),
),
GateCheck(
key="analysis_coverage",
label="Analysis coverage",
state=GateState.WARN if coverage and coverage.truncated else GateState.PASS,
detail=(
"; ".join(coverage.notes) if coverage and coverage.truncated else "Analysis completed within configured limits"
),
),
GateCheck(
key="dependencies",
label="Dependency hygiene",
state=GateState.WARN if dependency_issues else GateState.PASS,
detail=(
f"{dependencies.unbounded_dependencies} unbounded and {dependencies.unpinned_dependencies} unpinned declaration(s)"
if dependency_issues
else f"{dependencies.declared_dependencies} dependency declaration(s), no unbounded or unpinned versions"
),
),
GateCheck(
key="health",
label="Repository health",
state=(
GateState.PASS
if health_score >= max(85, policy.min_health_score)
else GateState.WARN
if health_score >= max(65, policy.min_health_score)
else GateState.BLOCK
),
detail=(
f"Health score {health_score}/100"
if not policy.min_health_score
else f"Health score {health_score}/100; policy minimum {policy.min_health_score}"
),
),
GateCheck(
key="warnings",
label="Engineering warnings",
state=GateState.WARN if warnings else GateState.PASS,
detail=f"{len(warnings)} warning finding(s)" if warnings else "No warning-level findings",
),
]
if any(check.state == GateState.BLOCK for check in checks):
status = "BLOCKED"
elif any(check.state == GateState.WARN for check in checks):
status = "READY WITH WARNINGS"
else:
status = "READY"
return ReleaseGate(status=status, checks=checks)