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