Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,29 @@ After creating connections, run `configure scope` to create a project and start

---

### `gh devlake configure connection list`

List all existing plugin connections. Useful for scripting, debugging, and answering "what do I have?".

```bash
gh devlake configure connection list
gh devlake configure connection list --plugin gh-copilot
```

| Flag | Default | Description |
|------|---------|-------------|
| `--plugin` | *(all plugins)* | Filter by plugin (`github`, `gh-copilot`) |

**Output:**
```
Plugin ID Name Organization Enterprise
────────── ── ────────────────────────── ──────────── ──────────
github 1 GitHub - my-org my-org
gh-copilot 2 GitHub Copilot - my-org my-org avocado-corp
```

---

### `gh devlake configure scope`

Add repository scopes, create a DORA project with a blueprint, and trigger the first data sync.
Expand Down
118 changes: 118 additions & 0 deletions cmd/configure_connection_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cmd

import (
"fmt"
"strings"
"text/tabwriter"

"github.com/DevExpGBB/gh-devlake/internal/devlake"
"github.com/spf13/cobra"
)

var connListPlugin string

var listConnectionsCmd = &cobra.Command{
Use: "list",
Short: "List all plugin connections in DevLake",
Long: `Lists all DevLake plugin connections, optionally filtered by plugin.

Examples:
gh devlake configure connection list
gh devlake configure connection list --plugin gh-copilot`,
RunE: runListConnections,
}

func init() {
listConnectionsCmd.Flags().StringVar(&connListPlugin, "plugin", "", "Filter by plugin (github, gh-copilot)")
configureConnectionsCmd.AddCommand(listConnectionsCmd)
}

func runListConnections(cmd *cobra.Command, args []string) error {
fmt.Println()
fmt.Println("════════════════════════════════════════")
fmt.Println(" DevLake — List Connections")
fmt.Println("════════════════════════════════════════")

// ── Validate --plugin flag ──
if connListPlugin != "" {
def := FindConnectionDef(connListPlugin)
if def == nil || !def.Available {
slugs := availablePluginSlugs()
return fmt.Errorf("unknown plugin %q — choose: %s", connListPlugin, strings.Join(slugs, ", "))
}
}

// ── Discover DevLake ──
fmt.Println("\n🔍 Discovering DevLake instance...")
disc, err := devlake.Discover(cfgURL)
if err != nil {
return err
}
fmt.Printf(" Found DevLake at %s (via %s)\n", disc.URL, disc.Source)

client := devlake.NewClient(disc.URL)

// ── Determine which plugins to query ──
var defs []*ConnectionDef
if connListPlugin != "" {
defs = []*ConnectionDef{FindConnectionDef(connListPlugin)}
} else {
defs = AvailableConnections()
}

// ── Collect connections from all relevant plugins ──
type row struct {
plugin string
id int
name string
organization string
enterprise string
}
var rows []row

for _, def := range defs {
conns, err := client.ListConnections(def.Plugin)
if err != nil {
fmt.Printf("\n⚠️ Could not list %s connections: %v\n", def.DisplayName, err)
continue
}
for _, c := range conns {
rows = append(rows, row{
plugin: def.Plugin,
id: c.ID,
name: c.Name,
organization: c.Organization,
enterprise: c.Enterprise,
})
}
}

// ── Render table ──
fmt.Println()
if len(rows) == 0 {
fmt.Println(" No connections found.")
fmt.Println()
return nil
}

w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "Plugin\tID\tName\tOrganization\tEnterprise")
fmt.Fprintln(w, strings.Repeat("─", 10)+"\t"+strings.Repeat("─", 4)+"\t"+strings.Repeat("─", 30)+"\t"+strings.Repeat("─", 14)+"\t"+strings.Repeat("─", 12))
for _, r := range rows {
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", r.plugin, r.id, r.name, r.organization, r.enterprise)
}
w.Flush()
fmt.Println()

return nil
}

// availablePluginSlugs returns the Plugin slugs of all available connections.
func availablePluginSlugs() []string {
defs := AvailableConnections()
slugs := make([]string, len(defs))
for i, d := range defs {
slugs[i] = d.Plugin
}
return slugs
}
59 changes: 59 additions & 0 deletions cmd/configure_connection_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"strings"
"testing"

"github.com/spf13/cobra"
)

func TestRunListConnections_UnknownPlugin(t *testing.T) {
origPlugin := connListPlugin
t.Cleanup(func() { connListPlugin = origPlugin })

connListPlugin = "gitlab"
cmd := &cobra.Command{RunE: runListConnections}
cmd.Flags().StringVar(&connListPlugin, "plugin", "", "")
_ = cmd.Flags().Set("plugin", "gitlab")

err := runListConnections(cmd, nil)
if err == nil {
t.Fatal("expected error for unknown plugin, got nil")
}
if !strings.Contains(err.Error(), "unknown plugin") {
t.Errorf("unexpected error message: %v", err)
}
}

func TestAvailablePluginSlugs(t *testing.T) {
slugs := availablePluginSlugs()
if len(slugs) == 0 {
t.Fatal("expected at least one available plugin slug")
}
// All returned slugs should correspond to available ConnectionDefs.
for _, s := range slugs {
def := FindConnectionDef(s)
if def == nil {
t.Errorf("slug %q has no ConnectionDef", s)
continue
}
if !def.Available {
t.Errorf("slug %q is marked unavailable but returned by availablePluginSlugs", s)
}
}
}

func TestListConnectionsCmd_Registered(t *testing.T) {
// Verify the list subcommand is registered under configureConnectionsCmd.
found := false
for _, sub := range configureConnectionsCmd.Commands() {
if sub.Use == "list" {
found = true
break
}
}
if !found {
t.Error("'list' subcommand not registered under configureConnectionsCmd")
}
}