compliance.rs

  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}