Skip to content

Commit 06c9480

Browse files
refactor(configure): reduce complexity of run by extracting helpers (#349)
Extract five focused helpers from the monolithic run() function: - resolve_token: CLI flag → interactive prompt - resolve_auth: PAT flag → Azure CLI → interactive prompt - resolve_ado_context: git remote → override with --org/--project flags - resolve_definitions: explicit IDs or auto-detect + match - apply_token_updates: update loop with per-definition reporting run() becomes a thin coordinator that delegates to each helper and handles the two early-exit paths (no pipelines found, dry-run). Cognitive complexity: 27 → below threshold (25) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d6cca21 commit 06c9480

1 file changed

Lines changed: 176 additions & 149 deletions

File tree

src/configure.rs

Lines changed: 176 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -614,105 +614,207 @@ async fn update_pipeline_variable(
614614

615615
// ==================== Command orchestration ====================
616616

617-
/// Run the configure command.
618-
pub async fn run(
619-
token: Option<&str>,
620-
org: Option<&str>,
621-
project: Option<&str>,
622-
pat: Option<&str>,
623-
path: Option<&Path>,
624-
dry_run: bool,
625-
definition_ids: Option<&[u64]>,
626-
) -> Result<()> {
627-
let repo_path = match path {
628-
Some(p) => tokio::fs::canonicalize(p)
629-
.await
630-
.with_context(|| format!("Could not resolve path: {}", p.display()))?,
631-
None => tokio::fs::canonicalize(".")
632-
.await
633-
.context("Could not resolve current directory")?,
634-
};
635-
636-
// Resolve token: CLI flag > env var (handled by clap) > interactive prompt
637-
let token = match token {
638-
Some(t) => t.to_string(),
617+
/// Resolves the GitHub token from the CLI flag or an interactive prompt.
618+
fn resolve_token(token: Option<&str>) -> Result<String> {
619+
match token {
620+
Some(t) => Ok(t.to_string()),
639621
None => inquire::Password::new("Enter the new GITHUB_TOKEN:")
640622
.without_confirmation()
641623
.prompt()
642-
.context("Failed to read token from interactive prompt")?,
643-
};
624+
.context("Failed to read token from interactive prompt"),
625+
}
626+
}
644627

645-
// Resolve auth: CLI flag > env var (handled by clap) > Azure CLI > interactive prompt
646-
let auth = match pat {
628+
/// Resolves ADO authentication: PAT flag > Azure CLI > interactive prompt.
629+
async fn resolve_auth(pat: Option<&str>) -> Result<AdoAuth> {
630+
match pat {
647631
Some(p) => {
648632
info!("Using PAT from --pat flag or AZURE_DEVOPS_EXT_PAT env var");
649-
AdoAuth::Pat(p.to_string())
633+
Ok(AdoAuth::Pat(p.to_string()))
650634
}
651635
None => {
652636
info!("No PAT provided, trying Azure CLI authentication...");
653637
match try_azure_cli_token().await {
654638
Ok(token) => {
655639
println!("Using Azure CLI authentication (az account get-access-token)");
656-
AdoAuth::Bearer(token)
640+
Ok(AdoAuth::Bearer(token))
657641
}
658642
Err(e) => {
659643
warn!("Azure CLI auth failed: {:#}. Falling back to interactive prompt.", e);
660644
let pat = inquire::Password::new("Enter your Azure DevOps PAT:")
661645
.without_confirmation()
662646
.prompt()
663647
.context("Failed to read PAT from interactive prompt. Set AZURE_DEVOPS_EXT_PAT env var, log in with 'az login', or use --pat flag.")?;
664-
AdoAuth::Pat(pat)
648+
Ok(AdoAuth::Pat(pat))
665649
}
666650
}
667651
}
668-
};
652+
}
653+
}
669654

670-
// Resolve ADO context from git remote (best-effort), with CLI overrides.
671-
// If a git remote exists but isn't an ADO URL (e.g. GitHub), fall back to --org/--project.
672-
let ado_ctx = {
673-
let remote_ctx = get_git_remote_url(&repo_path)
674-
.await
675-
.ok()
676-
.and_then(|url| {
677-
info!("Git remote: {}", url);
678-
match parse_ado_remote(&url) {
679-
Ok(ctx) => Some(ctx),
680-
Err(e) => {
681-
debug!("Git remote is not an ADO URL: {:#}", e);
682-
None
683-
}
655+
/// Resolves the ADO context from the git remote (best-effort) with CLI overrides.
656+
/// Falls back to explicit `--org`/`--project` when the remote is absent or non-ADO.
657+
async fn resolve_ado_context(
658+
repo_path: &Path,
659+
org: Option<&str>,
660+
project: Option<&str>,
661+
) -> Result<AdoContext> {
662+
let remote_ctx = get_git_remote_url(repo_path)
663+
.await
664+
.ok()
665+
.and_then(|url| {
666+
info!("Git remote: {}", url);
667+
match parse_ado_remote(&url) {
668+
Ok(ctx) => Some(ctx),
669+
Err(e) => {
670+
debug!("Git remote is not an ADO URL: {:#}", e);
671+
None
684672
}
685-
});
673+
}
674+
});
686675

687-
match (remote_ctx, org, project) {
688-
// Git remote parsed — apply overrides
689-
(Some(mut ctx), org, project) => {
690-
if let Some(org) = org {
691-
ctx.org_url = org.to_string();
692-
}
693-
if let Some(project) = project {
694-
ctx.project = project.to_string();
695-
}
696-
ctx
676+
match (remote_ctx, org, project) {
677+
// Git remote parsed — apply overrides
678+
(Some(mut ctx), org, project) => {
679+
if let Some(org) = org {
680+
ctx.org_url = org.to_string();
697681
}
698-
// No usable remote — require explicit --org and --project
699-
(None, Some(org), Some(project)) => {
700-
info!("No ADO git remote; using --org and --project");
701-
AdoContext {
702-
org_url: org.to_string(),
703-
project: project.to_string(),
704-
repo_name: String::new(),
705-
}
682+
if let Some(project) = project {
683+
ctx.project = project.to_string();
706684
}
707-
(None, _, _) => {
708-
anyhow::bail!(
709-
"Could not determine ADO context: no ADO git remote found and --org/--project not both provided.\n\
710-
When using --definition-ids outside an ADO repo, both --org and --project are required."
711-
);
685+
Ok(ctx)
686+
}
687+
// No usable remote — require explicit --org and --project
688+
(None, Some(org), Some(project)) => {
689+
info!("No ADO git remote; using --org and --project");
690+
Ok(AdoContext {
691+
org_url: org.to_string(),
692+
project: project.to_string(),
693+
repo_name: String::new(),
694+
})
695+
}
696+
(None, _, _) => {
697+
anyhow::bail!(
698+
"Could not determine ADO context: no ADO git remote found and --org/--project not both provided.\n\
699+
When using --definition-ids outside an ADO repo, both --org and --project are required."
700+
);
701+
}
702+
}
703+
}
704+
705+
/// Builds the list of definitions to update from explicit IDs or auto-detection.
706+
/// Returns `None` when auto-detection finds no agentic pipelines (caller should exit cleanly).
707+
async fn resolve_definitions(
708+
client: &reqwest::Client,
709+
ado_ctx: &AdoContext,
710+
auth: &AdoAuth,
711+
definition_ids: Option<&[u64]>,
712+
repo_path: &Path,
713+
) -> Result<Option<Vec<MatchedDefinition>>> {
714+
if let Some(ids) = definition_ids {
715+
println!("Using explicit definition IDs: {:?}", ids);
716+
let mut matched = Vec::new();
717+
for &id in ids {
718+
let name = get_definition_name(client, ado_ctx, auth, id)
719+
.await
720+
.unwrap_or_else(|| format!("definition {}", id));
721+
matched.push(MatchedDefinition {
722+
id,
723+
name,
724+
match_method: MatchMethod::Explicit,
725+
yaml_path: String::new(),
726+
});
727+
}
728+
return Ok(Some(matched));
729+
}
730+
731+
// Auto-detect: scan local repo and match to ADO definitions
732+
println!("Scanning for agentic pipelines...");
733+
let detected = detect::detect_pipelines(repo_path).await?;
734+
735+
if detected.is_empty() {
736+
println!(
737+
"No agentic pipelines found. Make sure your pipelines were compiled with the latest ado-aw."
738+
);
739+
return Ok(None);
740+
}
741+
742+
println!("Found {} agentic pipeline(s):", detected.len());
743+
for p in &detected {
744+
println!(
745+
" {} (source: {}, version: {})",
746+
p.yaml_path.display(),
747+
p.source,
748+
p.version
749+
);
750+
}
751+
println!();
752+
753+
println!("Matching to Azure DevOps pipeline definitions...");
754+
Ok(Some(
755+
match_definitions(client, ado_ctx, auth, &detected).await?,
756+
))
757+
}
758+
759+
/// Updates the `GITHUB_TOKEN` variable on every matched pipeline definition and
760+
/// reports per-definition success/failure.
761+
async fn apply_token_updates(
762+
client: &reqwest::Client,
763+
ado_ctx: &AdoContext,
764+
auth: &AdoAuth,
765+
matched: &[MatchedDefinition],
766+
token: &str,
767+
) -> Result<()> {
768+
println!("Updating GITHUB_TOKEN on matched definitions...");
769+
let mut success_count = 0;
770+
let mut failure_count = 0;
771+
772+
for m in matched {
773+
match update_pipeline_variable(client, ado_ctx, auth, m.id, "GITHUB_TOKEN", token).await {
774+
Ok(()) => {
775+
println!(" \u{2713} Updated '{}' (id={})", m.name, m.id);
776+
success_count += 1;
777+
}
778+
Err(e) => {
779+
eprintln!(" \u{2717} Failed to update '{}' (id={}): {}", m.name, m.id, e);
780+
failure_count += 1;
712781
}
713782
}
783+
}
784+
785+
println!();
786+
println!("Done: {} updated, {} failed.", success_count, failure_count);
787+
788+
if failure_count > 0 {
789+
anyhow::bail!("{} definition(s) failed to update", failure_count);
790+
}
791+
792+
Ok(())
793+
}
794+
795+
/// Run the configure command.
796+
pub async fn run(
797+
token: Option<&str>,
798+
org: Option<&str>,
799+
project: Option<&str>,
800+
pat: Option<&str>,
801+
path: Option<&Path>,
802+
dry_run: bool,
803+
definition_ids: Option<&[u64]>,
804+
) -> Result<()> {
805+
let repo_path = match path {
806+
Some(p) => tokio::fs::canonicalize(p)
807+
.await
808+
.with_context(|| format!("Could not resolve path: {}", p.display()))?,
809+
None => tokio::fs::canonicalize(".")
810+
.await
811+
.context("Could not resolve current directory")?,
714812
};
715813

814+
let token = resolve_token(token)?;
815+
let auth = resolve_auth(pat).await?;
816+
let ado_ctx = resolve_ado_context(&repo_path, org, project).await?;
817+
716818
println!(
717819
"ADO context: org={}, project={}{}",
718820
ado_ctx.org_url,
@@ -730,48 +832,10 @@ pub async fn run(
730832
.build()
731833
.context("Failed to create HTTP client")?;
732834

733-
// Build the list of definitions to update — either from explicit IDs or auto-detection
734-
let matched = if let Some(ids) = definition_ids {
735-
println!("Using explicit definition IDs: {:?}", ids);
736-
737-
let mut matched = Vec::new();
738-
for &id in ids {
739-
let name = get_definition_name(&client, &ado_ctx, &auth, id)
740-
.await
741-
.unwrap_or_else(|| format!("definition {}", id));
742-
matched.push(MatchedDefinition {
743-
id,
744-
name,
745-
match_method: MatchMethod::Explicit,
746-
yaml_path: String::new(),
747-
});
748-
}
749-
matched
750-
} else {
751-
// Auto-detect: scan local repo and match to ADO definitions
752-
println!("Scanning for agentic pipelines...");
753-
let detected = detect::detect_pipelines(&repo_path).await?;
754-
755-
if detected.is_empty() {
756-
println!(
757-
"No agentic pipelines found. Make sure your pipelines were compiled with the latest ado-aw."
758-
);
759-
return Ok(());
760-
}
761-
762-
println!("Found {} agentic pipeline(s):", detected.len());
763-
for p in &detected {
764-
println!(
765-
" {} (source: {}, version: {})",
766-
p.yaml_path.display(),
767-
p.source,
768-
p.version
769-
);
770-
}
771-
println!();
772-
773-
println!("Matching to Azure DevOps pipeline definitions...");
774-
match_definitions(&client, &ado_ctx, &auth, &detected).await?
835+
let Some(matched) =
836+
resolve_definitions(&client, &ado_ctx, &auth, definition_ids, &repo_path).await?
837+
else {
838+
return Ok(());
775839
};
776840

777841
if matched.is_empty() {
@@ -783,10 +847,7 @@ pub async fn run(
783847
println!("{} definition(s) to update:", matched.len());
784848
for m in &matched {
785849
if m.yaml_path.is_empty() {
786-
println!(
787-
" [{}] '{}' (id={})",
788-
m.match_method, m.name, m.id
789-
);
850+
println!(" [{}] '{}' (id={})", m.match_method, m.name, m.id);
790851
} else {
791852
println!(
792853
" [{}] '{}' (id={}) \u{2190} {}",
@@ -796,7 +857,6 @@ pub async fn run(
796857
}
797858
println!();
798859

799-
// Step 4: Update GITHUB_TOKEN
800860
if dry_run {
801861
println!("Dry run \u{2014} no changes applied.");
802862
println!(
@@ -806,40 +866,7 @@ pub async fn run(
806866
return Ok(());
807867
}
808868

809-
println!("Updating GITHUB_TOKEN on matched definitions...");
810-
let mut success_count = 0;
811-
let mut failure_count = 0;
812-
813-
for m in &matched {
814-
match update_pipeline_variable(
815-
&client,
816-
&ado_ctx,
817-
&auth,
818-
m.id,
819-
"GITHUB_TOKEN",
820-
&token,
821-
)
822-
.await
823-
{
824-
Ok(()) => {
825-
println!(" \u{2713} Updated '{}' (id={})", m.name, m.id);
826-
success_count += 1;
827-
}
828-
Err(e) => {
829-
eprintln!(" \u{2717} Failed to update '{}' (id={}): {}", m.name, m.id, e);
830-
failure_count += 1;
831-
}
832-
}
833-
}
834-
835-
println!();
836-
println!("Done: {} updated, {} failed.", success_count, failure_count);
837-
838-
if failure_count > 0 {
839-
anyhow::bail!("{} definition(s) failed to update", failure_count);
840-
}
841-
842-
Ok(())
869+
apply_token_updates(&client, &ado_ctx, &auth, &matched, &token).await
843870
}
844871

845872
#[cfg(test)]

0 commit comments

Comments
 (0)