1use std::{path::PathBuf, rc::Rc};
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5
6use compliance::{
7 checks::Reporter,
8 git::{CommitsFromVersionToVersion, GetVersionTags, GitCommand, InfoForCommit, VersionTag},
9 github::{GithubApiClient as _, OctocrabClient, Repository},
10 report::ReportReviewSummary,
11};
12
13const MAX_CONCURRENT_REQUESTS: usize = 5;
14
15#[derive(Parser)]
16pub(crate) struct ComplianceArgs {
17 #[clap(subcommand)]
18 mode: ComplianceMode,
19}
20
21const IGNORE_LIST: &[&str] = &[
22 "75fa566511e3ae7d03cfd76008512080291bd81d", // GitHub nuked this PR out of orbit
23];
24
25#[derive(Subcommand)]
26pub(crate) enum ComplianceMode {
27 // Check compliance for all commits between two version tags
28 Version(VersionArgs),
29 // Check compliance for a single commit
30 Single {
31 // The full commit SHA to check
32 commit_sha: String,
33 },
34}
35
36#[derive(Parser)]
37pub(crate) struct VersionArgs {
38 #[arg(value_parser = VersionTag::parse)]
39 // The version to be on the lookout for
40 version_tag: VersionTag,
41 #[arg(long)]
42 // The markdown file to write the compliance report to
43 report_path: PathBuf,
44 #[arg(long)]
45 // An optional branch to use instead of the determined version branch
46 branch: Option<String>,
47}
48
49impl VersionArgs {
50 pub(crate) fn version_tag(&self) -> &VersionTag {
51 &self.version_tag
52 }
53
54 fn version_head(&self) -> String {
55 self.branch
56 .clone()
57 .unwrap_or_else(|| self.version_tag().to_string())
58 }
59}
60
61async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> {
62 let app_id = std::env::var("GITHUB_APP_ID").context("Missing GITHUB_APP_ID")?;
63 let key = std::env::var("GITHUB_APP_KEY").context("Missing GITHUB_APP_KEY")?;
64
65 let client = Rc::new(
66 OctocrabClient::new(
67 app_id.parse().context("Failed to parse app ID as int")?,
68 key.as_ref(),
69 Repository::ZED.owner(),
70 )
71 .await?,
72 );
73
74 println!("Initialized GitHub client for app ID {app_id}");
75
76 let args = match args.mode {
77 ComplianceMode::Version(version) => version,
78 ComplianceMode::Single { commit_sha } => {
79 let commit = GitCommand::run(InfoForCommit::new(&commit_sha))?;
80
81 return match Reporter::result_for_commit(commit, client).await {
82 Ok(review_success) => {
83 println!("Check for commit {commit_sha} succeeded. Result: {review_success}",);
84 Ok(())
85 }
86
87 Err(review_failure) => Err(anyhow::anyhow!(
88 "Check for commit {commit_sha} failed. Result: {review_failure}"
89 )),
90 };
91 }
92 };
93
94 let tag = args.version_tag();
95
96 let previous_version = GitCommand::run(GetVersionTags)?
97 .sorted()
98 .find_previous_minor_version(&tag)
99 .cloned()
100 .ok_or_else(|| {
101 anyhow::anyhow!(
102 "Could not find previous version for tag {tag}",
103 tag = tag.to_string()
104 )
105 })?;
106
107 println!(
108 "Checking compliance for version {} with version {} as base",
109 tag.version(),
110 previous_version.version()
111 );
112
113 let commits = GitCommand::run(CommitsFromVersionToVersion::new(
114 previous_version,
115 args.version_head(),
116 ))?;
117
118 let Some(range) = commits.range() else {
119 anyhow::bail!("No commits found to check");
120 };
121
122 println!("Checking commit range {range}, {} total", commits.len());
123
124 let report = Reporter::new(commits, client.clone())
125 .generate_report(MAX_CONCURRENT_REQUESTS)
126 .await;
127
128 println!(
129 "Generated report for version {}",
130 args.version_tag().to_string()
131 );
132
133 let summary = report.summary();
134
135 println!(
136 "Applying compliance labels to {} pull requests",
137 summary.prs_with_errors()
138 );
139
140 let all_errors_known = report.errors().all(|error| {
141 error.is_unknown_error() && IGNORE_LIST.contains(&error.commit.sha().as_str())
142 });
143
144 for report in report.errors() {
145 if let Some(pr_number) = report.commit.pr_number()
146 && let Ok(pull_request) = client.get_pull_request(&Repository::ZED, pr_number).await
147 && pull_request.labels.is_none_or(|labels| {
148 labels
149 .iter()
150 .all(|label| label != compliance::github::PR_REVIEW_LABEL)
151 })
152 {
153 println!("Adding review label to PR {}...", pr_number);
154
155 client
156 .add_label_to_issue(
157 &Repository::ZED,
158 compliance::github::PR_REVIEW_LABEL,
159 pr_number,
160 )
161 .await?;
162 }
163 }
164
165 report.write_markdown(&args.report_path)?;
166
167 println!("Wrote compliance report to {}", args.report_path.display());
168
169 match summary.review_summary() {
170 ReportReviewSummary::MissingReviews => Err(anyhow::anyhow!(
171 "Compliance check failed, found {} commits not reviewed",
172 summary.not_reviewed
173 )),
174 ReportReviewSummary::MissingReviewsWithErrors if all_errors_known => {
175 println!(
176 "Compliance check failed with {} unreviewed commits, but all errors are known.",
177 summary.not_reviewed
178 );
179
180 Ok(())
181 }
182 ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!(
183 "Compliance check failed with {} unreviewed commits and {} other issues",
184 summary.not_reviewed,
185 summary.errors
186 )),
187 ReportReviewSummary::NoIssuesFound => {
188 println!("No issues found, compliance check passed.");
189 Ok(())
190 }
191 }
192}
193
194pub fn check_compliance(args: ComplianceArgs) -> Result<()> {
195 tokio::runtime::Runtime::new()
196 .context("Failed to create tokio runtime")
197 .and_then(|handle| handle.block_on(check_compliance_impl(args)))
198}