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, Repository},
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_in_repo(
73 app_id.parse().context("Failed to parse app ID as int")?,
74 key.as_ref(),
75 Repository::ZED.owner(),
76 )
77 .await?;
78
79 println!("Initialized GitHub client for app ID {app_id}");
80
81 let report = Reporter::new(commits, &client).generate_report().await?;
82
83 println!(
84 "Generated report for version {}",
85 args.version_tag().to_string()
86 );
87
88 let summary = report.summary();
89
90 println!(
91 "Applying compliance labels to {} pull requests",
92 summary.prs_with_errors()
93 );
94
95 for report in report.errors() {
96 if let Some(pr_number) = report.commit.pr_number()
97 && let Ok(pull_request) = client.get_pull_request(&Repository::ZED, pr_number).await
98 && pull_request.labels.is_none_or(|labels| {
99 labels
100 .iter()
101 .all(|label| label != compliance::github::PR_REVIEW_LABEL)
102 })
103 {
104 println!("Adding review label to PR {}...", pr_number);
105
106 client
107 .add_label_to_issue(
108 &Repository::ZED,
109 compliance::github::PR_REVIEW_LABEL,
110 pr_number,
111 )
112 .await?;
113 }
114 }
115
116 report.write_markdown(&args.report_path)?;
117
118 println!("Wrote compliance report to {}", args.report_path.display());
119
120 match summary.review_summary() {
121 ReportReviewSummary::MissingReviews => Err(anyhow::anyhow!(
122 "Compliance check failed, found {} commits not reviewed",
123 summary.not_reviewed
124 )),
125 ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!(
126 "Compliance check failed with {} unreviewed commits and {} other issues",
127 summary.not_reviewed,
128 summary.errors
129 )),
130 ReportReviewSummary::NoIssuesFound => {
131 println!("No issues found, compliance check passed.");
132 Ok(())
133 }
134 }
135}
136
137pub fn check_compliance(args: ComplianceArgs) -> Result<()> {
138 tokio::runtime::Runtime::new()
139 .context("Failed to create tokio runtime")
140 .and_then(|handle| handle.block_on(check_compliance_impl(args)))
141}