@@ -142,14 +142,15 @@ func resolveConnectionID(client *devlake.Client, state *devlake.State, plugin st
142142 if flagValue > 0 {
143143 return flagValue , nil
144144 }
145+ canonical := plugin
145146 if state != nil {
146147 for _ , c := range state .Connections {
147- if c .Plugin == plugin {
148+ if canonicalPluginSlug ( c .Plugin ) == canonical {
148149 return c .ConnectionID , nil
149150 }
150151 }
151152 }
152- conns , err := client .ListConnections (plugin )
153+ conns , err := client .ListConnections (canonical )
153154 if err != nil {
154155 return 0 , fmt .Errorf ("could not list %s connections: %w" , plugin , err )
155156 }
@@ -472,6 +473,183 @@ func scopeCopilotHandler(client *devlake.Client, connID int, org, enterprise str
472473 return scopeCopilot (client , connID , org , enterprise )
473474}
474475
476+ // scopeAzureDevOpsHandler browses Azure DevOps projects and repositories via the
477+ // remote-scope API and adds the selected repositories as scopes.
478+ func scopeAzureDevOpsHandler (client * devlake.Client , connID int , org , enterprise string , opts * ScopeOpts ) (* devlake.BlueprintConnection , error ) {
479+ fmt .Println ("\n 🔍 Fetching Azure DevOps projects..." )
480+ rootChildren , err := listAzureDevOpsRemoteChildren (client , connID , "" )
481+ if err != nil {
482+ return nil , fmt .Errorf ("listing Azure DevOps projects: %w" , err )
483+ }
484+
485+ var (
486+ projects []devlake.RemoteScopeChild
487+ scopes []devlake.RemoteScopeChild
488+ )
489+ for _ , child := range rootChildren {
490+ switch child .Type {
491+ case "group" :
492+ projects = append (projects , child )
493+ case "scope" :
494+ scopes = append (scopes , child )
495+ }
496+ }
497+
498+ var selectedScopes []devlake.RemoteScopeChild
499+ if len (projects ) > 0 {
500+ projectLabels := make ([]string , 0 , len (projects ))
501+ projectByLabel := make (map [string ]devlake.RemoteScopeChild )
502+ for _ , p := range projects {
503+ label := azureScopeLabel (p )
504+ projectLabels = append (projectLabels , label )
505+ projectByLabel [label ] = p
506+ }
507+
508+ fmt .Println ()
509+ chosenProjects := prompt .SelectMulti ("Select Azure DevOps projects" , projectLabels )
510+ if len (chosenProjects ) == 0 {
511+ return nil , fmt .Errorf ("at least one Azure DevOps project must be selected" )
512+ }
513+
514+ for _ , label := range chosenProjects {
515+ project := projectByLabel [label ]
516+ fmt .Printf ("\n 🔍 Listing repositories in project %q...\n " , label )
517+ children , err := listAzureDevOpsRemoteChildren (client , connID , project .ID )
518+ if err != nil {
519+ return nil , fmt .Errorf ("listing repositories in project %q: %w" , label , err )
520+ }
521+ var repoLabels []string
522+ repoByLabel := make (map [string ]devlake.RemoteScopeChild )
523+ for _ , child := range children {
524+ if child .Type != "scope" {
525+ continue
526+ }
527+ l := azureScopeLabel (child )
528+ repoLabels = append (repoLabels , l )
529+ repoByLabel [l ] = child
530+ }
531+ if len (repoLabels ) == 0 {
532+ fmt .Printf (" ⚠️ No repositories found in project %q\n " , label )
533+ continue
534+ }
535+
536+ fmt .Println ()
537+ chosenRepos := prompt .SelectMulti ("Select repositories to collect" , repoLabels )
538+ for _ , repoLabel := range chosenRepos {
539+ selectedScopes = append (selectedScopes , repoByLabel [repoLabel ])
540+ }
541+ }
542+ } else if len (scopes ) > 0 {
543+ scopeLabels := make ([]string , 0 , len (scopes ))
544+ scopeByLabel := make (map [string ]devlake.RemoteScopeChild )
545+ for _ , s := range scopes {
546+ label := azureScopeLabel (s )
547+ scopeLabels = append (scopeLabels , label )
548+ scopeByLabel [label ] = s
549+ }
550+
551+ fmt .Println ()
552+ chosenScopes := prompt .SelectMulti ("Select Azure DevOps scopes to collect" , scopeLabels )
553+ for _ , label := range chosenScopes {
554+ selectedScopes = append (selectedScopes , scopeByLabel [label ])
555+ }
556+ }
557+
558+ if len (selectedScopes ) == 0 {
559+ return nil , fmt .Errorf ("no Azure DevOps scopes selected" )
560+ }
561+
562+ fmt .Println ("\n 📝 Adding Azure DevOps scopes..." )
563+ var (
564+ data []any
565+ bpScopes []devlake.BlueprintScope
566+ pluginSlug = "azuredevops_go"
567+ )
568+ for _ , child := range selectedScopes {
569+ payload := azureDevOpsScopePayload (child , connID )
570+ data = append (data , payload )
571+
572+ scopeID := child .ID
573+ if idVal , ok := payload ["id" ].(string ); ok && idVal != "" {
574+ scopeID = idVal
575+ }
576+ name := azureScopeLabel (child )
577+ if name == "" {
578+ if n , ok := payload ["name" ].(string ); ok {
579+ name = n
580+ }
581+ }
582+ bpScopes = append (bpScopes , devlake.BlueprintScope {
583+ ScopeID : scopeID ,
584+ ScopeName : name ,
585+ })
586+ }
587+
588+ if err := client .PutScopes (pluginSlug , connID , & devlake.ScopeBatchRequest {Data : data }); err != nil {
589+ return nil , fmt .Errorf ("failed to add Azure DevOps scopes: %w" , err )
590+ }
591+ fmt .Printf (" ✅ Added %d Azure DevOps scope(s)\n " , len (data ))
592+
593+ return & devlake.BlueprintConnection {
594+ PluginName : pluginSlug ,
595+ ConnectionID : connID ,
596+ Scopes : bpScopes ,
597+ }, nil
598+ }
599+
600+ func listAzureDevOpsRemoteChildren (client * devlake.Client , connID int , groupID string ) ([]devlake.RemoteScopeChild , error ) {
601+ var (
602+ children []devlake.RemoteScopeChild
603+ pageToken string
604+ )
605+ for {
606+ resp , err := client .ListRemoteScopes ("azuredevops_go" , connID , groupID , pageToken )
607+ if err != nil {
608+ return nil , err
609+ }
610+ children = append (children , resp .Children ... )
611+ if resp .NextPageToken == "" {
612+ break
613+ }
614+ pageToken = resp .NextPageToken
615+ }
616+ return children , nil
617+ }
618+
619+ func azureDevOpsScopePayload (child devlake.RemoteScopeChild , connID int ) map [string ]any {
620+ var payload map [string ]any
621+ if len (child .Data ) > 0 {
622+ if err := json .Unmarshal (child .Data , & payload ); err != nil {
623+ fmt .Printf ("\n ⚠️ Could not decode Azure DevOps scope data for %s: %v\n " , child .ID , err )
624+ payload = make (map [string ]any )
625+ }
626+ }
627+ if payload == nil {
628+ payload = make (map [string ]any )
629+ }
630+ if _ , ok := payload ["id" ]; ! ok || payload ["id" ] == "" {
631+ payload ["id" ] = child .ID
632+ }
633+ if _ , ok := payload ["name" ]; ! ok || payload ["name" ] == "" {
634+ payload ["name" ] = child .Name
635+ }
636+ if v , ok := payload ["fullName" ]; (! ok || v == "" ) && child .FullName != "" {
637+ payload ["fullName" ] = child .FullName
638+ }
639+ payload ["connectionId" ] = connID
640+ return payload
641+ }
642+
643+ func azureScopeLabel (child devlake.RemoteScopeChild ) string {
644+ if child .FullName != "" {
645+ return child .FullName
646+ }
647+ if child .Name != "" {
648+ return child .Name
649+ }
650+ return child .ID
651+ }
652+
475653// scopeGitLabHandler is the ScopeHandler for the gitlab plugin.
476654// It resolves projects via the DevLake remote-scope API and PUTs the selected
477655// projects as scopes on the connection.
0 commit comments