@@ -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