use std::path::PathBuf;
use std::time::Instant;
use crate::collect::collector::{FetchOutcome, PerRepoFetch};
use crate::collect::correlate::correlate_commits;
use crate::commands::args::{ClassifyArgs, CollectArgs, ReportArgs};
use crate::commands::deployments::DeploymentsCollectArgs;
use crate::commands::incidents::IncidentsCollectArgs;
use crate::commands::jira::JiraSyncArgs;
use crate::commands::{classify, collect, deployments, dora, incidents, jira, pr_metrics, report};
use crate::commands::{dora::DoraArgs, pr_metrics::PrMetricsArgs};
use crate::core::config::Config;
use crate::core::db::Database;
use crate::core::progress::{ProgressBus, ProgressEvent, Stage};
use super::stage::{AuditSweepStats, DeclaredSkip, StaleFetch, SweepStage};
pub(crate) const TOTAL_STAGES: usize = 9;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SweepOptions {
pub output: Option<PathBuf>,
pub weeks: Option<u32>,
}
pub async fn run_full_sweep(
config: &Config,
db: &mut Database,
options: &SweepOptions,
progress: Option<&ProgressBus>,
) -> anyhow::Result<AuditSweepStats> {
if let Some(dir) = options.output.as_ref() {
std::fs::create_dir_all(dir)?;
}
let mut stats = AuditSweepStats::default();
record_declared_skips(&mut stats, config);
let collect_bus = progress.cloned().unwrap_or_default();
let t = begin(progress, &stats, SweepStage::Collect);
let args = CollectArgs {
weeks: options.weeks,
allow_stale: true,
..CollectArgs::default()
};
let result = collect::run_reporting_fetch(config.clone(), db, args, &collect_bus).await;
let result = record_stale_fetches(&mut stats, result);
finish(progress, &mut stats, SweepStage::Collect, t, result);
let t = begin(progress, &stats, SweepStage::Correlate);
let result = correlate_commits(db.connection_mut(), &collect_bus)
.map(|outcome| tracing::info!(summary = %outcome.summary(), "correlation pass finished"))
.map_err(anyhow::Error::from);
finish(progress, &mut stats, SweepStage::Correlate, t, result);
let t = begin(progress, &stats, SweepStage::Classify);
let args = ClassifyArgs {
weeks: options.weeks,
..ClassifyArgs::default()
};
let result = classify::run(config.clone(), db, args).await;
finish(progress, &mut stats, SweepStage::Classify, t, result);
let t = begin(progress, &stats, SweepStage::JiraSync);
let result = jira::run_sync(config.clone(), db, JiraSyncArgs::default()).await;
finish(progress, &mut stats, SweepStage::JiraSync, t, result);
let t = begin(progress, &stats, SweepStage::Deployments);
let result = deployments::run(config.clone(), db, DeploymentsCollectArgs::default()).await;
finish(progress, &mut stats, SweepStage::Deployments, t, result);
let t = begin(progress, &stats, SweepStage::Incidents);
let result = incidents::run(config.clone(), db, IncidentsCollectArgs::default());
finish(progress, &mut stats, SweepStage::Incidents, t, result);
let t = begin(progress, &stats, SweepStage::Dora);
let result = dora::run(config.clone(), db, DoraArgs::default());
finish(progress, &mut stats, SweepStage::Dora, t, result);
let t = begin(progress, &stats, SweepStage::PrMetrics);
let args = PrMetricsArgs {
weeks: options.weeks,
csv: true,
output: options.output.as_ref().map(|d| d.join("pr-metrics.csv")),
};
let result = pr_metrics::run(config.clone(), db, args);
finish(progress, &mut stats, SweepStage::PrMetrics, t, result);
let t = begin(progress, &stats, SweepStage::Report);
let args = ReportArgs {
output: options.output.clone(),
..ReportArgs::default()
};
let result = report::run(config.clone(), db, args);
finish(progress, &mut stats, SweepStage::Report, t, result);
tracing::info!(summary = %stats.summary(), "audit sweep finished");
Ok(stats)
}
fn record_declared_skips(stats: &mut AuditSweepStats, config: &Config) {
if let Some(reason) = config
.github
.as_ref()
.and_then(|gh| gh.work_items_declared_absent())
{
stats.record_declared_skip(DeclaredSkip {
leg: "GitHub work items".to_owned(),
reason: reason.to_owned(),
});
}
}
fn record_stale_fetches(
stats: &mut AuditSweepStats,
result: anyhow::Result<Vec<PerRepoFetch>>,
) -> anyhow::Result<()> {
for fetch in result? {
if let FetchOutcome::Failed { remote, error } = fetch.outcome {
stats.record_stale_fetch(StaleFetch {
repo: fetch.repo,
remote,
error,
});
}
}
Ok(())
}
fn begin(progress: Option<&ProgressBus>, stats: &AuditSweepStats, stage: SweepStage) -> Instant {
if let Some(bus) = progress {
let position = stats.outcomes.len() + 1;
bus.emit(
ProgressEvent::started(Stage::Audit, stage.as_str(), Some(1))
.with_detail(format!("stage {position} of {TOTAL_STAGES}")),
);
}
Instant::now()
}
fn finish(
progress: Option<&ProgressBus>,
stats: &mut AuditSweepStats,
stage: SweepStage,
started: Instant,
result: anyhow::Result<()>,
) {
if let Some(bus) = progress {
bus.emit(match &result {
Ok(()) => ProgressEvent::completed(Stage::Audit, stage.as_str(), 1),
Err(e) => ProgressEvent::failed(Stage::Audit, stage.as_str(), format!("{e:#}")),
});
}
stats.record(stage, started, result);
}