///|
pub(all) enum GateStatus {
Passed
Review
Rejected
} derive(Eq, @debug.Debug)
///|
pub fn GateStatus::to_text(self : GateStatus) -> String {
match self {
Passed => "passed"
Review => "review"
Rejected => "rejected"
}
}
///|
pub(all) enum FindingLevel {
Attention
Blocking
} derive(Eq, @debug.Debug)
///|
pub fn FindingLevel::to_text(self : FindingLevel) -> String {
match self {
Attention => "attention"
Blocking => "blocking"
}
}
///|
pub struct Finding {
level : FindingLevel
code : String
path : String
message : String
} derive(Eq, @debug.Debug)
///|
pub fn Finding::new(
level : FindingLevel,
code : String,
path : String,
message : String,
) -> Finding {
{ level, code, path, message, }
}
///|
pub fn Finding::level(self : Finding) -> FindingLevel {
self.level
}
///|
pub fn Finding::code(self : Finding) -> String {
self.code
}
///|
pub fn Finding::path(self : Finding) -> String {
self.path
}
///|
pub fn Finding::message(self : Finding) -> String {
self.message
}
///|
pub fn Finding::to_text(self : Finding) -> String {
self.level.to_text() + " " + self.code + " " + self.path + ": " + self.message
}
///|
pub struct PathDecision {
path : String
owners : Array[String]
owner_pattern : String?
matched_rules : Array[String]
approvals : Int
checks : Array[String]
labels : Array[String]
forbidden : Array[ChangeKind]
max_lines : Int?
release_note : Bool
allow_binary : Bool
} derive(Eq, @debug.Debug)
///|
pub fn PathDecision::path(self : PathDecision) -> String {
self.path
}
///|
pub fn PathDecision::owners(self : PathDecision) -> Array[String] {
self.owners.copy()
}
///|
pub fn PathDecision::owner_pattern(self : PathDecision) -> String? {
self.owner_pattern
}
///|
pub fn PathDecision::matched_rules(self : PathDecision) -> Array[String] {
self.matched_rules.copy()
}
///|
pub fn PathDecision::approvals(self : PathDecision) -> Int {
self.approvals
}
///|
pub fn PathDecision::checks(self : PathDecision) -> Array[String] {
self.checks.copy()
}
///|
pub fn PathDecision::labels(self : PathDecision) -> Array[String] {
self.labels.copy()
}
///|
pub fn PathDecision::forbidden(self : PathDecision) -> Array[ChangeKind] {
self.forbidden.copy()
}
///|
pub fn PathDecision::max_lines(self : PathDecision) -> Int? {
self.max_lines
}
///|
pub fn PathDecision::requires_release_note(self : PathDecision) -> Bool {
self.release_note
}
///|
pub fn PathDecision::allows_binary(self : PathDecision) -> Bool {
self.allow_binary
}
///|
fn resolve_path(policy : Policy, path : String) -> PathDecision {
let mut owners : Array[String] = []
let mut owner_pattern : String? = None
let mut owner_specificity = -1
for rule in policy.owner_rules {
if rule.pattern.matches(path) &&
rule.pattern.specificity() >= owner_specificity {
owners = rule.owners.copy()
owner_pattern = Some(rule.pattern.source())
owner_specificity = rule.pattern.specificity()
}
}
let matched_rules : Array[String] = []
let checks : Array[String] = []
let labels : Array[String] = []
let forbidden : Array[ChangeKind] = []
let mut approvals = policy.default_approvals
let mut max_lines : Int? = None
let mut release_note = false
let mut allow_binary = true
for rule in policy.rules {
if !rule.pattern.matches(path) {
continue
}
matched_rules.push(rule.name)
if rule.approvals > approvals {
approvals = rule.approvals
}
for check in rule.checks {
push_unique_string(checks, check)
}
for label in rule.labels {
push_unique_string(labels, label)
}
for kind in rule.forbidden {
if !contains_kind(forbidden, kind) {
forbidden.push(kind)
}
}
match rule.max_lines {
Some(value) =>
match max_lines {
Some(current) => if value < current { max_lines = Some(value) }
None => max_lines = Some(value)
}
None => ()
}
release_note = release_note || rule.release_note
allow_binary = allow_binary && rule.allow_binary
}
checks.sort_by(fn(left, right) { left.lexical_compare(right) })
labels.sort_by(fn(left, right) { left.lexical_compare(right) })
matched_rules.sort_by(fn(left, right) { left.lexical_compare(right) })
{
path,
owners,
owner_pattern,
matched_rules,
approvals,
checks,
labels,
forbidden,
max_lines,
release_note,
allow_binary,
}
}
///|
pub fn Policy::explain(
self : Policy,
path : String,
) -> Result[PathDecision, Diagnostic] {
if !is_safe_repo_path(path) {
return Err(
Diagnostic::new(
"explain.path.unsafe", "path", "path is not repository-relative and normalized",
"safe repository-relative path", path,
),
)
}
Ok(resolve_path(self, path))
}
///|
fn check_state(evidence : Evidence, expected : String) -> CheckState? {
let mut state : CheckState? = None
for result in evidence.checks {
if result.name == expected {
match result.state {
Failed => return Some(Failed)
Pending => state = Some(Pending)
Passed => if state is None { state = Some(Passed) }
}
}
}
state
}
///|
fn count_owner_approvals(
owners : Array[String],
approvals : Array[String],
) -> Int {
let mut count = 0
if owners.is_empty() {
return approvals.length()
}
for owner in owners {
if contains_string(approvals, owner) {
count = count + 1
}
}
count
}
///|
fn add_finding(
findings : Array[Finding],
level : FindingLevel,
code : String,
path : String,
message : String,
) -> Unit {
findings.push(Finding::new(level, code, path, message))
}
///|
pub struct AuditReport {
change_id : String
status : GateStatus
total_lines : Int
decisions : Array[PathDecision]
findings : Array[Finding]
plan : ReviewPlan
} derive(Eq, @debug.Debug)
///|
pub fn AuditReport::change_id(self : AuditReport) -> String {
self.change_id
}
///|
pub fn AuditReport::status(self : AuditReport) -> GateStatus {
self.status
}
///|
pub fn AuditReport::total_lines(self : AuditReport) -> Int {
self.total_lines
}
///|
pub fn AuditReport::decisions(self : AuditReport) -> Array[PathDecision] {
self.decisions.copy()
}
///|
pub fn AuditReport::findings(self : AuditReport) -> Array[Finding] {
self.findings.copy()
}
///|
pub fn AuditReport::plan(self : AuditReport) -> ReviewPlan {
self.plan
}
///|
pub fn AuditReport::has_code(self : AuditReport, code : String) -> Bool {
for finding in self.findings {
if finding.code == code {
return true
}
}
false
}
///|
fn change_policy_paths(change : Change) -> Array[String] {
if change.kind == Rename && change.old_path != change.new_path {
[change.old_path.unwrap(), change.new_path.unwrap()]
} else {
[change.policy_path()]
}
}
///|
fn evaluate_path(
policy : Policy,
change : Change,
path : String,
evidence : Evidence,
findings : Array[Finding],
) -> PathDecision {
let decision = resolve_path(policy, path)
if policy.require_owned && decision.owners.is_empty() {
add_finding(
findings,
Blocking,
"owner.unassigned",
path,
"no owner rule covers this path",
)
}
let owner_approvals = count_owner_approvals(
decision.owners,
evidence.approvals,
)
if !decision.owners.is_empty() &&
decision.approvals > decision.owners.length() {
add_finding(
findings,
Blocking,
"approval.quorum.impossible",
path,
"needs " +
decision.approvals.to_string() +
" owner approval(s), but only " +
decision.owners.length().to_string() +
" owner principal(s) are eligible",
)
} else if owner_approvals < decision.approvals {
add_finding(
findings,
Attention,
"approval.missing",
path,
"needs " +
decision.approvals.to_string() +
" owner approval(s), has " +
owner_approvals.to_string(),
)
}
if contains_kind(decision.forbidden, change.kind) {
add_finding(
findings,
Blocking,
"operation.forbidden",
path,
change.kind.to_text() + " is forbidden by a matching rule",
)
}
match decision.max_lines {
Some(limit) =>
if change.changed_lines() > limit {
add_finding(
findings,
Blocking,
"budget.path.exceeded",
path,
change.changed_lines().to_string() +
" changed lines exceed limit " +
limit.to_string(),
)
}
None => ()
}
if change.binary && !decision.allow_binary {
add_finding(
findings,
Blocking,
"binary.forbidden",
path,
"binary content is denied by a matching rule",
)
}
for required in decision.checks {
match check_state(evidence, required) {
Some(Passed) => ()
Some(Failed) =>
add_finding(
findings,
Blocking,
"check.failed",
path,
"required check " + required + " failed",
)
Some(Pending) =>
add_finding(
findings,
Attention,
"check.pending",
path,
"required check " + required + " is pending",
)
None =>
add_finding(
findings,
Attention,
"check.missing",
path,
"required check " + required + " has no result",
)
}
}
for required in decision.labels {
if !contains_string(evidence.labels, required) {
add_finding(
findings,
Attention,
"label.missing",
path,
"required label " + required + " is absent",
)
}
}
if decision.release_note && !evidence.release_note {
add_finding(
findings,
Attention,
"release_note.missing",
path,
"a release note is required",
)
}
decision
}
///|
pub fn evaluate(policy : Policy, changes : ChangeSet) -> AuditReport {
let findings : Array[Finding] = []
let decisions : Array[PathDecision] = []
let evidence = changes.evidence
let mut total_lines = 0
for change in changes.changes {
total_lines = total_lines + change.changed_lines()
for path in change_policy_paths(change) {
decisions.push(evaluate_path(policy, change, path, evidence, findings))
}
}
match policy.max_total_lines {
Some(limit) =>
if total_lines > limit {
add_finding(
findings,
Blocking,
"budget.total.exceeded",
"*",
total_lines.to_string() +
" changed lines exceed total limit " +
limit.to_string(),
)
}
None => ()
}
let mut status = GateStatus::Passed
for finding in findings {
match finding.level {
Blocking => status = Rejected
Attention => if status == GateStatus::Passed { status = Review }
}
}
let plan = build_review_plan(decisions, evidence)
{ change_id: changes.id, status, total_lines, decisions, findings, plan, }
}
///|
pub fn AuditReport::to_text(self : AuditReport) -> String {
let output = StringBuilder()
output.write_string(
"MOONCHANGE_REPORT 1\nID " +
self.change_id +
"\nSTATUS " +
self.status.to_text() +
"\nTOTAL_LINES " +
self.total_lines.to_string(),
)
for decision in self.decisions {
let owner_pattern = decision.owner_pattern.unwrap_or("-")
output.write_string(
"\nPATH " +
decision.path +
" owners=" +
format_optional_csv(decision.owners) +
" owner_pattern=" +
owner_pattern +
" approvals=" +
decision.approvals.to_string() +
" rules=" +
format_optional_csv(decision.matched_rules),
)
}
for finding in self.findings {
output.write_string("\nFINDING " + finding.to_text())
}
output.write_string("\nPLAN\n" + self.plan.to_text())
output.to_string()
}