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