Skip to content

configure scope: restructure into add/list/delete subcommands#76

Merged
ewega merged 3 commits into
mainfrom
copilot/add-crud-subcommands-for-scope-management
Mar 2, 2026
Merged

configure scope: restructure into add/list/delete subcommands#76
ewega merged 3 commits into
mainfrom
copilot/add-crud-subcommands-for-scope-management

Conversation

Copilot AI commented Mar 2, 2026

Copy link
Copy Markdown
Contributor
  • Plan established
  • Create cmd/configure_scope_add.go — move runConfigureScopesrunScopeAdd in new add subcommand
  • Create cmd/configure_scope_list.golist subcommand using client.ListScopes with tabwriter table output
  • Create cmd/configure_scope_delete.godelete subcommand with interactive connection→scope picker + confirm
  • Add DeleteScope() to internal/devlake/client.go with url.PathEscape(scopeID) to handle Copilot scope IDs containing /
  • Update cmd/configure_scopes.go — remove RunE/flags, register add/list/delete subcommands
  • Update cmd/configure_scopes_test.go — rename runConfigureScopesrunScopeAdd in test
  • Update README.md — expanded single scope row into four rows; fixed prose examples to scope add
  • Update docs/configure-scope.md — restructured to document add, list, delete subcommands following configure-connection.md pattern
  • Update .github/skills/devlake-dev-architecture/SKILL.md — expanded scope entry in command tree
  • Fix cross-references in cmd/configure_scope_add.goconfigure projectconfigure project add
  • Build (go build ./...), vet (go vet ./...), test (go test ./...) — all pass
Original prompt

This section details on the original issue you should resolve

<issue_title>configure scope add/list/delete: Add CRUD subcommands for scope management</issue_title>
<issue_description>## Problem

configure scope is currently a single command with one action: add scopes to a connection. There are no subcommands for listing or removing scopes. This is inconsistent with configure connection, which has list, delete, update, and test subcommands. Users who have added scopes cannot inspect or remove them without using the DevLake API directly.

Additionally, the current scope command auto-detects connection IDs from state files, which can lead to ambiguity when multiple connections exist for the same plugin. Connection ID should be an explicit, mandatory input for standalone scope commands.

Current command tree

configure scope              ← RunE = add scopes (only action)

Desired command tree

configure scope              ← no RunE, prints help
  ├── add                    ← current scope logic (interactive or flags)
  ├── list                   ← list scopes on a connection
  └── delete                 ← remove a scope from a connection

Dependencies

Blocked by:

Blocks:

Parallel with: #56 (project CRUD) — independent files, can be done in parallel

Design decisions

Connection ID is mandatory

A scope is always tied to a specific plugin connection. For standalone scope subcommands:

  • Flag mode: --plugin and --connection-id are required
  • Interactive mode: show a connection picker (list all connections across plugins, let user choose)

The orchestrators (configureAllPhases, scopeAllConnections) continue to resolve connection IDs internally since they just created the connections.

Plugin-specific flags stay on add

The scope add command retains all current flags. Plugin-specific flags (--repos for GitHub, --enterprise for Copilot) are addressed separately in #58.

Scope of changes

1. Create cmd/configure_scope_add.go

Move runConfigureScopes() from configure_scopes.go into a new add subcommand:

func newScopeAddCmd() *cobra.Command {
    var opts ScopeOpts
    cmd := &cobra.Command{
        Use:   "add",
        Short: "Add scopes (repos, orgs) to an existing connection",
        RunE: func(cmd *cobra.Command, args []string) error {
            return runScopeAdd(cmd, args, &opts)
        },
    }
    // Move all current scope flags here
    cmd.Flags().StringVar(&opts.Plugin, "plugin", "", ...)
    cmd.Flags().IntVar(&opts.ConnectionID, "connection-id", 0, ...)
    // ...
    return cmd
}

2. Create cmd/configure_scope_list.go

// gh devlake configure scope list --plugin github --connection-id 1
// gh devlake configure scope list   (interactive: pick a connection)

Implementation:

  • In flag mode, require --plugin + --connection-id
  • In interactive mode, use pickConnection() (same helper used by delete/update/test in connection commands)
  • Call existing client.ListScopes(plugin, connID) — this API already exists
  • Render results as a table: Scope ID | Name | Full Name

3. Create cmd/configure_scope_delete.go

