From 5c2114bf61fbec355e22dae539097711dc5f8622 Mon Sep 17 00:00:00 2001 From: ewega Date: Thu, 19 Feb 2026 13:23:42 +0300 Subject: [PATCH] feat: enterprise slug support, connection testing, roadmap skill - Fix Copilot scope ID computation (enterprise/org, enterprise, org) - Add Enterprise field to CopilotScope and Connection structs - Enable connection testing for gh-copilot (SupportsTest: true) - Add org/enterprise to ConnectionTestRequest - Align Copilot rate limit to 5000 (plugin default) - Thread --enterprise flag through scopes, projects, full commands - Add resolveEnterprise() helper (flag -> state -> skip) - Add copilotScopeID() with 7 unit test cases - Add BuildCreateRequest/BuildTestRequest tests (8 cases) - Add AGENTS.md plugin system + scope ID docs - Add gh-devlake-roadmap skill (.github/skills/) Closes #1, closes #2, closes #3, closes #4 --- .github/skills/gh-devlake-roadmap/SKILL.md | 82 ++++++++++++++ AGENTS.md | 11 ++ cmd/configure_full.go | 6 + cmd/configure_projects.go | 44 +++++--- cmd/configure_scopes.go | 64 +++++++++-- cmd/configure_scopes_test.go | 59 ++++++++++ cmd/connection_types.go | 19 +++- cmd/connection_types_test.go | 124 +++++++++++++++++++++ internal/devlake/client.go | 8 +- internal/devlake/types.go | 3 +- 10 files changed, 389 insertions(+), 31 deletions(-) create mode 100644 .github/skills/gh-devlake-roadmap/SKILL.md create mode 100644 cmd/configure_scopes_test.go create mode 100644 cmd/connection_types_test.go diff --git a/.github/skills/gh-devlake-roadmap/SKILL.md b/.github/skills/gh-devlake-roadmap/SKILL.md new file mode 100644 index 0000000..98c4fa7 --- /dev/null +++ b/.github/skills/gh-devlake-roadmap/SKILL.md @@ -0,0 +1,82 @@ +--- +name: gh-devlake-roadmap +description: Look up the gh-devlake CLI roadmap, milestones, issues, version plans, and release priorities. Use when the user asks about roadmap, priorities, what's planned, what version something is in, or what issues exist for a milestone. +--- + +# gh-devlake Roadmap Lookup + +## Repository + +- **Owner:** DevExpGBB +- **Repo:** gh-devlake +- **Project Board:** https://github.com/orgs/DevExpGbb/projects/21 (Project #21) +- **Milestones URL:** https://github.com/DevExpGBB/gh-devlake/milestones + +## How to Look Up Roadmap Information + +Use the GitHub MCP tools to query issues and milestones from `DevExpGBB/gh-devlake`. + +### List milestones and their issues + +Use `mcp_github_list_issues` with the repo `DevExpGBB/gh-devlake` to get issues. +Filter by milestone name to see what's in each release. + +To find all milestones, use the `gh` CLI: +``` +gh api repos/DevExpGBB/gh-devlake/milestones --jq '.[] | "\(.title): \(.description) (\(.open_issues) open, \(.closed_issues) closed)"' +``` + +To find issues for a specific milestone: +``` +gh issue list --repo DevExpGBB/gh-devlake --milestone "v0.3.4" --json number,title,state,labels +``` + +### Issue labels + +| Label | Meaning | +|-------|---------| +| `enhancement` | New feature or request | +| `bug` | Something isn't working | +| `refactor` | Code restructure, no behavior change | +| `documentation` | Docs, skills, instructions | + +## Versioning Scheme + +Semantic versioning: `MAJOR.MINOR.PATCH` + +- **0.3.x** — Current development line. Incremental features, restructuring, and lifecycle commands. + - PATCH bumps for features that don't change the CLI's plugin surface area (same set of supported DevOps tools). +- **0.4.x** — Multi-tool expansion. New plugin types (GitLab, Azure DevOps) that expand the CLI's supported tool surface. +- **MINOR bumps** only when genuinely new categories of capability arrive (new DevOps tool plugins, new token resolution chains). +- **MAJOR bump (1.0)** — Reserved for production-ready stability declaration. + +## Current Release Plan + +| Version | Theme | Status | +|---------|-------|--------| +| v0.3.3 | Enterprise Support | In progress — scope ID fix, connection testing, rate limit, enterprise threading | +| v0.3.4 | CLI Restructure | Planned — singular commands, --plugin flag, list command, CLI versioning | +| v0.3.5 | Connection Lifecycle | Planned — delete and test commands | +| v0.3.6 | Connection Update + Skill | Planned — update command, this roadmap skill | +| v0.4.0 | Multi-Tool Expansion | Future — GitLab, Azure DevOps, per-plugin token chains | + +## CLI Command Architecture (Option A) + +Connection lifecycle commands live under `configure connection`: +``` +gh devlake configure connection create --plugin gh-copilot ... +gh devlake configure connection delete --plugin gh-copilot --id 2 +gh devlake configure connection update --plugin gh-copilot --id 2 --token ghp_new +gh devlake configure connection list +gh devlake configure connection test --plugin gh-copilot --id 2 +``` + +Each command operates on one plugin at a time. Interactive mode prompts for plugin selection. + +## Key Design Decisions + +1. **One plugin per invocation** in flag mode. Interactive mode walks through plugins sequentially. +2. **`--plugin` flag** replaces `--skip-copilot`/`--skip-github` (positive selection, not negative exclusion). +3. **Singular command names** (`connection`, `scope`, `project`) — not plurals. +4. **Delete/update/test are subcommands**, not flags — each is a distinct action with distinct UX. +5. **Plugin-specific fields** (org, enterprise, repos) are validated per-plugin, not shared across all. diff --git a/AGENTS.md b/AGENTS.md index a568606..5bfbf03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,17 @@ internal/ - **Discovery chain**: explicit `--url` → state file → well-known ports - **Generic API helpers**: `doPost[T]`, `doGet[T]`, `doPut[T]`, `doPatch[T]` in `internal/devlake/client.go` +### Plugin System +Plugins are defined via `ConnectionDef` structs in `cmd/connection_types.go`. Each entry declares the plugin slug, endpoint, required fields (`NeedsOrg`, `NeedsEnterprise`), and PAT scopes. To add a new DevOps tool, add a `ConnectionDef` to `connectionRegistry` — no other registration needed. + +**One plugin per invocation.** Flag-based commands target a single `--plugin`. Interactive mode walks through plugins sequentially. This keeps plugin-specific fields (org, enterprise, repos, tokens) self-contained. + +### Copilot Scope ID Convention +The `gh-copilot` plugin computes scope IDs as: enterprise + org → `"enterprise/org"`, enterprise only → `"enterprise"`, org only → `"org"`. See `copilotScopeID()` in `cmd/configure_scopes.go`. The scope ID must match the plugin's `listGhCopilotRemoteScopes` logic exactly or blueprint references will break. + +## Roadmap +See `.github/skills/gh-devlake-roadmap/SKILL.md` for version plan, milestones, and design decisions. Project board: https://github.com/orgs/DevExpGbb/projects/21 + ## Terminal Output & UX — CRITICAL **The terminal IS the UI.** Every `fmt.Print` call is a UX decision. Readability, rhythm, and breathing room are non-negotiable. diff --git a/cmd/configure_full.go b/cmd/configure_full.go index e08a85e..4f31bb3 100644 --- a/cmd/configure_full.go +++ b/cmd/configure_full.go @@ -109,11 +109,17 @@ func runConfigureFull(cmd *cobra.Command, args []string) error { case "gh-copilot": scopeCopilotConnID = r.ConnectionID scopeSkipCopilot = false + if scopeEnterprise == "" && r.Enterprise != "" { + scopeEnterprise = r.Enterprise + } } } if fullOrg != "" { scopeOrg = fullOrg } + if fullEnterprise != "" { + scopeEnterprise = fullEnterprise + } cfgURL = devlakeURL if err := runConfigureProjects(cmd, args); err != nil { diff --git a/cmd/configure_projects.go b/cmd/configure_projects.go index ed16cc2..1cfe03d 100644 --- a/cmd/configure_projects.go +++ b/cmd/configure_projects.go @@ -34,6 +34,7 @@ Example: } cmd.Flags().StringVar(&scopeOrg, "org", "", "GitHub organization slug") + cmd.Flags().StringVar(&scopeEnterprise, "enterprise", "", "GitHub enterprise slug (enables enterprise-level Copilot metrics)") cmd.Flags().StringVar(&scopeRepos, "repos", "", "Comma-separated repos (owner/repo)") cmd.Flags().StringVar(&scopeReposFile, "repos-file", "", "Path to file with repos (one per line)") cmd.Flags().IntVar(&scopeGHConnID, "github-connection-id", 0, "GitHub connection ID (auto-detected if omitted)") @@ -54,20 +55,21 @@ Example: // connChoice represents a discovered connection for the interactive picker. type connChoice struct { - plugin string - id int - label string + plugin string + id int + label string + enterprise string // enterprise slug from state/API, if available } // addedConnection tracks a connection that has been scoped and is ready // for inclusion in the final blueprint. type addedConnection struct { - plugin string - connID int - label string - summary string // short summary shown in "Added so far" list - bpConn devlake.BlueprintConnection - repos []string // only populated for GitHub connections + plugin string + connID int + label string + summary string // short summary shown in "Added so far" list + bpConn devlake.BlueprintConnection + repos []string // only populated for GitHub connections } func runConfigureProjects(cmd *cobra.Command, args []string) error { @@ -98,6 +100,12 @@ func runConfigureProjects(cmd *cobra.Command, args []string) error { } fmt.Printf(" Organization: %s\n", org) + // ── Resolve enterprise ── + enterprise := resolveEnterprise(state, scopeEnterprise) + if enterprise != "" { + fmt.Printf(" Enterprise: %s\n", enterprise) + } + // ── Project name ── if scopeProjectName == "" { def := org @@ -176,7 +184,7 @@ func runConfigureProjects(cmd *cobra.Command, args []string) error { } // Scope the picked connection - ac, err := scopeConnection(client, picked, org) + ac, err := scopeConnection(client, picked, org, enterprise) if err != nil { fmt.Printf(" ⚠️ Could not scope %s: %v\n", picked.label, err) // Remove from remaining so user doesn't loop on a failing connection @@ -248,7 +256,7 @@ func runConfigureProjects(cmd *cobra.Command, args []string) error { // scopeConnection scopes a single connection (GitHub repos or Copilot org) // and returns an addedConnection with the BlueprintConnection entry. -func scopeConnection(client *devlake.Client, c connChoice, org string) (*addedConnection, error) { +func scopeConnection(client *devlake.Client, c connChoice, org, enterprise string) (*addedConnection, error) { switch c.plugin { case "github": result, err := scopeGitHub(client, c.id, org) @@ -267,11 +275,17 @@ func scopeConnection(client *devlake.Client, c connChoice, org string) (*addedCo }, nil case "gh-copilot": - conn, err := scopeCopilot(client, c.id, org) + // Prefer enterprise from the connection's own state, fall back to resolved value + ent := enterprise + if c.enterprise != "" { + ent = c.enterprise + } + conn, err := scopeCopilot(client, c.id, org, ent) if err != nil { return nil, err } - summary := fmt.Sprintf("GitHub Copilot (ID: %d, org: %s)", c.id, org) + scopeID := copilotScopeID(org, ent) + summary := fmt.Sprintf("GitHub Copilot (ID: %d, scope: %s)", c.id, scopeID) return &addedConnection{ plugin: c.plugin, connID: c.id, @@ -308,7 +322,7 @@ func discoverConnections(client *devlake.Client, state *devlake.State) []connCho key := fmt.Sprintf("%s:%d", c.Plugin, c.ConnectionID) seen[key] = true label := fmt.Sprintf("%s (ID: %d, Name: %q)", pluginDisplayName(c.Plugin), c.ConnectionID, c.Name) - choices = append(choices, connChoice{plugin: c.Plugin, id: c.ConnectionID, label: label}) + choices = append(choices, connChoice{plugin: c.Plugin, id: c.ConnectionID, label: label, enterprise: c.Enterprise}) } } @@ -325,7 +339,7 @@ func discoverConnections(client *devlake.Client, state *devlake.State) []connCho } seen[key] = true label := fmt.Sprintf("%s (ID: %d, Name: %q)", pluginDisplayName(plugin), c.ID, c.Name) - choices = append(choices, connChoice{plugin: plugin, id: c.ID, label: label}) + choices = append(choices, connChoice{plugin: plugin, id: c.ID, label: label, enterprise: c.Enterprise}) } } return choices diff --git a/cmd/configure_scopes.go b/cmd/configure_scopes.go index aaff50d..5d534e4 100644 --- a/cmd/configure_scopes.go +++ b/cmd/configure_scopes.go @@ -17,6 +17,7 @@ import ( var ( scopeOrg string + scopeEnterprise string scopeRepos string scopeReposFile string scopeGHConnID int @@ -49,6 +50,7 @@ Example: } cmd.Flags().StringVar(&scopeOrg, "org", "", "GitHub organization slug") + cmd.Flags().StringVar(&scopeEnterprise, "enterprise", "", "GitHub enterprise slug (enables enterprise-level Copilot metrics)") cmd.Flags().StringVar(&scopeRepos, "repos", "", "Comma-separated repos (owner/repo)") cmd.Flags().StringVar(&scopeReposFile, "repos-file", "", "Path to file with repos (one per line)") cmd.Flags().IntVar(&scopeGHConnID, "github-connection-id", 0, "GitHub connection ID (auto-detected if omitted)") @@ -143,21 +145,22 @@ func scopeGitHub(client *devlake.Client, connID int, org string) (*scopeGitHubRe }, nil } -// scopeCopilot PUTs the org scope for a Copilot connection. +// scopeCopilot PUTs the org/enterprise scope for a Copilot connection. // Returns the BlueprintConnection entry. -func scopeCopilot(client *devlake.Client, connID int, org string) (*devlake.BlueprintConnection, error) { +func scopeCopilot(client *devlake.Client, connID int, org, enterprise string) (*devlake.BlueprintConnection, error) { fmt.Println("\n📝 Adding Copilot scope...") - err := putCopilotScope(client, connID, org) + scopeID := copilotScopeID(org, enterprise) + err := putCopilotScope(client, connID, org, enterprise) if err != nil { return nil, fmt.Errorf("could not add Copilot scope: %w", err) } - fmt.Printf(" ✅ Copilot scope added: %s\n", org) + fmt.Printf(" ✅ Copilot scope added: %s\n", scopeID) return &devlake.BlueprintConnection{ PluginName: "gh-copilot", ConnectionID: connID, Scopes: []devlake.BlueprintScope{ - {ScopeID: org, ScopeName: org}, + {ScopeID: scopeID, ScopeName: scopeID}, }, }, nil } @@ -295,6 +298,12 @@ func runConfigureScopes(cmd *cobra.Command, args []string) error { } fmt.Printf(" Organization: %s\n", org) + // ── Step 4: Resolve enterprise ── + enterprise := resolveEnterprise(state, scopeEnterprise) + if enterprise != "" { + fmt.Printf(" Enterprise: %s\n", enterprise) + } + projectName := scopeProjectName if projectName == "" { projectName = org @@ -314,7 +323,7 @@ func runConfigureScopes(cmd *cobra.Command, args []string) error { } if !scopeSkipCopilot && copilotConnID > 0 { - conn, err := scopeCopilot(client, copilotConnID, org) + conn, err := scopeCopilot(client, copilotConnID, org, enterprise) if err != nil { fmt.Printf(" ⚠️ %v\n", err) } else { @@ -390,6 +399,21 @@ func resolveOrg(state *devlake.State, flagValue string) string { return prompt.ReadLine("Enter your GitHub organization slug") } +// resolveEnterprise determines the enterprise slug from flag, state, or API. +func resolveEnterprise(state *devlake.State, flagValue string) string { + if flagValue != "" { + return flagValue + } + if state != nil { + for _, c := range state.Connections { + if c.Enterprise != "" { + return c.Enterprise + } + } + } + return "" +} + // resolveRepos determines repos from flags, file, or interactive gh CLI selection. func resolveRepos(org string) ([]string, error) { // From --repos flag @@ -491,20 +515,38 @@ func putGitHubScopes(client *devlake.Client, connID, scopeConfigID int, details return client.PutScopes("github", connID, &devlake.ScopeBatchRequest{Data: data}) } -// putCopilotScope adds the organization scope to the Copilot connection. -func putCopilotScope(client *devlake.Client, connID int, org string) error { +// putCopilotScope adds the organization/enterprise scope to the Copilot connection. +func putCopilotScope(client *devlake.Client, connID int, org, enterprise string) error { + scopeID := copilotScopeID(org, enterprise) data := []any{ devlake.CopilotScope{ - ID: org, + ID: scopeID, ConnectionID: connID, Organization: org, - Name: org, - FullName: org, + Enterprise: enterprise, + Name: scopeID, + FullName: scopeID, }, } return client.PutScopes("gh-copilot", connID, &devlake.ScopeBatchRequest{Data: data}) } +// copilotScopeID computes the scope ID matching the plugin's convention: +// - enterprise + org → "enterprise/org" +// - enterprise only → "enterprise" +// - org only → "org" +func copilotScopeID(org, enterprise string) string { + org = strings.TrimSpace(org) + enterprise = strings.TrimSpace(enterprise) + if enterprise != "" { + if org != "" { + return enterprise + "/" + org + } + return enterprise + } + return org +} + // ensureProjectWithFlags creates a project or returns an existing one's blueprint ID. // Uses explicit hasGitHub/hasCopilot flags instead of package globals. func ensureProjectWithFlags(client *devlake.Client, name, org string, hasGitHub, hasCopilot bool) (int, error) { diff --git a/cmd/configure_scopes_test.go b/cmd/configure_scopes_test.go new file mode 100644 index 0000000..c51b23b --- /dev/null +++ b/cmd/configure_scopes_test.go @@ -0,0 +1,59 @@ +package cmd + +import "testing" + +func TestCopilotScopeID(t *testing.T) { + tests := []struct { + name string + org string + enterprise string + want string + }{ + { + name: "org only", + org: "my-org", + want: "my-org", + }, + { + name: "enterprise and org", + org: "my-org", + enterprise: "my-enterprise", + want: "my-enterprise/my-org", + }, + { + name: "enterprise only", + enterprise: "my-enterprise", + want: "my-enterprise", + }, + { + name: "enterprise with whitespace-only org", + org: " ", + enterprise: "my-enterprise", + want: "my-enterprise", + }, + { + name: "whitespace-only enterprise falls back to org", + org: "my-org", + enterprise: " ", + want: "my-org", + }, + { + name: "both with leading/trailing spaces", + org: " my-org ", + enterprise: " my-ent ", + want: "my-ent/my-org", + }, + { + name: "both empty", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := copilotScopeID(tt.org, tt.enterprise) + if got != tt.want { + t.Errorf("copilotScopeID(%q, %q) = %q, want %q", tt.org, tt.enterprise, got, tt.want) + } + }) + } +} diff --git a/cmd/connection_types.go b/cmd/connection_types.go index 16f2132..3e99731 100644 --- a/cmd/connection_types.go +++ b/cmd/connection_types.go @@ -53,13 +53,17 @@ func (d *ConnectionDef) BuildCreateRequest(name string, params ConnectionParams) if params.Endpoint != "" { endpoint = params.Endpoint } + rateLimitPerHour := 4500 + if d.Plugin == "gh-copilot" { + rateLimitPerHour = 5000 + } req := &devlake.ConnectionCreateRequest{ Name: name, Endpoint: endpoint, Proxy: params.Proxy, AuthMethod: "AccessToken", Token: params.Token, - RateLimitPerHour: 4500, + RateLimitPerHour: rateLimitPerHour, } if d.Plugin == "github" { req.EnableGraphql = true @@ -79,16 +83,26 @@ func (d *ConnectionDef) BuildTestRequest(params ConnectionParams) *devlake.Conne if params.Endpoint != "" { endpoint = params.Endpoint } + rateLimitPerHour := 4500 + if d.Plugin == "gh-copilot" { + rateLimitPerHour = 5000 + } req := &devlake.ConnectionTestRequest{ Endpoint: endpoint, AuthMethod: "AccessToken", Token: params.Token, - RateLimitPerHour: 4500, + RateLimitPerHour: rateLimitPerHour, Proxy: params.Proxy, } if d.Plugin == "github" { req.EnableGraphql = true } + if d.NeedsOrg && params.Org != "" { + req.Organization = params.Org + } + if d.NeedsEnterprise && params.Enterprise != "" { + req.Enterprise = params.Enterprise + } return req } @@ -110,6 +124,7 @@ var connectionRegistry = []*ConnectionDef{ Endpoint: "https://api.github.com/", NeedsOrg: true, NeedsEnterprise: true, + SupportsTest: true, RequiredScopes: []string{"manage_billing:copilot", "read:org"}, ScopeHint: "manage_billing:copilot, read:org (+ read:enterprise for enterprise metrics)", }, diff --git a/cmd/connection_types_test.go b/cmd/connection_types_test.go new file mode 100644 index 0000000..0040e7b --- /dev/null +++ b/cmd/connection_types_test.go @@ -0,0 +1,124 @@ +package cmd + +import "testing" + +func TestBuildCreateRequest_RateLimit(t *testing.T) { + tests := []struct { + name string + plugin string + wantRate int + }{ + {"github uses 4500", "github", 4500}, + {"gh-copilot uses 5000", "gh-copilot", 5000}, + {"unknown uses 4500", "gitlab", 4500}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + def := &ConnectionDef{Plugin: tt.plugin, Endpoint: "https://api.github.com/"} + req := def.BuildCreateRequest("test", ConnectionParams{Token: "tok"}) + if req.RateLimitPerHour != tt.wantRate { + t.Errorf("got rate limit %d, want %d", req.RateLimitPerHour, tt.wantRate) + } + }) + } +} + +func TestBuildCreateRequest_EnterpriseOrg(t *testing.T) { + def := &ConnectionDef{ + Plugin: "gh-copilot", + Endpoint: "https://api.github.com/", + NeedsOrg: true, + NeedsEnterprise: true, + } + + t.Run("org and enterprise", func(t *testing.T) { + req := def.BuildCreateRequest("test", ConnectionParams{ + Token: "tok", + Org: "my-org", + Enterprise: "my-ent", + }) + if req.Organization != "my-org" { + t.Errorf("got Organization %q, want %q", req.Organization, "my-org") + } + if req.Enterprise != "my-ent" { + t.Errorf("got Enterprise %q, want %q", req.Enterprise, "my-ent") + } + }) + + t.Run("org only", func(t *testing.T) { + req := def.BuildCreateRequest("test", ConnectionParams{ + Token: "tok", + Org: "my-org", + }) + if req.Organization != "my-org" { + t.Errorf("got Organization %q, want %q", req.Organization, "my-org") + } + if req.Enterprise != "" { + t.Errorf("got Enterprise %q, want empty", req.Enterprise) + } + }) + + t.Run("enterprise only", func(t *testing.T) { + req := def.BuildCreateRequest("test", ConnectionParams{ + Token: "tok", + Enterprise: "my-ent", + }) + if req.Organization != "" { + t.Errorf("got Organization %q, want empty", req.Organization) + } + if req.Enterprise != "my-ent" { + t.Errorf("got Enterprise %q, want %q", req.Enterprise, "my-ent") + } + }) +} + +func TestBuildTestRequest_CopilotFields(t *testing.T) { + def := &ConnectionDef{ + Plugin: "gh-copilot", + Endpoint: "https://api.github.com/", + NeedsOrg: true, + NeedsEnterprise: true, + } + + t.Run("includes org and enterprise", func(t *testing.T) { + req := def.BuildTestRequest(ConnectionParams{ + Token: "tok", + Org: "my-org", + Enterprise: "my-ent", + }) + if req.Organization != "my-org" { + t.Errorf("got Organization %q, want %q", req.Organization, "my-org") + } + if req.Enterprise != "my-ent" { + t.Errorf("got Enterprise %q, want %q", req.Enterprise, "my-ent") + } + if req.RateLimitPerHour != 5000 { + t.Errorf("got rate limit %d, want 5000", req.RateLimitPerHour) + } + }) + + t.Run("github does not include org/enterprise", func(t *testing.T) { + ghDef := &ConnectionDef{ + Plugin: "github", + Endpoint: "https://api.github.com/", + SupportsTest: true, + } + req := ghDef.BuildTestRequest(ConnectionParams{ + Token: "tok", + Org: "ignored", + Enterprise: "ignored", + }) + if req.Organization != "" { + t.Errorf("github test request should not have Organization, got %q", req.Organization) + } + if req.Enterprise != "" { + t.Errorf("github test request should not have Enterprise, got %q", req.Enterprise) + } + if req.RateLimitPerHour != 4500 { + t.Errorf("got rate limit %d, want 4500", req.RateLimitPerHour) + } + if !req.EnableGraphql { + t.Error("github test request should have EnableGraphql=true") + } + }) +} diff --git a/internal/devlake/client.go b/internal/devlake/client.go index 2d4f479..94d4b2d 100644 --- a/internal/devlake/client.go +++ b/internal/devlake/client.go @@ -41,8 +41,10 @@ func (c *Client) Ping() error { // Connection represents a DevLake plugin connection. type Connection struct { - ID int `json:"id"` - Name string `json:"name"` + ID int `json:"id"` + Name string `json:"name"` + Organization string `json:"organization,omitempty"` + Enterprise string `json:"enterprise,omitempty"` } // ConnectionCreateRequest is the payload for creating a GitHub or Copilot connection. @@ -68,6 +70,8 @@ type ConnectionTestRequest struct { EnableGraphql bool `json:"enableGraphql,omitempty"` RateLimitPerHour int `json:"rateLimitPerHour"` Proxy string `json:"proxy"` + Organization string `json:"organization,omitempty"` + Enterprise string `json:"enterprise,omitempty"` } // ConnectionTestResult is the response from testing a connection. diff --git a/internal/devlake/types.go b/internal/devlake/types.go index 684689c..7f4c615 100644 --- a/internal/devlake/types.go +++ b/internal/devlake/types.go @@ -29,11 +29,12 @@ type GitHubRepoScope struct { ScopeConfigID int `json:"scopeConfigId,omitempty"` } -// CopilotScope represents a Copilot organization scope entry. +// CopilotScope represents a Copilot organization or enterprise scope entry. type CopilotScope struct { ID string `json:"id"` ConnectionID int `json:"connectionId"` Organization string `json:"organization"` + Enterprise string `json:"enterprise,omitempty"` Name string `json:"name"` FullName string `json:"fullName"` }