1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use clap::Parser;
5
6use compliance::{
7 checks::Reporter,
8 git::{CommitsFromVersionToVersion, 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_head(&self) -> String {
32 self.branch
33 .clone()
34 .unwrap_or_else(|| self.version_tag().to_string())
35 }
36}
37
38async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> {
39 let app_id = std::env::var("GITHUB_APP_ID").context("Missing GITHUB_APP_ID")?;
40 let key = std::env::var("GITHUB_APP_KEY").context("Missing GITHUB_APP_KEY")?;
41
42 let tag = args.version_tag();
43
44 let previous_version = GitCommand::run(GetVersionTags)?
45 .sorted()
46 .find_previous_minor_version(&tag)
47 .cloned()
48 .ok_or_else(|| {
49 anyhow::anyhow!(
50 "Could not find previous version for tag {tag}",
51 tag = tag.to_string()
52 )
53 })?;
54
55 println!(
56 "Checking compliance for version {} with version {} as base",
57 tag.version(),
58 previous_version.version()
59 );
60
61 let commits = GitCommand::run(CommitsFromVersionToVersion::new(
62 previous_version,
63 args.version_head(),
64 ))?;
65
66 let Some(range) = commits.range() else {
67 anyhow::bail!("No commits found to check");
68 };
69
70 println!("Checking commit range {range}, {} total", commits.len());
71
72 let client = GitHubClient::for_app(
73 app_id.parse().context("Failed to parse app ID as int")?,
74 key.as_ref(),
75 )
76 .await?;
77
78 println!("Initialized GitHub client for app ID {app_id}");
79
80 let report = Reporter::new(commits, &client).generate_report().await?;
81
82 println!(
83 "Generated report for version {}",
84 args.version_tag().to_string()
85 );
86
87 let summary = report.summary();
88
89 println!(
90 "Applying compliance labels to {} pull requests",
91 summary.prs_with_errors()
92 );
93
94 for report in report.errors() {
95 if let Some(pr_number) = report.commit.pr_number()
96 && let Ok(pull_request) = client.get_pull_request(pr_number).await
97 && pull_request.labels.is_none_or(|labels| {
98 labels
99 .iter()
100 .all(|label| label != compliance::github::PR_REVIEW_LABEL)
101 })
102 {
103 println!("Adding review label to PR {}...", pr_number);
104
105 client
106 .add_label_to_issue(compliance::github::PR_REVIEW_LABEL, pr_number)
107 .await?;
108 }
109 }
110
111 report.write_markdown(&args.report_path)?;
112
113 println!("Wrote compliance report to {}", args.report_path.display());
114
115 match summary.review_summary() {
116 ReportReviewSummary::MissingReviews => Err(anyhow::anyhow!(
117 "Compliance check failed, found {} commits not reviewed",
118 summary.not_reviewed
119 )),
120 ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!(
121 "Compliance check failed with {} unreviewed commits and {} other issues",
122 summary.not_reviewed,
123 summary.errors
124 )),
125 ReportReviewSummary::NoIssuesFound => {
126 println!("No issues found, compliance check passed.");
127 Ok(())
128 }
129 }
130}
131
132pub fn check_compliance(args: ComplianceArgs) -> Result<()> {
133 tokio::runtime::Runtime::new()
134 .context("Failed to create tokio runtime")
135 .and_then(|handle| handle.block_on(check_compliance_impl(args)))
136}