// gh devlake configure scope delete --plugin github --connection-id 1 --scope-id 12345
// gh devlake configure scope delete   (interactive: pick connection, then pick scope)

Implementation:

  • In flag mode, require --plugin + --connection-id + --scope-id
  • In interactive mode: pick connection → list scopes → pick scope to delete
  • Add new client method: client.DeleteScope(plugin, connID, scopeID)
  • DevLake API: DELETE /plugins/{plugin}/connections/{connId}/scopes/{scopeId}
  • Confirm before deletion (warn about blueprint impact)

4. Update configure_scopes.go

  • Remove RunE from the parent scope command
  • Remove flag registrations (they move to add)
  • Register add, list, delete as subcommands

5. Add DeleteScope to internal/devlake/client.go

func (c *Client) DeleteScope(plugin string, connID int, scopeID string) error {
    url := fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s", c.BaseURL, plugin, connID, scopeID)
    req, _ := http.NewRequest(http.MethodDelete, url, nil)
    resp, err := c.HTTPClient.Do(req)
    // ... error handling ...
}

6. Verify orchestrators are unaffected

scopeAllConnections() in helpers.go calls scopeGitHub() and scopeCopilot() directly — it does not invoke the Cobra command. The restructuring should not affect it.

Acceptance criteria

  • gh devlake configure scope prints help showing add,...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
Copilot AI changed the title [WIP] Add CRUD subcommands for scope management configure scope: restructure into add/list/delete subcommands Mar 2, 2026
Copilot finished work on behalf of ewega March 2, 2026 11:28
@ewega ewega requested a review from Copilot March 2, 2026 11:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restructures gh devlake configure scope to match the connection command’s CRUD-style UX by turning it into a group command with add, list, and delete subcommands, plus supporting client/test/readme updates.

Changes:

  • Converted configure scope into a group command and moved former behavior into configure scope add.
  • Added new configure scope list (tabular listing) and configure scope delete (interactive + flag-driven deletion).
  • Added DevLake client DeleteScope method and updated tests/README references.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/devlake/client.go Adds Client.DeleteScope for deleting a scope on a connection.
cmd/configure_scopes.go Converts configure scope into a group command and registers new subcommands.
cmd/configure_scope_add.go New add subcommand containing prior scoping logic.
cmd/configure_scope_list.go New list subcommand that lists scopes for a connection via a table.
cmd/configure_scope_delete.go New delete subcommand with interactive scope selection + confirmation.
cmd/configure_scopes_test.go Updates tests to call runScopeAdd instead of the removed runConfigureScopes.
README.md Updates command reference to include scope subcommands.
Comments suppressed due to low confidence (6)

cmd/configure_scope_delete.go:53

  • New configure scope delete behavior isn’t covered by tests (flag-mode validation, interactive flow, and the deletion request path). Add tests similar to the existing configure_connection_delete_test.go coverage so the command’s safety/confirmation logic and required-flag enforcement don’t regress.
func newScopeDeleteCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "delete",
		Short: "Remove a scope from a connection",
		Long: `Removes a scope from an existing DevLake plugin connection.

If --plugin, --connection-id, and --scope-id are not specified, prompts interactively.

Examples:
  gh devlake configure scope delete
  gh devlake configure scope delete --plugin github --connection-id 1 --scope-id 12345678`,
		RunE: runScopeDelete,
	}

	cmd.Flags().StringVar(&scopeDeletePlugin, "plugin", "", fmt.Sprintf("Plugin of the connection (%s)", strings.Join(availablePluginSlugs(), ", ")))
	cmd.Flags().IntVar(&scopeDeleteConnID, "connection-id", 0, "Connection ID")
	cmd.Flags().StringVar(&scopeDeleteScopeID, "scope-id", "", "Scope ID to delete")

	return cmd
}

