use std::sync::Arc;
use tracing::{info, warn};
use crate::{
config::ReviewConfig,
integrations::github::{AuthStrategy, GithubClient, RunMode, post_pr_review},
models::ReviewResult,
pipeline::{
output::{print_review_result, write_review_log},
trigger::{TriggerDecision, effective_dry_run},
},
store::DedupStore,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FinalizeAction {
Post,
LogOnly,
}
pub fn decide_action(
config_dry_run: bool,
trigger: TriggerDecision,
allow_posting: bool,
is_github_source: bool,
) -> FinalizeAction {
let dry = effective_dry_run(config_dry_run, trigger);
if !dry && allow_posting && is_github_source {
FinalizeAction::Post
} else {
FinalizeAction::LogOnly
}
}
pub struct PostContext<'a> {
pub owner: &'a str,
pub repo: &'a str,
pub pr: u64,
pub head_sha: &'a str,
pub run_mode: RunMode,
pub dedup: Option<&'a Arc<DedupStore>>,
}
pub async fn finalize_review(
mut result: ReviewResult,
config: &ReviewConfig,
trigger: TriggerDecision,
allow_posting: bool,
write_log: bool,
print_result: bool,
post_ctx: PostContext<'_>,
) -> ReviewResult {
let is_github = !post_ctx.owner.is_empty() && post_ctx.owner != "local";
let action = decide_action(config.dry_run, trigger, allow_posting, is_github);
match action {
FinalizeAction::Post => {
match post_live(&mut result, config, &post_ctx).await {
Ok(()) => {
result.posted = true;
result.dry_run = false;
info!(
owner = post_ctx.owner,
repo = post_ctx.repo,
pr = post_ctx.pr,
verdict = %result.verdict,
"review posted live to GitHub PR"
);
if let Some(store) = post_ctx.dedup
&& !post_ctx.head_sha.is_empty()
&& let Err(e) = store.complete(
post_ctx.owner,
post_ctx.repo,
post_ctx.pr,
post_ctx.head_sha,
)
{
warn!("dedup complete() failed (non-fatal): {e}");
}
}
Err(e) => {
warn!("live post failed (falling back to dry-run log): {e}");
result.dry_run = true;
if result.error.is_none() {
result.error = Some(format!("post failed: {e}"));
}
write_review_log(&result, &config.log_dir);
}
}
}
FinalizeAction::LogOnly => {
result.dry_run = true;
if write_log {
write_review_log(&result, &config.log_dir);
}
}
}
if print_result {
print_review_result(&result);
}
result
}
async fn post_live(
result: &mut ReviewResult,
config: &ReviewConfig,
ctx: &PostContext<'_>,
) -> Result<(), crate::integrations::github::GithubError> {
let client = GithubClient::new();
let strategy = AuthStrategy::select(ctx.run_mode, None);
let token = strategy.resolve_token(&client, config, ctx.owner).await?;
let posted = post_pr_review(&client, ctx.owner, ctx.repo, ctx.pr, &token, result).await?;
if !posted.html_url.is_empty() {
info!(review_url = %posted.html_url, "posted review URL");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decide_action_live_posts() {
assert_eq!(
decide_action(false, TriggerDecision::None, true, true),
FinalizeAction::Post
);
}
#[test]
fn decide_action_force_live_overrides_dry_config() {
assert_eq!(
decide_action(true, TriggerDecision::ForceLive, true, true),
FinalizeAction::Post
);
}
#[test]
fn decide_action_dry_logs() {
assert_eq!(
decide_action(true, TriggerDecision::None, true, true),
FinalizeAction::LogOnly
);
}
#[test]
fn decide_action_force_dry_run_overrides_live_config() {
assert_eq!(
decide_action(false, TriggerDecision::ForceDryRun, true, true),
FinalizeAction::LogOnly
);
}
#[test]
fn decide_action_disallowed_logs() {
assert_eq!(
decide_action(false, TriggerDecision::ForceLive, false, true),
FinalizeAction::LogOnly
);
}
#[test]
fn decide_action_local_logs() {
assert_eq!(
decide_action(false, TriggerDecision::ForceLive, true, false),
FinalizeAction::LogOnly
);
}
}