configure scope: restructure into add/list/delete subcommands#76
Conversation
Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
There was a problem hiding this comment.
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 scopeinto a group command and moved former behavior intoconfigure scope add. - Added new
configure scope list(tabular listing) andconfigure scope delete(interactive + flag-driven deletion). - Added DevLake client
DeleteScopemethod 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 deletebehavior isn’t covered by tests (flag-mode validation, interactive flow, and the deletion request path). Add tests similar to the existingconfigure_connection_delete_test.gocoverage 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, butdocs/configure-scope.mdstill documents the oldgh 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-idis omitted (flag help says so, andresolveConnectionID(...)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-idrequired in flag mode forscope add, and using the same interactive connection picker aslist/deletewhen 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 selectedPluginwith explicitcase "github"/case "gh-copilot". This conflicts with the repo’s plugin-registry pattern where plugin-specific behavior should be data-driven fromconnectionRegistry(to avoid scattering plugin slugs throughout the code). Consider moving scope dispatch into the registry (e.g., a function field onConnectionDefor 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 listis a read-style command, but it ignores the global--jsonflag contract (it always prints banners/emoji/table and can prompt interactively). Either add a JSON output path (similar toconfigure connection list --json) and disallow interactive mode when--jsonis set, or explicitly return an error when--jsonis 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 listbehavior isn’t covered by tests (there are tests for other CRUD-style commands likeconfigure 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
}
| 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@copilot Four fixes needed:
|
Co-authored-by: ewega <26189114+ewega@users.noreply.github.com>
All four fixes applied in 922eaa5:
|
There was a problem hiding this comment.
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 deletebehavior (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 throughrunScopeAdd. 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 namedurlshadows the importednet/urlpackage (even though the RHS still resolves correctly). Renaming the local variable (e.g.,deleteURLorendpoint) would reduce confusion and make theurl.PathEscapeusage 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
flagModeonly 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-labelwithout--plugin, the command will drop into interactive mode instead of enforcing the “--plugin is required when using flags” rule. Consider treating any non---pluginflag as flag mode (e.g., check all relevant flags or usecmd.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 addstill auto-resolves the connection ID from the state file / API when--connection-idis 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-idin flag mode (or always prompting withpickConnectionwhen 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 listbehavior (flag validation, interactive connection selection, table rendering) isn’t covered by tests. There are existing tests for otherconfigure connectionsubcommands; adding a focused unit test forrunScopeListwould 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")
cmd/configure_scope_add.go— moverunConfigureScopes→runScopeAddin newaddsubcommandcmd/configure_scope_list.go—listsubcommand usingclient.ListScopeswith tabwriter table outputcmd/configure_scope_delete.go—deletesubcommand with interactive connection→scope picker + confirmDeleteScope()tointernal/devlake/client.gowithurl.PathEscape(scopeID)to handle Copilot scope IDs containing/cmd/configure_scopes.go— remove RunE/flags, register add/list/delete subcommandscmd/configure_scopes_test.go— renamerunConfigureScopes→runScopeAddin testREADME.md— expanded single scope row into four rows; fixed prose examples toscope adddocs/configure-scope.md— restructured to documentadd,list,deletesubcommands followingconfigure-connection.mdpattern.github/skills/devlake-dev-architecture/SKILL.md— expanded scope entry in command treecmd/configure_scope_add.go—configure project→configure project addgo build ./...), vet (go vet ./...), test (go test ./...) — all passOriginal 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 scopeis currently a single command with one action: add scopes to a connection. There are no subcommands for listing or removing scopes. This is inconsistent withconfigure connection, which haslist,delete,update, andtestsubcommands. Users who have added scopes cannot inspect or remove them without using the DevLake API directly.Additionally, the current
scopecommand 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
Desired command tree
Dependencies
Blocked by:
configure connection add: Extract connection creation intoaddsubcommand #54 (connection add) — sets the extract-to-addpattern this issue followsBlocks:
ScopeFunctoConnectionDef#57 (ScopeFunc) — touches the same scope dispatch code; best done after this restructuring landsLongtext moves toaddsubcommand created hereParallel 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:
--pluginand--connection-idare requiredThe orchestrators (
configureAllPhases,scopeAllConnections) continue to resolve connection IDs internally since they just created the connections.Plugin-specific flags stay on
addThe scope
addcommand retains all current flags. Plugin-specific flags (--reposfor GitHub,--enterprisefor Copilot) are addressed separately in #58.Scope of changes
1. Create
cmd/configure_scope_add.goMove
runConfigureScopes()fromconfigure_scopes.gointo a newaddsubcommand:2. Create
cmd/configure_scope_list.goImplementation:
--plugin+--connection-idpickConnection()(same helper used bydelete/update/testin connection commands)client.ListScopes(plugin, connID)— this API already existsScope ID | Name | Full Name3. Create
cmd/configure_scope_delete.goImplementation:
--plugin+--connection-id+--scope-idclient.DeleteScope(plugin, connID, scopeID)DELETE /plugins/{plugin}/connections/{connId}/scopes/{scopeId}4. Update
configure_scopes.goRunEfrom the parent scope commandadd)add,list,deleteas subcommands5. Add
DeleteScopetointernal/devlake/client.go6. Verify orchestrators are unaffected
scopeAllConnections()inhelpers.gocallsscopeGitHub()andscopeCopilot()directly — it does not invoke the Cobra command. The restructuring should not affect it.Acceptance criteria
gh devlake configure scopeprints help showingadd,...configure scope add/list/delete: Add CRUD subcommands for scope management #55✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.