func runScopeDelete(cmd *cobra.Command, args []string) error {
	printBanner("DevLake \u2014 Delete Scope")

	pluginFlagSet := cmd.Flags().Changed("plugin")
	connIDFlagSet := cmd.Flags().Changed("connection-id")
	scopeIDFlagSet := cmd.Flags().Changed("scope-id")

	// If any flag is set, all three must be provided
	if pluginFlagSet || connIDFlagSet || scopeIDFlagSet {
		if !pluginFlagSet || !connIDFlagSet || !scopeIDFlagSet ||
			scopeDeletePlugin == "" || scopeDeleteConnID == 0 || scopeDeleteScopeID == "" {
			return fmt.Errorf("--plugin, --connection-id, and --scope-id must all be provided together")
		}
	}

README.md:213

  • The command reference now lists configure scope add/list/delete, but docs/configure-scope.md still documents the old gh devlake configure scope [flags] interface (flags on the parent command, no subcommands). Update the linked doc to match the new subcommand structure, otherwise users will follow stale instructions.
| `gh devlake configure scope` | Manage scopes on connections (subcommands below) | [configure-scope.md](docs/configure-scope.md) |
| `gh devlake configure scope add` | Add repo/org scopes to a connection | [configure-scope.md](docs/configure-scope.md) |
| `gh devlake configure scope list` | List scopes on a connection | [configure-scope.md](docs/configure-scope.md) |
| `gh devlake configure scope delete` | Remove a scope from a connection | [configure-scope.md](docs/configure-scope.md) |

cmd/configure_scope_add.go:93

  • This command still auto-detects the connection ID when --connection-id is omitted (flag help says so, and resolveConnectionID(...) is called). That conflicts with the PR/issue requirement that standalone scope subcommands require an explicit connection ID to avoid ambiguity when multiple connections exist. Consider making --connection-id required in flag mode for scope add, and using the same interactive connection picker as list/delete when not provided.
	cmd.Flags().IntVar(&opts.ConnectionID, "connection-id", 0, "Connection ID (auto-detected if omitted)")
	cmd.Flags().StringVar(&opts.DeployPattern, "deployment-pattern", "(?i)deploy", "Regex to match deployment workflows")
	cmd.Flags().StringVar(&opts.ProdPattern, "production-pattern", "(?i)prod", "Regex to match production environment")
	cmd.Flags().StringVar(&opts.IncidentLabel, "incident-label", "incident", "Issue label for incidents")

	return cmd
}

func runScopeAdd(cmd *cobra.Command, args []string, opts *ScopeOpts) error {
	printBanner("DevLake \u2014 Configure Scopes")

	// Determine which plugin to scope
	var selectedPlugin string
	if opts.Plugin != "" {
		if _, err := requirePlugin(opts.Plugin); err != nil {
			return err
		}
		selectedPlugin = opts.Plugin
	} else {
		flagMode := cmd.Flags().Changed("org") ||
			cmd.Flags().Changed("repos") ||
			cmd.Flags().Changed("repos-file") ||
			cmd.Flags().Changed("connection-id")
		if flagMode {
			slugs := availablePluginSlugs()
			return fmt.Errorf("--plugin is required when using flags (choose: %s)", strings.Join(slugs, ", "))
		}
		available := AvailableConnections()
		var labels []string
		for _, d := range available {
			labels = append(labels, d.DisplayName)
		}
		fmt.Println()
		chosen := prompt.Select("Which plugin to configure?", labels)
		if chosen == "" {
			return fmt.Errorf("plugin selection is required")
		}
		for _, d := range available {
			if d.DisplayName == chosen {
				selectedPlugin = d.Plugin
				break
			}
		}
		if selectedPlugin == "" {
			return fmt.Errorf("plugin selection is required")
		}
	}

	client, disc, err := discoverClient(cfgURL)
	if err != nil {
		return err
	}
	_, state := devlake.FindStateFile(disc.URL, disc.GrafanaURL)

	fmt.Println("\n\U0001f517 Resolving connection...")
	connID, err := resolveConnectionID(client, state, selectedPlugin, opts.ConnectionID)
	if err != nil {

cmd/configure_scope_add.go:117

  • Scope dispatch is hardcoded via switch selectedPlugin with explicit case "github" / case "gh-copilot". This conflicts with the repo’s plugin-registry pattern where plugin-specific behavior should be data-driven from connectionRegistry (to avoid scattering plugin slugs throughout the code). Consider moving scope dispatch into the registry (e.g., a function field on ConnectionDef or a map keyed by plugin) and calling it here.
	// Dispatch to plugin-specific scope handler
	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)
	}

cmd/configure_scope_list.go:70

  • scope list is a read-style command, but it ignores the global --json flag contract (it always prints banners/emoji/table and can prompt interactively). Either add a JSON output path (similar to configure connection list --json) and disallow interactive mode when --json is set, or explicitly return an error when --json is used so scripts don’t get non-JSON output.

This issue also appears on line 17 of the same file.

