This is an automated email from the ASF dual-hosted git repository.

ewega pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new f93895f32 fix(gh-copilot): preserve hyphenated enterprise slugs (#8935)
f93895f32 is described below

commit f93895f32dc8846de2cdc503824fa4ea03b02dbb
Author: bujjibabukatta <[email protected]>
AuthorDate: Tue Jul 14 00:11:57 2026 +0530

    fix(gh-copilot): preserve hyphenated enterprise slugs (#8935)
    
    Co-authored-by: Bujjibabukatta <[email protected]>
    Co-authored-by: Eldrick Wega <[email protected]>
    Co-authored-by: Eldrick Wega <[email protected]>
---
 backend/plugins/gh-copilot/service/api_path.go     | 28 ++++++++++++++++++++
 .../plugins/gh-copilot/service/api_path_test.go    | 30 ++++++++++++++++++++++
 .../gh-copilot/service/connection_test_helper.go   |  4 +--
 backend/plugins/gh-copilot/tasks/api_path.go       | 28 ++++++++++++++++++++
 backend/plugins/gh-copilot/tasks/api_path_test.go  | 30 ++++++++++++++++++++++
 .../tasks/enterprise_metrics_collector.go          |  8 +++---
 .../gh-copilot/tasks/org_metrics_collector.go      |  8 +++---
 backend/plugins/gh-copilot/tasks/seat_collector.go |  4 +--
 .../gh-copilot/tasks/user_metrics_collector.go     |  5 ++--
 .../gh-copilot/tasks/user_teams_collector.go       |  5 ++--
 .../plugins/components/connection-form/index.tsx   |  2 ++
 docker-compose-dev-postgresql.yml                  |  2 +-
 12 files changed, 133 insertions(+), 21 deletions(-)

diff --git a/backend/plugins/gh-copilot/service/api_path.go 
b/backend/plugins/gh-copilot/service/api_path.go
new file mode 100644
index 000000000..ec593c946
--- /dev/null
+++ b/backend/plugins/gh-copilot/service/api_path.go
@@ -0,0 +1,28 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package service
+
+import (
+       "fmt"
+       "net/url"
+       "strings"
+)
+
+func copilotAPIPath(namespace, slug, resource string) string {
+       return fmt.Sprintf("%s/%s/%s", namespace, 
url.PathEscape(strings.TrimSpace(slug)), strings.TrimPrefix(resource, "/"))
+}
diff --git a/backend/plugins/gh-copilot/service/api_path_test.go 
b/backend/plugins/gh-copilot/service/api_path_test.go
new file mode 100644
index 000000000..4a57cc1e4
--- /dev/null
+++ b/backend/plugins/gh-copilot/service/api_path_test.go
@@ -0,0 +1,30 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package service
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestCopilotAPIPathPreservesHyphenatedEnterpriseSlug(t *testing.T) {
+       path := copilotAPIPath("enterprises", "my-enterprise", 
"copilot/billing/seats")
+
+       assert.Equal(t, "enterprises/my-enterprise/copilot/billing/seats", path)
+}
diff --git a/backend/plugins/gh-copilot/service/connection_test_helper.go 
b/backend/plugins/gh-copilot/service/connection_test_helper.go
index 9cda8ae1d..889147602 100644
--- a/backend/plugins/gh-copilot/service/connection_test_helper.go
+++ b/backend/plugins/gh-copilot/service/connection_test_helper.go
@@ -85,7 +85,7 @@ func TestConnection(ctx stdctx.Context, br corectx.BasicRes, 
connection *models.
        // Note: /enterprises/{ent}/copilot/billing does not exist — use 
/billing/seats instead.
        if hasEnterprise {
                entSlug := strings.TrimSpace(connection.Enterprise)
-               seatsPath := 
fmt.Sprintf("enterprises/%s/copilot/billing/seats", entSlug)
+               seatsPath := copilotAPIPath("enterprises", entSlug, 
"copilot/billing/seats")
                entSummary, entErr := fetchSeatsSummary(apiClient, seatsPath)
                if entErr != nil {
                        return nil, entErr
@@ -97,7 +97,7 @@ func TestConnection(ctx stdctx.Context, br corectx.BasicRes, 
connection *models.
 
        // Test org endpoint when configured.
        if hasOrg {
-               orgSummary, orgErr := fetchBillingSummary(apiClient, 
fmt.Sprintf("orgs/%s/copilot/billing", connection.Organization))
+               orgSummary, orgErr := fetchBillingSummary(apiClient, 
copilotAPIPath("orgs", connection.Organization, "copilot/billing"))
                if orgErr != nil {
                        return nil, orgErr
                }
diff --git a/backend/plugins/gh-copilot/tasks/api_path.go 
b/backend/plugins/gh-copilot/tasks/api_path.go
new file mode 100644
index 000000000..0d177ee14
--- /dev/null
+++ b/backend/plugins/gh-copilot/tasks/api_path.go
@@ -0,0 +1,28 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "fmt"
+       "net/url"
+       "strings"
+)
+
+func copilotAPIPath(namespace, slug, resource string) string {
+       return fmt.Sprintf("%s/%s/%s", namespace, 
url.PathEscape(strings.TrimSpace(slug)), strings.TrimPrefix(resource, "/"))
+}
diff --git a/backend/plugins/gh-copilot/tasks/api_path_test.go 
b/backend/plugins/gh-copilot/tasks/api_path_test.go
new file mode 100644
index 000000000..b92420484
--- /dev/null
+++ b/backend/plugins/gh-copilot/tasks/api_path_test.go
@@ -0,0 +1,30 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/require"
+)
+
+func TestCopilotAPIPathPreservesHyphenatedEnterpriseSlug(t *testing.T) {
+       path := copilotAPIPath("enterprises", "my-enterprise", 
"copilot/billing/seats")
+
+       require.Equal(t, "enterprises/my-enterprise/copilot/billing/seats", 
path)
+}
diff --git a/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go 
b/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
index 16061cf50..e153d59c9 100644
--- a/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
@@ -19,7 +19,6 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "net/http"
        "net/url"
        "time"
@@ -81,10 +80,9 @@ func CollectEnterpriseMetrics(taskCtx plugin.SubTaskContext) 
errors.Error {
        dayIter := newDayIterator(start, until)
 
        err = collector.InitCollector(helper.ApiCollectorArgs{
-               ApiClient: apiClient,
-               Input:     dayIter,
-               UrlTemplate: 
fmt.Sprintf("enterprises/%s/copilot/metrics/reports/enterprise-1-day",
-                       connection.Enterprise),
+               ApiClient:   apiClient,
+               Input:       dayIter,
+               UrlTemplate: copilotAPIPath("enterprises", 
connection.Enterprise, "copilot/metrics/reports/enterprise-1-day"),
                Query: func(reqData *helper.RequestData) (url.Values, 
errors.Error) {
                        input := reqData.Input.(*dayInput)
                        q := url.Values{}
diff --git a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go 
b/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
index c3a8b5e44..c75ce7b6c 100644
--- a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
@@ -19,7 +19,6 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "io"
        "net/http"
        "net/url"
@@ -76,10 +75,9 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext) 
errors.Error {
        dayIter := newDayIterator(start, until)
 
        err = collector.InitCollector(helper.ApiCollectorArgs{
-               ApiClient: apiClient,
-               Input:     dayIter,
-               UrlTemplate: 
fmt.Sprintf("orgs/%s/copilot/metrics/reports/organization-1-day",
-                       connection.Organization),
+               ApiClient:   apiClient,
+               Input:       dayIter,
+               UrlTemplate: copilotAPIPath("orgs", connection.Organization, 
"copilot/metrics/reports/organization-1-day"),
                Query: func(reqData *helper.RequestData) (url.Values, 
errors.Error) {
                        input := reqData.Input.(*dayInput)
                        q := url.Values{}
diff --git a/backend/plugins/gh-copilot/tasks/seat_collector.go 
b/backend/plugins/gh-copilot/tasks/seat_collector.go
index 8724805f6..546a0e30a 100644
--- a/backend/plugins/gh-copilot/tasks/seat_collector.go
+++ b/backend/plugins/gh-copilot/tasks/seat_collector.go
@@ -90,9 +90,9 @@ func CollectCopilotSeatAssignments(taskCtx 
plugin.SubTaskContext) errors.Error {
        var urlTemplate string
        switch {
        case connection.HasEnterprise():
-               urlTemplate = 
fmt.Sprintf("enterprises/%s/copilot/billing/seats", connection.Enterprise)
+               urlTemplate = copilotAPIPath("enterprises", 
connection.Enterprise, "copilot/billing/seats")
        case connection.Organization != "":
-               urlTemplate = fmt.Sprintf("orgs/%s/copilot/billing/seats", 
connection.Organization)
+               urlTemplate = copilotAPIPath("orgs", connection.Organization, 
"copilot/billing/seats")
        default:
                taskCtx.GetLogger().Warn(nil, "skipping seat collection: no 
enterprise or organization configured on connection %d", connection.ID)
                return nil
diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_collector.go 
b/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
index a092a0450..8198b48a6 100644
--- a/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
@@ -19,7 +19,6 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "io"
        "net/http"
        "net/url"
@@ -95,9 +94,9 @@ func CollectUserMetrics(taskCtx plugin.SubTaskContext) 
errors.Error {
        var urlTemplate string
 
        if connection.HasEnterprise() {
-               urlTemplate = 
fmt.Sprintf("enterprises/%s/copilot/metrics/reports/users-1-day", 
connection.Enterprise)
+               urlTemplate = copilotAPIPath("enterprises", 
connection.Enterprise, "copilot/metrics/reports/users-1-day")
        } else if connection.Organization != "" {
-               urlTemplate = 
fmt.Sprintf("orgs/%s/copilot/metrics/reports/users-1-day", 
connection.Organization)
+               urlTemplate = copilotAPIPath("orgs", connection.Organization, 
"copilot/metrics/reports/users-1-day")
        } else {
                return nil
        }
diff --git a/backend/plugins/gh-copilot/tasks/user_teams_collector.go 
b/backend/plugins/gh-copilot/tasks/user_teams_collector.go
index 2ae0200d2..dc3287e5c 100644
--- a/backend/plugins/gh-copilot/tasks/user_teams_collector.go
+++ b/backend/plugins/gh-copilot/tasks/user_teams_collector.go
@@ -19,7 +19,6 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "io"
        "net/http"
        "net/url"
@@ -45,9 +44,9 @@ func CollectUserTeams(taskCtx plugin.SubTaskContext) 
errors.Error {
        var urlTemplate string
 
        if connection.HasEnterprise() {
-               urlTemplate = 
fmt.Sprintf("enterprises/%s/copilot/metrics/reports/user-teams-1-day", 
connection.Enterprise)
+               urlTemplate = copilotAPIPath("enterprises", 
connection.Enterprise, "copilot/metrics/reports/user-teams-1-day")
        } else if connection.Organization != "" {
-               urlTemplate = 
fmt.Sprintf("orgs/%s/copilot/metrics/reports/user-teams-1-day", 
connection.Organization)
+               urlTemplate = copilotAPIPath("orgs", connection.Organization, 
"copilot/metrics/reports/user-teams-1-day")
        } else {
                return nil
        }
diff --git a/config-ui/src/plugins/components/connection-form/index.tsx 
b/config-ui/src/plugins/components/connection-form/index.tsx
index 39f4cf0df..e846c278c 100644
--- a/config-ui/src/plugins/components/connection-form/index.tsx
+++ b/config-ui/src/plugins/components/connection-form/index.tsx
@@ -59,6 +59,7 @@ const buildUpdateTestPayload = (connection: any, values: any, 
customHeaders: ICu
   dbUrl: isEqual(connection?.dbUrl, values.dbUrl) ? undefined : values.dbUrl,
   companyId: isEqual(connection?.companyId, values.companyId) ? undefined : 
values.companyId,
   organization: isEqual(connection?.organization, values.organization) ? 
undefined : values.organization,
+  enterprise: isEqual(connection?.enterprise, values.enterprise) ? undefined : 
values.enterprise,
   customHeaders: isEqual(connection?.customHeaders, customHeaders) ? undefined 
: customHeaders,
 });
 
@@ -85,6 +86,7 @@ const buildCreateTestPayload = (initialValues: any, values: 
any, customHeaders:
     'dbUrl',
     'companyId',
     'organization',
+    'enterprise',
     'customHeaders',
   ]);
 
diff --git a/docker-compose-dev-postgresql.yml 
b/docker-compose-dev-postgresql.yml
index eff854777..b718cf5d3 100644
--- a/docker-compose-dev-postgresql.yml
+++ b/docker-compose-dev-postgresql.yml
@@ -16,7 +16,7 @@
 name: devlake-postgresql
 
 services:
-  postgres:
+  postgres: 
     image: postgres:17.2
     volumes:
       - postgres-storage:/var/lib/postgresql

Reply via email to