compliance.rs

  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}