1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use clap::Parser;
5
6use compliance::{
7 checks::Reporter,
8 git::{CommitsFromVersionToHead, GetVersionTags, GitCommand, VersionTag},
9 github::GitHubClient,
10 report::ReportReviewSummary,
11};
12
13#[derive(Parser)]
14pub struct ComplianceArgs {
15 #[arg(value_parser = VersionTag::parse)]
16 // The version to be on the lookout for
17 pub(crate) version_tag: VersionTag,
18 #[arg(long)]
19 // The markdown file to write the compliance report to
20 report_path: PathBuf,
21 #[arg(long)]
22 // An optional branch to use instead of the determined version branch
23 branch: Option<String>,
24}
25
26impl ComplianceArgs {
27 pub(crate) fn version_tag(&self) -> &VersionTag {
28 &self.version_tag
29 }
30
31 fn version_branch(&self) -> String {
32 self.branch.clone().unwrap_or_else(|| {
33 format!(
34 "v{major}.{minor}.x",
35 major = self.version_tag().version().major,
36 minor = self.version_tag().version().minor
37 )
38 })
39 }
40}
41
42async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> {
43 let app_id = std::env::var("GITHUB_APP_ID").context("Missing GITHUB_APP_ID")?;
44 let key = std::env::var("GITHUB_APP_KEY").context("Missing GITHUB_APP_KEY")?;
45
46 let tag = args.version_tag();
47
48 let previous_version = GitCommand::run(GetVersionTags)?
49 .sorted()
50 .find_previous_minor_version(&tag)
51 .cloned()
52 .ok_or_else(|| {
53 anyhow::anyhow!(
54 "Could not find previous version for tag {tag}",
55 tag = tag.to_string()
56 )
57 })?;
58
59 println!(
60 "Checking compliance for version {} with version {} as base",
61 tag.version(),
62 previous_version.version()
63 );
64
65 let commits = GitCommand::run(CommitsFromVersionToHead::new(
66 previous_version,
67 args.version_branch(),
68 ))?;
69
70 let Some(range) = commits.range() else {
71 anyhow::bail!("No commits found to check");
72 };
73
74 println!("Checking commit range {range}, {} total", commits.len());
75
76 let client = GitHubClient::for_app(
77 app_id.parse().context("Failed to parse app ID as int")?,
78 key.as_ref(),
79 )
80 .await?;
81
82 println!("Initialized GitHub client for app ID {app_id}");
83
84 let report = Reporter::new(commits, &client).generate_report().await?;
85
86 println!(
87 "Generated report for version {}",
88 args.version_tag().to_string()
89 );
90
91 let summary = report.summary();
92
93 println!(
94 "Applying compliance labels to {} pull requests",
95 summary.prs_with_errors()
96 );
97
98 for report in report.errors() {
99 if let Some(pr_number) = report.commit.pr_number() {
100 println!("Adding review label to PR {}...", pr_number);
101
102 client
103 .ensure_pull_request_has_label(compliance::github::PR_REVIEW_LABEL, pr_number)
104 .await?;
105 }
106 }
107
108 report.write_markdown(&args.report_path)?;
109
110 println!("Wrote compliance report to {}", args.report_path.display());
111
112 match summary.review_summary() {
113 ReportReviewSummary::MissingReviews => Err(anyhow::anyhow!(
114 "Compliance check failed, found {} commits not reviewed",
115 summary.not_reviewed
116 )),
117 ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!(
118 "Compliance check failed with {} unreviewed commits and {} other issues",
119 summary.not_reviewed,
120 summary.errors
121 )),
122 ReportReviewSummary::NoIssuesFound => {
123 println!("No issues found, compliance check passed.");
124 Ok(())
125 }
126 }
127}
128
129pub fn check_compliance(args: ComplianceArgs) -> Result<()> {
130 tokio::runtime::Runtime::new()
131 .context("Failed to create tokio runtime")
132 .and_then(|handle| handle.block_on(check_compliance_impl(args)))
133}