compliance.rs

  1use std::path::PathBuf;
  2
  3use anyhow::{Context, Result};
  4use clap::Parser;
  5
  6use compliance::{
  7    checks::Reporter,
  8    git::{CommitsFromVersionToHead, 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_branch(&self) -> String {
 32        self.branch.clone().unwrap_or_else(|| {
 33            format!(
 34                "v{major}.{minor}.x",
 35                major = self.version_tag().version().major,
 36                minor = self.version_tag().version().minor
 37            )
 38        })
 39    }
 40}
 41
 42async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> {
 43    let app_id = std::env::var("GITHUB_APP_ID").context("Missing GITHUB_APP_ID")?;
 44    let key = std::env::var("GITHUB_APP_KEY").context("Missing GITHUB_APP_KEY")?;
 45
 46    let tag = args.version_tag();
 47
 48    let previous_version = GitCommand::run(GetVersionTags)?
 49        .sorted()
 50        .find_previous_minor_version(&tag)
 51        .cloned()
 52        .ok_or_else(|| {
 53            anyhow::anyhow!(
 54                "Could not find previous version for tag {tag}",
 55                tag = tag.to_string()
 56            )
 57        })?;
 58
 59    println!(
 60        "Checking compliance for version {} with version {} as base",
 61        tag.version(),
 62        previous_version.version()
 63    );
 64
 65    let commits = GitCommand::run(CommitsFromVersionToHead::new(
 66        previous_version,
 67        args.version_branch(),
 68    ))?;
 69
 70    let Some(range) = commits.range() else {
 71        anyhow::bail!("No commits found to check");
 72    };
 73
 74    println!("Checking commit range {range}, {} total", commits.len());
 75
 76    let client = GitHubClient::for_app(
 77        app_id.parse().context("Failed to parse app ID as int")?,
 78        key.as_ref(),
 79    )
 80    .await?;
 81
 82    println!("Initialized GitHub client for app ID {app_id}");
 83
 84    let report = Reporter::new(commits, &client).generate_report().await?;
 85
 86    println!(
 87        "Generated report for version {}",
 88        args.version_tag().to_string()
 89    );
 90
 91    let summary = report.summary();
 92
 93    println!(
 94        "Applying compliance labels to {} pull requests",
 95        summary.pull_requests
 96    );
 97
 98    for report in report.errors() {
 99        if let Some(pr_number) = report.commit.pr_number() {
100            println!("Adding review label to PR {}...", pr_number);
101
102            client
103                .add_label_to_pull_request(compliance::github::PR_REVIEW_LABEL, pr_number)
104                .await?;
105        }
106    }
107
108    let report_path = args.report_path.with_extension("md");
109
110    report.write_markdown(&report_path)?;
111
112    println!("Wrote compliance report to {}", report_path.display());
113
114    match summary.review_summary() {
115        ReportReviewSummary::MissingReviews => Err(anyhow::anyhow!(
116            "Compliance check failed, found {} commits not reviewed",
117            summary.not_reviewed
118        )),
119        ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!(
120            "Compliance check failed with {} unreviewed commits and {} other issues",
121            summary.not_reviewed,
122            summary.errors
123        )),
124        ReportReviewSummary::NoIssuesFound => {
125            println!("No issues found, compliance check passed.");
126            Ok(())
127        }
128    }
129}
130
131pub fn check_compliance(args: ComplianceArgs) -> Result<()> {
132    tokio::runtime::Runtime::new()
133        .context("Failed to create tokio runtime")
134        .and_then(|handle| handle.block_on(check_compliance_impl(args)))
135}