148 lines
5.5 KiB
Python
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)
|