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