Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
027e999
feat(monorepo): add plugin for per-sub-project DORA metrics in a mono…
uriahrokach Aug 9, 2026
37a0b6e
feat(monorepo): add config-ui panel for editing sub-project settings
uriahrokach Aug 16, 2026
aeacb70
fix(monorepo): enforce dora-before-monorepo pipeline ordering
uriahrokach Aug 16, 2026
bd0b3cb
Merge branch 'apache:main' into feature/subprojects-for-monorepo
eyal4D Aug 16, 2026
57a3d74
Merge remote-tracking branch 'origin/feature/subprojects-for-monorepo…
uriahrokach Aug 16, 2026
3d9465d
feat(core): add sub_project columns and cicd_deployment_subprojects t…
yotams123 Aug 27, 2026
5388ec3
fix(gitlab): extract MR commits before converting them
yotams123 Aug 27, 2026
c6d911c
refactor(monorepo): split attribution from metrics, add includeUnattr…
yotams123 Aug 27, 2026
be9a509
feat(config-ui): reject reserved monorepo sub-project names
yotams123 Aug 27, 2026
8081600
feat(dashboards): migrate monorepo-subprojects dashboard to core tables
yotams123 Aug 27, 2026
c708d16
feat(dashboards): add sub_project grouping to single-metric PR time s…
yotams123 Aug 27, 2026
aa50a8d
Merge remote-tracking branch 'upstream/main' into feature/subprojects…
yotams123 Aug 28, 2026
71cba6b
fix(monorepo): freeze migration script models to satisfy migration li…
yotams123 Aug 31, 2026
0dd4a03
fix(monorepo): suppress staticcheck SA1019 for intentional deprecated…
yotams123 Aug 31, 2026
9d12cb2
test: register monorepo plugin in table-info and schema-drift test su…
yotams123 Aug 31, 2026
6b9fa20
fix(migrations): use CURRENT_TIMESTAMP for cicd_deployment_subproject…
yotams123 Aug 31, 2026
3856a46
Merge branch 'main' into feature/subprojects-for-monorepo
yotams123 Aug 31, 2026
61e96c5
fix(dashboards): join pull_request_commits on full PK in PR Details p…
yotams123 Aug 31, 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
3 changes: 3 additions & 0 deletions backend/core/models/domainlayer/code/pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type PullRequest struct {
Additions int
Deletions int
IsDraft bool
// SubProject is the monorepo sub-project this pull request was attributed to by the
// monorepo plugin, or empty/NULL when the project has no monorepo configuration.
SubProject string `gorm:"index;type:varchar(100)"`
}

