Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,49 @@
/*
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/domainlayer/devops"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/migrationhelper"
)

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

type addCicdDeploymentSubprojects struct{}

// Up creates the new cicd_deployment_subprojects mapping table. This is a brand new
// table (not an existing one gaining a column), so it is migrated straight from the live
// domain model, matching the precedent set by the monorepo plugin's own
// 20260809_add_init_tables.go rather than a versioned snapshot struct.
func (script *addCicdDeploymentSubprojects) Up(basicRes context.BasicRes) errors.Error {
return migrationhelper.AutoMigrateTables(
basicRes,
&devops.CicdDeploymentSubproject{},
)
}

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,145 @@
/*
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 (
"time"

"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 {
now := time.Now()
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, ?, ?
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
)
`, now, now); 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
5 changes: 4 additions & 1 deletion backend/plugins/gitlab/tasks/mr_commit_convertor.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ var ConvertApiMrCommitsMeta = plugin.SubTaskMeta{
EnabledByDefault: true,
Description: "Add domain layer PullRequestCommit according to GitlabMrCommit",
DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_REVIEW},
Dependencies: []*plugin.SubTaskMeta{&ConvertApiMergeRequestsMeta},
// ExtractApiMrCommitsMeta must run first: it populates _tool_gitlab_mr_commits, which
// this subtask reads from. Without this dependency, conversion can race ahead of
// extraction and silently convert zero or partial commits.
Dependencies: []*plugin.SubTaskMeta{&ConvertApiMergeRequestsMeta, &ExtractApiMrCommitsMeta},
}

func ConvertApiMergeRequestsCommits(subtaskCtx plugin.SubTaskContext) errors.Error {
Expand Down
Loading