func runScopeList(cmd *cobra.Command, args []string) error {
	printBanner("DevLake \u2014 List Scopes")

	pluginFlagSet := cmd.Flags().Changed("plugin")
	connIDFlagSet := cmd.Flags().Changed("connection-id")

	// If any flag is set, both must be provided
	if pluginFlagSet || connIDFlagSet {
		if !pluginFlagSet || !connIDFlagSet || scopeListPlugin == "" || scopeListConnID == 0 {
			return fmt.Errorf("both --plugin and --connection-id must be provided together")
		}
	}

	if scopeListPlugin != "" {
		if _, err := requirePlugin(scopeListPlugin); err != nil {
			return err
		}
	}

	client, _, err := discoverClient(cfgURL)
	if err != nil {
		return err
	}

	selectedPlugin := scopeListPlugin
	selectedConnID := scopeListConnID

	if !(pluginFlagSet && connIDFlagSet) {
		picked, err := pickConnection(client, "Select a connection to list scopes")
		if err != nil {
			if err.Error() == "no connections found \u2014 create one with 'gh devlake configure connection add'" {
				fmt.Println("\n  No connections found.")
				return nil
			}

cmd/configure_scope_list.go:35

  • New configure scope list behavior isn’t covered by tests (there are tests for other CRUD-style commands like configure connection list/delete). Add unit tests for flag-mode validation and (at least) the JSON/human output behavior so regressions are caught when command UX/output changes.
func newScopeListCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "list",
		Short: "List scopes on a connection",
		Long: `Lists all scopes configured on a DevLake plugin connection.

If --plugin and --connection-id are not specified, prompts interactively.

Examples:
  gh devlake configure scope list
  gh devlake configure scope list --plugin github --connection-id 1`,
		RunE: runScopeList,
	}

	cmd.Flags().StringVar(&scopeListPlugin, "plugin", "", fmt.Sprintf("Plugin to query (%s)", strings.Join(availablePluginSlugs(), ", ")))
	cmd.Flags().IntVar(&scopeListConnID, "connection-id", 0, "Connection ID")

	return cmd
}

Comment thread internal/devlake/client.go Outdated
Comment on lines +358 to +360
url := fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s", c.BaseURL, plugin, connID, scopeID)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scopeID can contain / (e.g., Copilot scope IDs like enterprise/org). Building the DELETE URL with the raw scopeID will turn it into multiple path segments and likely 404. URL-escape the scope ID (e.g., url.PathEscape) when constructing the request path/URL so all plugins’ scope IDs are deletable.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 922eaa5. Added net/url import to client.go and changed the URL construction to use url.PathEscape(scopeID), so Copilot scope IDs like enterprise/org are correctly encoded as enterprise%2Forg instead of being split into extra path segments.

@ewega

ewega commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

@copilot Four fixes needed:

  1. Fix README.md inline examples: In the "Step 3: Add Scopes" section and the "Day-2 Operations" section, update gh devlake configure scope --plugin ... to gh devlake configure scope add --plugin .... The command table is correct, but the prose examples still use the old syntax.

  2. Update docs/configure-scope.md: Restructure it to document the three new subcommands (add, list, delete) instead of treating configure scope as a single command. Follow the same pattern as docs/configure-connection.md which already documents CRUD subcommands.

  3. Update .github/skills/devlake-dev-architecture/SKILL.md: In the command tree, change:

    │   ├── scope                     # Add scopes to existing connections
    

    to:

    │   ├── scope                     # Manage scopes on connections
    │   │   ├── add                   # Add repo/org scopes to a connection
    │   │   ├── list                  # List scopes on a connection
    │   │   └── delete               # Remove a scope from a connection
    
  4. Fix stale cross-reference in cmd/configure_scope_add.go: Two places reference gh devlake configure project — these should be gh devlake configure project add (since the project command is also being restructured into CRUD subcommands in a parallel PR). Update both the Long description and the "Next step" success message.

Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>

Copilot AI commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Four fixes needed:

  1. Fix README.md inline examples: In the "Step 3: Add Scopes" section and the "Day-2 Operations" section, update gh devlake configure scope --plugin ... to `gh devla...

All four fixes applied in 922eaa5:

  1. README.md prose examples — updated configure scope --plugin ...configure scope add --plugin ... in both Step 3 and Day-2 Operations sections.
  2. docs/configure-scope.md — fully restructured following the configure-connection.md pattern, with separate sections for add, list, and delete subcommands including flags, examples, and output.
  3. .github/skills/devlake-dev-architecture/SKILL.md — expanded the scope entry in the command tree to show add, list, delete subcommands.
  4. cmd/configure_scope_add.go — updated both the Long description and the "Next step" success message to reference gh devlake configure project add.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

cmd/configure_scope_delete.go:46

  • New scope delete behavior (flag validation, interactive connection→scope selection, confirmation flow, DeleteScope call) isn’t covered by tests. Adding unit tests around the flag-mode validation and the “no scopes found” path would align with the existing test coverage for other CRUD-style configure subcommands.
func runScopeDelete(cmd *cobra.Command, args []string) error {
	printBanner("DevLake \u2014 Delete Scope")

	pluginFlagSet := cmd.Flags().Changed("plugin")
	connIDFlagSet := cmd.Flags().Changed("connection-id")
	scopeIDFlagSet := cmd.Flags().Changed("scope-id")

cmd/configure_scopes_test.go:41

  • This test name still references runConfigureScopes, but the behavior under test now goes through runScopeAdd. Renaming the test (and any subtest labels if needed) would make failures easier to interpret and keep terminology aligned with the new subcommand structure.
func TestRunConfigureScopes_PluginFlag(t *testing.T) {
	makeCmd := func() (*cobra.Command, *ScopeOpts) {
		opts := &ScopeOpts{}
		cmd := &cobra.Command{RunE: func(cmd *cobra.Command, args []string) error {
			return runScopeAdd(cmd, args, opts)
		}}

internal/devlake/client.go:360

  • In DeleteScope, the local variable named url shadows the imported net/url package (even though the RHS still resolves correctly). Renaming the local variable (e.g., deleteURL or endpoint) would reduce confusion and make the url.PathEscape usage clearer.
func (c *Client) DeleteScope(plugin string, connID int, scopeID string) error {
	url := fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s", c.BaseURL, plugin, connID, url.PathEscape(scopeID))
	req, err := http.NewRequest(http.MethodDelete, url, nil)

cmd/configure_scope_add.go:59

  • flagMode only checks a subset of flags (org/repos/repos-file/connection-id). If a user sets other flags like --enterprise, --deployment-pattern, --production-pattern, or --incident-label without --plugin, the command will drop into interactive mode instead of enforcing the “--plugin is required when using flags” rule. Consider treating any non---plugin flag as flag mode (e.g., check all relevant flags or use cmd.Flags().NFlag() with an allowlist) so validation is consistent.

This issue also appears on line 91 of the same file.

		flagMode := cmd.Flags().Changed("org") ||
			cmd.Flags().Changed("repos") ||
			cmd.Flags().Changed("repos-file") ||
			cmd.Flags().Changed("connection-id")

cmd/configure_scope_add.go:95

  • scope add still auto-resolves the connection ID from the state file / API when --connection-id is omitted, even when running in flag mode (e.g., --plugin + --org). This conflicts with the stated goal of making standalone scope commands require an explicit connection target to avoid ambiguity when multiple connections exist. Consider requiring --connection-id in flag mode (or always prompting with pickConnection when it’s not provided) to ensure the user is explicitly selecting the connection.
	fmt.Println("\n\U0001f517 Resolving connection...")
	connID, err := resolveConnectionID(client, state, selectedPlugin, opts.ConnectionID)
	if err != nil {
		return fmt.Errorf("no %s connection found \u2014 run 'configure connection' first: %w", pluginDisplayName(selectedPlugin), err)
	}

cmd/configure_scope_list.go:41

  • New scope list behavior (flag validation, interactive connection selection, table rendering) isn’t covered by tests. There are existing tests for other configure connection subcommands; adding a focused unit test for runScopeList would help prevent regressions (e.g., flag-mode validation and the empty-scope output path).
func runScopeList(cmd *cobra.Command, args []string) error {
	printBanner("DevLake \u2014 List Scopes")

	pluginFlagSet := cmd.Flags().Changed("plugin")
	connIDFlagSet := cmd.Flags().Changed("connection-id")

@ewega ewega marked this pull request as ready for review March 2, 2026 12:45
@ewega ewega merged commit 5c132b3 into main Mar 2, 2026
4 checks passed
ewega added a commit that referenced this pull request Mar 2, 2026
@ewega ewega deleted the copilot/add-crud-subcommands-for-scope-management branch March 2, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

configure scope add/list/delete: Add CRUD subcommands for scope management

3 participants