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 println!("Adding review label to PR {}...", pr_number);
97
98 client
99 .ensure_pull_request_has_label(compliance::github::PR_REVIEW_LABEL, pr_number)
100 .await?;
101 }
102 }
103
104 report.write_markdown(&args.report_path)?;
105
106 println!("Wrote compliance report to {}", args.report_path.display());
107
108 match summary.review_summary() {
109 ReportReviewSummary::MissingReviews => Err(anyhow::anyhow!(
110 "Compliance check failed, found {} commits not reviewed",
111 summary.not_reviewed
112 )),
113 ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!(
114 "Compliance check failed with {} unreviewed commits and {} other issues",
115 summary.not_reviewed,
116 summary.errors
117 )),
118 ReportReviewSummary::NoIssuesFound => {
119 println!("No issues found, compliance check passed.");
120 Ok(())
121 }
122 }
123}
124
125pub fn check_compliance(args: ComplianceArgs) -> Result<()> {
126 tokio::runtime::Runtime::new()
127 .context("Failed to create tokio runtime")
128 .and_then(|handle| handle.block_on(check_compliance_impl(args)))
129}