func (PullRequest) TableName() string {
Expand Down
4 changes: 4 additions & 0 deletions backend/core/models/domainlayer/code/pull_request_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type PullRequestCommit struct {
CommitAuthorEmail string `gorm:"type:varchar(255)"`
CommitAuthoredDate time.Time
common.NoPKModel
// SubProject mirrors the owning pull request's SubProject (see code.PullRequest), kept
// denormalized here so commit-level dashboards can group without joining back to
// pull_requests.
SubProject string `gorm:"index;type:varchar(100)"`
}

func (PullRequestCommit) TableName() string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ type ProjectPrMetric struct {
PrCreatedDate *time.Time
PrMergedDate *time.Time
PrDeployedDate *time.Time

// SubProject mirrors pull_requests.sub_project, tagged by the monorepo plugin's
// updateProjectPrMetricsSubProject subtask after DORA computes this row. Empty/NULL
// when the project has no monorepo configuration.
SubProject string `gorm:"index;type:varchar(100)"`
}

func (ProjectPrMetric) TableName() string {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
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 devops

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

// CicdDeploymentSubproject maps a deployment (identified by its pipeline id,
// CicdDeploymentId) to the monorepo sub-project(s) it deployed. The relationship is
// many-to-many: a single pipeline can run the deploy jobs of several sub-projects, in
// which case it produces one row per sub-project rather than a delimited value, so
// dashboards can `GROUP BY sub_project` without double counting.
//
// Rows are written by the monorepo plugin's attributeDeployments subtask. Projects
// without monorepo configuration have no rows here at all; dashboards should treat a
// missing mapping as the single, implicit "All" group via COALESCE(sub_project, 'All').
type CicdDeploymentSubproject struct {
common.NoPKModel
ProjectName string `gorm:"primaryKey;type:varchar(100)"`
// CicdDeploymentId is the pipeline id, matching cicd_deployment_commits.cicd_deployment_id.
// It also carries its own secondary index (idx_cds_deployment) because it sits in the
// middle of the composite primary key, so dashboards filtering by deployment id alone
// cannot use the primary key's leftmost prefix.
CicdDeploymentId string `gorm:"primaryKey;type:varchar(255);index:idx_cds_deployment"`
SubProject string `gorm:"primaryKey;type:varchar(100)"`
}

func (CicdDeploymentSubproject) TableName() string {
return "cicd_deployment_subprojects"
}
1 change: 1 addition & 0 deletions backend/core/models/domainlayer/domaininfo/domaininfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func GetDomainTablesInfo() []dal.Tabler {
&devops.CICDPipeline{},
&devops.CICDTask{},
&devops.CicdDeploymentCommit{},
&devops.CicdDeploymentSubproject{},
&devops.CiCDPipelineCommit{},
&devops.CicdScope{},
&devops.CICDDeployment{},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
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/core/models/migrationscripts/archived"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/migrationhelper"
)

var _ plugin.MigrationScript = (*addCicdDeploymentSubprojects)(nil)

// cicdDeploymentSubproject20260810 is a version-frozen snapshot of
// devops.CicdDeploymentSubproject as it looked when this migration was written.
// Migration scripts must not import live model packages (see core/migration/linter),
// so the shape is duplicated here on purpose rather than imported from the domain layer.
type cicdDeploymentSubproject20260810 struct {
archived.NoPKModel
ProjectName string `gorm:"primaryKey;type:varchar(100)"`
CicdDeploymentId string `gorm:"primaryKey;type:varchar(255);index:idx_cds_deployment"`
SubProject string `gorm:"primaryKey;type:varchar(100)"`
}

func (cicdDeploymentSubproject20260810) TableName() string {
return "cicd_deployment_subprojects"
}

type addCicdDeploymentSubprojects struct{}

// Up creates the new cicd_deployment_subprojects mapping table.
func (script *addCicdDeploymentSubprojects) Up(basicRes context.BasicRes) errors.Error {
return migrationhelper.AutoMigrateTables(
basicRes,
&cicdDeploymentSubproject20260810{},
)
}

func (*addCicdDeploymentSubprojects) Version() uint64 {
return 20260810100100
}

func (*addCicdDeploymentSubprojects) Name() string {
return "create cicd_deployment_subprojects mapping table for monorepo support"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
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/core/plugin"
)

var _ plugin.MigrationScript = (*addSubProjectToPrAndMetrics)(nil)

// pullRequest20260810 adds the sub_project column that the monorepo plugin's
// attributePullRequests subtask writes. Nullable, so single-repo projects (and rows not
// yet processed) simply read as NULL and dashboards fall back to the "All" group.
type pullRequest20260810 struct {
SubProject string `gorm:"index;type:varchar(100)"`
}

func (pullRequest20260810) TableName() string {
return "pull_requests"
}

// pullRequestCommit20260810 mirrors the owning pull request's sub_project.
type pullRequestCommit20260810 struct {
SubProject string `gorm:"index;type:varchar(100)"`
}

func (pullRequestCommit20260810) TableName() string {
return "pull_request_commits"
}

// projectPrMetric20260810 is tagged by the monorepo plugin's
// updateProjectPrMetricsSubProject subtask after DORA computes this row.
type projectPrMetric20260810 struct {
SubProject string `gorm:"index;type:varchar(100)"`
}

func (projectPrMetric20260810) TableName() string {
return "project_pr_metrics"
}

type addSubProjectToPrAndMetrics struct{}

func (script *addSubProjectToPrAndMetrics) Up(basicRes context.BasicRes) errors.Error {
db := basicRes.GetDal()
if err := db.AutoMigrate(&pullRequest20260810{}); err != nil {
return err
}
if err := db.AutoMigrate(&pullRequestCommit20260810{}); err != nil {
return err
}
if err := db.AutoMigrate(&projectPrMetric20260810{}); err != nil {
return err
}
return nil
}

func (*addSubProjectToPrAndMetrics) Version() uint64 {
return 20260810100000
}

func (*addSubProjectToPrAndMetrics) Name() string {
return "add sub_project to pull_requests, pull_request_commits and project_pr_metrics for monorepo support"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
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/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
)

var _ plugin.MigrationScript = (*backfillSubProjectFromMonorepo)(nil)

type backfillSubProjectFromMonorepo struct{}

// Up copies sub_project values that existing monorepo-plugin installs already computed
// into the new core columns/table, so projects that were using the monorepo plugin before
// this release keep their classification instead of reverting to NULL ("All").
//
// Every statement is written with a correlated subquery / NOT EXISTS guard rather than the
// MySQL-only `UPDATE ... JOIN` or Postgres-only `UPDATE ... FROM` forms, so the same SQL
// runs unmodified on both of DevLake's supported databases. Each statement only touches
// rows it has not already touched (sub_project IS NULL / NOT EXISTS), which makes the
// whole migration idempotent and safe to re-run if it is interrupted partway through - the
// batching called out as a risk in the design doc was judged unnecessary for a first
// implementation given that guard, but would be a reasonable follow-up for very large
// instances (see the design doc's risk table).
//
// This is a core migration, so it runs on every DevLake install, including ones that have
// never enabled the monorepo plugin. On those installs the plugin's own tables
// (monorepo_subproject_pr_metrics / monorepo_subproject_deployments) do not exist, so each
// half of the backfill is skipped independently when its source table is absent, rather
// than breaking the migration for every non-monorepo user.
func (script *backfillSubProjectFromMonorepo) Up(basicRes context.BasicRes) errors.Error {
db := basicRes.GetDal()

if db.HasTable("monorepo_subproject_pr_metrics") {
if err := backfillPrSubProjects(db); err != nil {
return err
}
}
if db.HasTable("monorepo_subproject_deployments") {
if err := backfillDeploymentSubProjects(db); err != nil {
return err
}
}
return nil
}

func backfillPrSubProjects(db dal.Dal) errors.Error {
// 1. pull_requests.sub_project <- monorepo_subproject_pr_metrics.sub_project
if err := db.Exec(`
UPDATE pull_requests
SET sub_project = (
SELECT m.sub_project FROM monorepo_subproject_pr_metrics m
WHERE m.pull_request_id = pull_requests.id
)
WHERE sub_project IS NULL
AND EXISTS (
SELECT 1 FROM monorepo_subproject_pr_metrics m2
WHERE m2.pull_request_id = pull_requests.id
)
`); err != nil {
return errors.Default.Wrap(err, "error backfilling pull_requests.sub_project")
}

// 2. pull_request_commits.sub_project <- pull_requests.sub_project
if err := db.Exec(`
UPDATE pull_request_commits
SET sub_project = (
SELECT pr.sub_project FROM pull_requests pr
WHERE pr.id = pull_request_commits.pull_request_id
)
WHERE sub_project IS NULL
AND EXISTS (
SELECT 1 FROM pull_requests pr2
WHERE pr2.id = pull_request_commits.pull_request_id
AND pr2.sub_project IS NOT NULL
)
`); err != nil {
return errors.Default.Wrap(err, "error backfilling pull_request_commits.sub_project")
}

// 3. project_pr_metrics.sub_project <- pull_requests.sub_project
if err := db.Exec(`
UPDATE project_pr_metrics
SET sub_project = (
SELECT pr.sub_project FROM pull_requests pr
WHERE pr.id = project_pr_metrics.id
)
WHERE sub_project IS NULL
AND EXISTS (
SELECT 1 FROM pull_requests pr2
WHERE pr2.id = project_pr_metrics.id
AND pr2.sub_project IS NOT NULL
)
`); err != nil {
return errors.Default.Wrap(err, "error backfilling project_pr_metrics.sub_project")
}

return nil
}

func backfillDeploymentSubProjects(db dal.Dal) errors.Error {
// created_at/updated_at are set via CURRENT_TIMESTAMP rather than a bound
// time.Time parameter: when a parameter appears only in a bare SELECT list
// (not compared against a typed column), Postgres's extended query protocol
// can't infer its type and defaults it to text, which then fails to insert
// into the timestamptz column with "column is of type timestamp with time
// zone but expression is of type text". CURRENT_TIMESTAMP is standard SQL
// supported identically by MySQL and Postgres, so it sidesteps the
// parameter-typing issue entirely without needing a dialect branch.
if err := db.Exec(`
INSERT INTO cicd_deployment_subprojects (project_name, cicd_deployment_id, sub_project, created_at, updated_at)
SELECT DISTINCT d.project_name, d.cicd_deployment_id, d.sub_project, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM monorepo_subproject_deployments d
WHERE NOT EXISTS (
SELECT 1 FROM cicd_deployment_subprojects x
WHERE x.project_name = d.project_name
AND x.cicd_deployment_id = d.cicd_deployment_id
AND x.sub_project = d.sub_project
)
`); err != nil {
return errors.Default.Wrap(err, "error backfilling cicd_deployment_subprojects")
}
return nil
}

func (*backfillSubProjectFromMonorepo) Version() uint64 {
return 20260810100200
}

func (*backfillSubProjectFromMonorepo) Name() string {
return "backfill sub_project into core tables from existing monorepo plugin data"
}
3 changes: 3 additions & 0 deletions backend/core/models/migrationscripts/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ func All() []plugin.MigrationScript {
new(addCqProjectMetricsHistory),
new(addIsBotToAccounts),
new(addSprintVelocityFields),
new(addSubProjectToPrAndMetrics),
new(addCicdDeploymentSubprojects),
new(backfillSubProjectFromMonorepo),
new(addBlueprintIdIndexToPipelines),
new(expandDomainTextColumns),
}
Expand Down
Loading
Loading