Skip to content

Make scope dispatch tool-agnostic: Add ScopeFunc to ConnectionDef #57

@ewega

Description

@ewega

Problem

The scope dispatch logic is hard-coded with switch statements on plugin names in two locations:

1. cmd/helpers.goscopeAllConnections() (used by configureAllPhasesinit and configure full):

switch r.Plugin {
case "github":
    _, err := scopeGitHub(client, r.ConnectionID, r.Organization, scopeOpts)
case "gh-copilot":
    _, err := scopeCopilot(client, r.ConnectionID, r.Organization, r.Enterprise)
default:
    fmt.Printf("   ⚠️  Scope configuration for %q is not yet supported\n", r.Plugin)
}

2. cmd/configure_scopes.gorunConfigureScopes() (standalone scope add command):

switch selectedPlugin {
case "github":
    _, err = scopeGitHub(client, connID, org, opts)
case "gh-copilot":
    _, err = scopeCopilot(client, connID, org, enterprise)
default:
    return fmt.Errorf("scope configuration for %q is not yet supported", selectedPlugin)
}

This violates the tool-agnostic design principle. Connection creation is already declarative (driven by ConnectionDef fields) — adding a new plugin only requires a registry entry. But scope creation requires touching switch statements in two places. Adding GitLab (#13) or Azure DevOps (#14) would require editing both files.

Dependencies

Blocked by:

Blocks:

Parallel with: #58 (flag docs) — independent changes, no file overlap

Proposed solution

Add a ScopeFunc field to ConnectionDef in cmd/connection_types.go. Each plugin registers its scope handler as a function pointer. The dispatch code becomes a single generic call with no switch statement.

1. Add ScopeFunc to ConnectionDef

// ScopeHandler is a function that configures scopes for a connection.
// It receives the client, connection ID, org, enterprise, and an options struct.
// It returns the BlueprintConnection entry (for project creation) and an error.
type ScopeHandler func(client *devlake.Client, connID int, org, enterprise string, opts *ScopeOpts) (*devlake.BlueprintConnection, error)

type ConnectionDef struct {
    // ... existing fields ...
    ScopeFunc ScopeHandler  // nil = "scope configuration not yet supported"
}

2. Register scope handlers in connectionRegistry

var connectionRegistry = []*ConnectionDef{
    {
        Plugin:      "github",
        DisplayName: "GitHub",
        ScopeFunc:   scopeGitHubHandler,
        // ... existing fields ...
    },
    {
        Plugin:      "gh-copilot",
        DisplayName: "GitHub Copilot",
        ScopeFunc:   scopeCopilotHandler,
        // ... existing fields ...
    },
    {
        Plugin:      "gitlab",
        DisplayName: "GitLab",
        ScopeFunc:   nil,  // coming soon
        // ...
    },
}

The handler functions wrap the existing scopeGitHub() and scopeCopilot() to match the unified signature.

3. Replace switch statements with generic dispatch

In scopeAllConnections():

for _, r := range results {
    def := FindConnectionDef(r.Plugin)
    if def == nil || def.ScopeFunc == nil {
        fmt.Printf("   ⚠️  Scope configuration for %q is not yet supported\n", r.Plugin)
        continue
    }
    _, err := def.ScopeFunc(client, r.ConnectionID, r.Organization, r.Enterprise, scopeOpts)
    if err != nil {
        fmt.Printf("   ⚠️  %s scope setup failed: %v\n", def.DisplayName, err)
    }
}

In runConfigureScopes() / runScopeAdd():

def := FindConnectionDef(selectedPlugin)
if def == nil || def.ScopeFunc == nil {
    return fmt.Errorf("scope configuration for %q is not yet supported", selectedPlugin)
}
_, err := def.ScopeFunc(client, connID, org, enterprise, opts)

4. Handle plugin-specific interactive prompts

The GitHub scope handler includes interactive DORA pattern prompts (deployment regex, production regex, incident label). These stay inside scopeGitHubHandler — the handler function owns its interactive flow. The unified ScopeHandler signature passes *ScopeOpts which carries the defaults and flag values.

For Copilot, the handler is simpler — it just calls putCopilotScope().

5. Future: adding GitLab

With this pattern, adding GitLab scopes means:

  1. Write scopeGitLabHandler() that knows about GitLab groups/projects
  2. Set ScopeFunc: scopeGitLabHandler on the GitLab ConnectionDef entry
  3. Done — no switch statements to touch

Files to change

File Change
cmd/connection_types.go Add ScopeHandler type and ScopeFunc field to ConnectionDef
cmd/connection_types.go Set ScopeFunc on github and gh-copilot registry entries
cmd/configure_scopes.go Replace switch with def.ScopeFunc() call
cmd/helpers.go Replace switch in scopeAllConnections() with def.ScopeFunc() call
cmd/configure_scopes.go Create scopeGitHubHandler and scopeCopilotHandler wrapper functions

Acceptance criteria

  • No case "github": or case "gh-copilot": in scope dispatch code
  • ConnectionDef has a ScopeFunc field
  • github and gh-copilot entries in connectionRegistry set ScopeFunc
  • scopeAllConnections() in helpers.go uses def.ScopeFunc() — no switch
  • runScopeAdd() (or runConfigureScopes()) uses def.ScopeFunc() — no switch
  • A ConnectionDef with ScopeFunc: nil prints "not yet supported" (graceful fallback)
  • Interactive DORA prompts for GitHub still work (handled inside the handler)
  • init and configure full orchestrators continue to work
  • go build ./... and go test ./... pass

References

Metadata

Metadata

Assignees

Labels

refactorCode restructure, no behavior change

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions