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