Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a11ff36
upstream | chore: Collect teams and display per team metrics
tamas-la Mar 4, 2026
6cddb80
upstream | XPL-496: Fix queries and add missing panels to dashboard
tamas-la Mar 4, 2026
de77d55
XPL-496: Create new dashboard filterable by team and user
tamas-la Mar 6, 2026
ce2a9a5
XPL-496: Fix partially functional charts
tamas-la Mar 9, 2026
63c809e
Update URL for consistency across the plugin
la-tamas Mar 26, 2026
db2a921
fix(q_dev): prevent data duplication in user_report and user_data tab…
warren830 Feb 28, 2026
4bb62dd
feat(github): Extend exclusion of file extensions to github plugin (#…
emfrab Mar 2, 2026
756a0db
fix(doc): update expired Slack invite links in README (#8739)
spiffaz Mar 2, 2026
26f3a73
docs: add gh-devlake CLI to Getting Started installation options (#8733)
ewega Mar 2, 2026
1421904
fix(gitlab): add missing repos scope in project_mapping (#8743)
spiffaz Mar 2, 2026
9e5c911
fix(grafana): update dashboard descriptions to list all supported dat…
spiffaz Mar 4, 2026
d087b50
fix: modify cicd_deployments name from varchar to text (#8724)
dncrews Mar 5, 2026
14260c4
fix(q_dev): replace MariaDB-specific IF NOT EXISTS syntax with DAL me…
yamoyamoto Mar 5, 2026
f94bbbd
fix(azuredevops): default empty entities and add CROSS to repo scope …
spiffaz Mar 5, 2026
0554c4f
fix(bitbucket): default empty entities to all domain types in makeSco…
spiffaz Mar 6, 2026
c0845ea
feat(gh-copilot): add support for organization daily user metrics (#8…
ReeceXW Mar 10, 2026
f26ef79
feat(circleci): add server version requirement and endpoint help text…
jbsmith7741 Mar 10, 2026
a4e7351
feat(asana): add Asana plugin for project and task collection (#8758)
jawad-khan Mar 12, 2026
5f8c575
feat: GitHub App token refresh (#8746)
lrf-nitro Mar 12, 2026
2071811
fix: cwe89 sql injection (#8762)
klesh Mar 12, 2026
f9b431c
feat(q-dev): add logging data ingestion and enrich Kiro dashboards (#…
warren830 Mar 15, 2026
475dc49
feat(qa): add is_invalid field to qa_test_case_executions (#8764)
narrowizard Mar 17, 2026
c361808
feat(linker): link when branch names contain issue keys (#8777)
ReeceXW Mar 19, 2026
7a4b03d
Add codespell support with configuration and fixes (#8761)
yarikoptic Mar 19, 2026
815f02c
feat(q-dev): enrich logging fields, separate dashboards, add E2E test…
warren830 Mar 21, 2026
d9e6bc8
fix: add SQL identifier validation to prevent SQL injection via table…
warren830 Mar 21, 2026
e0714d5
feat(q-dev): add Kiro Credits + DORA Correlation dashboard (#8792)
warren830 Mar 22, 2026
4f4c7b5
feat(q-dev): add AI Cost-Efficiency dashboard (#8793)
warren830 Mar 22, 2026
aa1ef95
feat(q-dev): add Multi-AI Tool Comparison dashboard (Copilot vs Kiro)…
warren830 Mar 22, 2026
4f0e5ee
feat(q-dev): add Kiro AI Model ROI dashboard (#8795)
warren830 Mar 22, 2026
371cf68
feat(q-dev): add Steering & Spec Mode Adoption dashboard (#8798)
warren830 Mar 22, 2026
bb8103d
feat(q-dev): add Developer AI Productivity Hours dashboard (#8797)
warren830 Mar 22, 2026
e155321
feat(q-dev): add Language AI Heatmap dashboard (#8796)
warren830 Mar 22, 2026
46a5fa0
Fix/circleci column names (#8799)
varsis Mar 23, 2026
a502532
fix(jenkins): scope multi-branch build collection to current project …
KyriosGN0 Mar 23, 2026
4bc4643
fix: Make gh-copilot plugin database agnostic (#8779)
sclausson Mar 23, 2026
fd4d572
fix(sonarqube): increase cq_issues and cq_file_metrics project_key le…
jbsmith7741 Mar 23, 2026
89c370a
feat: added taiga plugin (#8755)
irfanuddinahmad Mar 23, 2026
7dc9144
feat: Collect teams and display per team metrics
tamas-la Mar 4, 2026
d77361f
Resolve conflicts in backend/plugins/gh-copilot/models/migrationscri…
tamas-la Mar 26, 2026
0fc2202
Merge branch 'main' into feat/copilot_per_team_metrics_rework
ewega Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
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 migrationscripts

import (
"github.com/apache/incubator-devlake/core/context"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/helpers/migrationhelper"
"github.com/apache/incubator-devlake/plugins/gh-copilot/models"

Check warning on line 24 in backend/plugins/gh-copilot/models/migrationscripts/20260227_add_team_tables.go

View workflow job for this annotation

GitHub Actions / migration-script-lint

./plugins/gh-copilot/models/migrationscripts/20260227_add_team_tables.go imports forbidden package "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
)

type addTeamTables struct{}

func (script *addTeamTables) Up(basicRes context.BasicRes) errors.Error {
return migrationhelper.AutoMigrateTables(
basicRes,
&models.GhCopilotTeam{},
&models.GhCopilotTeamUser{},
)
}

func (*addTeamTables) Version() uint64 {
return 20260227000000
}

func (*addTeamTables) Name() string {
return "copilot add team and team_user tables"
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func All() []plugin.MigrationScript {
new(addScopeConfig20260121),
new(migrateToUsageMetricsV2),
new(addPRFieldsToEnterpriseMetrics),
new(addTeamTables),
new(addOrganizationIdToUserMetrics),
new(addTeamTables),
new(addOrganizationIdToUserMetrics),
}
}
3 changes: 3 additions & 0 deletions backend/plugins/gh-copilot/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ func GetTablesInfo() []dal.Tabler {
&GhCopilotUserMetricsByModelFeature{},
// Seat assignments
&GhCopilotSeat{},
// Team / team-member tables (org-level)
&GhCopilotTeam{},
&GhCopilotTeamUser{},
}
}
36 changes: 19 additions & 17 deletions backend/plugins/gh-copilot/models/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,26 @@ import "testing"
func TestGetTablesInfo(t *testing.T) {
tables := GetTablesInfo()
expected := map[string]bool{
(&GhCopilotConnection{}).TableName(): false,
(&GhCopilotScope{}).TableName(): false,
(&GhCopilotScopeConfig{}).TableName(): false,
(&GhCopilotOrgMetrics{}).TableName(): false,
(&GhCopilotLanguageMetrics{}).TableName(): false,
(&GhCopilotEnterpriseDailyMetrics{}).TableName(): false,
(&GhCopilotMetricsByIde{}).TableName(): false,
(&GhCopilotMetricsByFeature{}).TableName(): false,
(&GhCopilotMetricsByLanguageFeature{}).TableName(): false,
(&GhCopilotMetricsByLanguageModel{}).TableName(): false,
(&GhCopilotMetricsByModelFeature{}).TableName(): false,
(&GhCopilotUserDailyMetrics{}).TableName(): false,
(&GhCopilotUserMetricsByIde{}).TableName(): false,
(&GhCopilotUserMetricsByFeature{}).TableName(): false,
(&GhCopilotConnection{}).TableName(): false,
(&GhCopilotScope{}).TableName(): false,
(&GhCopilotScopeConfig{}).TableName(): false,
(&GhCopilotOrgMetrics{}).TableName(): false,
(&GhCopilotLanguageMetrics{}).TableName(): false,
(&GhCopilotEnterpriseDailyMetrics{}).TableName(): false,
(&GhCopilotMetricsByIde{}).TableName(): false,
(&GhCopilotMetricsByFeature{}).TableName(): false,
(&GhCopilotMetricsByLanguageFeature{}).TableName(): false,
(&GhCopilotMetricsByLanguageModel{}).TableName(): false,
(&GhCopilotMetricsByModelFeature{}).TableName(): false,
(&GhCopilotUserDailyMetrics{}).TableName(): false,
(&GhCopilotUserMetricsByIde{}).TableName(): false,
(&GhCopilotUserMetricsByFeature{}).TableName(): false,
(&GhCopilotUserMetricsByLanguageFeature{}).TableName(): false,
(&GhCopilotUserMetricsByLanguageModel{}).TableName(): false,
(&GhCopilotUserMetricsByModelFeature{}).TableName(): false,
(&GhCopilotSeat{}).TableName(): false,
(&GhCopilotUserMetricsByLanguageModel{}).TableName(): false,
(&GhCopilotUserMetricsByModelFeature{}).TableName(): false,
(&GhCopilotSeat{}).TableName(): false,
(&GhCopilotTeam{}).TableName(): false,
(&GhCopilotTeamUser{}).TableName(): false,
}

if len(tables) != len(expected) {
Expand Down
47 changes: 47 additions & 0 deletions backend/plugins/gh-copilot/models/team.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
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 models

import (
"time"

"github.com/apache/incubator-devlake/core/models/common"
)

// GhCopilotTeam stores a GitHub organization team in the tool layer.
type GhCopilotTeam struct {
ConnectionId uint64 `gorm:"primaryKey"`
Id int `json:"id" gorm:"primaryKey;autoIncrement:false"`
OrgId *int `json:"organization_id" gorm:"index"`
OrgLogin string `json:"org_login" gorm:"type:varchar(255);index"`
Name string `json:"name" gorm:"type:varchar(255)"`
Slug string `json:"slug" gorm:"type:varchar(255);index"`
Description string `json:"description"`
Privacy string `json:"privacy" gorm:"type:varchar(100)"`
Permission string `json:"permission" gorm:"type:varchar(100)"`
NotificationSetting string `json:"notification_setting" gorm:"type:varchar(100)"`
ParentTeamId *int `json:"parent_team_id" gorm:"index"`
ParentTeamSlug string `json:"parent_team_slug" gorm:"type:varchar(255);index"`
GithubCreatedAt *time.Time
GithubUpdatedAt *time.Time
common.NoPKModel
}

func (GhCopilotTeam) TableName() string {
return "_tool_copilot_teams"
}
38 changes: 38 additions & 0 deletions backend/plugins/gh-copilot/models/team_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
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 models

import "github.com/apache/incubator-devlake/core/models/common"

// GhCopilotTeamUser stores a team-member relationship in the tool layer.
type GhCopilotTeamUser struct {
ConnectionId uint64 `gorm:"primaryKey"`
TeamId int `gorm:"primaryKey;autoIncrement:false"`
UserId int `gorm:"primaryKey;autoIncrement:false"`
OrgLogin string `gorm:"type:varchar(255);index"`
TeamSlug string `gorm:"type:varchar(255);index"`
UserLogin string `json:"login" gorm:"type:varchar(255);index"`
Type string `json:"type" gorm:"type:varchar(100)"`
ViewType string `json:"user_view_type" gorm:"type:varchar(100)"`
IsSiteAdmin bool `json:"site_admin"`
common.NoPKModel
}

func (GhCopilotTeamUser) TableName() string {
return "_tool_copilot_team_users"
}
55 changes: 55 additions & 0 deletions backend/plugins/gh-copilot/tasks/pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
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 (
"net/http"
"regexp"
"strconv"
"strings"

"github.com/apache/incubator-devlake/core/errors"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
)

// getTotalPagesFromResponse parses the GitHub Link header to determine the last page number.
// This is used for paginated list endpoints (teams, team members).
func getTotalPagesFromResponse(res *http.Response, _ *helper.ApiCollectorArgs) (int, errors.Error) {
link := res.Header.Get("Link")
if link == "" {
return 0, nil
}
pagePattern := regexp.MustCompile(`page=(\d+)`)
relPattern := regexp.MustCompile(`rel="([a-z]+)"`)
for _, part := range strings.Split(link, ",") {
relMatch := relPattern.FindStringSubmatch(part)
if len(relMatch) < 2 || relMatch[1] != "last" {
continue
}
pageMatch := pagePattern.FindStringSubmatch(part)
if len(pageMatch) < 2 {
continue
}
last, err := strconv.Atoi(pageMatch[1])
if err != nil {
return 0, errors.Default.Wrap(err, "failed to parse last page")
}
return last, nil
}
return 0, nil
}
9 changes: 7 additions & 2 deletions backend/plugins/gh-copilot/tasks/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ import "github.com/apache/incubator-devlake/core/plugin"
// GetSubTaskMetas returns the ordered list of Copilot subtasks.
func GetSubTaskMetas() []plugin.SubTaskMeta {
return []plugin.SubTaskMeta{
// Collectors
// Collectors – metrics
CollectOrgMetricsMeta,
CollectCopilotSeatAssignmentsMeta,
CollectEnterpriseMetricsMeta,
CollectUserMetricsMeta,
// Extractors
// Collectors – teams (extract teams before collecting team users)
CollectTeamsMeta,
ExtractTeamsMeta,
CollectTeamUsersMeta,
ExtractTeamUsersMeta,
// Extractors – metrics
ExtractSeatsMeta,
ExtractOrgMetricsMeta,
ExtractEnterpriseMetricsMeta,
Expand Down
103 changes: 103 additions & 0 deletions backend/plugins/gh-copilot/tasks/team_collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
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 (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
)

const rawCopilotTeamTable = "copilot_api_teams"

var CollectTeamsMeta = plugin.SubTaskMeta{
Name: "collectTeams",
EntryPoint: CollectTeams,
EnabledByDefault: true,
Description: "Collect teams data from GitHub API for the configured organization.",
DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
DependencyTables: []string{},
ProductTables: []string{rawCopilotTeamTable},
}

func CollectTeams(taskCtx plugin.SubTaskContext) errors.Error {
data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
if !ok {
return errors.Default.New("task data is not GhCopilotTaskData")
}
connection := data.Connection
connection.Normalize()

org := strings.TrimSpace(connection.Organization)
if org == "" {
taskCtx.GetLogger().Warn(nil, "skipping team collection: no organization configured on connection %d", connection.ID)
return nil
}

apiClient, err := CreateApiClient(taskCtx.TaskContext(), connection)
if err != nil {
return err
}

collector, cErr := helper.NewStatefulApiCollector(helper.RawDataSubTaskArgs{
Ctx: taskCtx,
Table: rawCopilotTeamTable,
Options: copilotRawParams{
ConnectionId: data.Options.ConnectionId,
ScopeId: data.Options.ScopeId,
Organization: org,
Endpoint: connection.Endpoint,
},
})
if cErr != nil {
return cErr
}

cErr = collector.InitCollector(helper.ApiCollectorArgs{
ApiClient: apiClient,
PageSize: 100,
UrlTemplate: fmt.Sprintf("orgs/%s/teams", org),
Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
query := url.Values{}
query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
query.Set("per_page", fmt.Sprintf("%v", reqData.Pager.Size))
return query, nil
},
GetTotalPages: getTotalPagesFromResponse,
ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
var items []json.RawMessage
e := helper.UnmarshalResponse(res, &items)
if e != nil {
return nil, e
}
return items, nil
},
AfterResponse: ignore404,
})
if cErr != nil {
return cErr
}

return collector.Execute()
}
Loading
Loading