diff --git a/docs/DORA.md b/docs/DORA.md
index d828d3388c5..49308324bb8 100644
--- a/docs/DORA.md
+++ b/docs/DORA.md
@@ -206,6 +206,17 @@ If you are not familiar with Grafana, please refer to our [Grafana doc](Configur
+## DORA for a monorepo
+
+DevLake measures DORA at the project level, and a project's scope is a repository. If a
+single repository hosts several logically separate projects — each with its own deploy job
+and its own PR label — all of them collapse into one set of DORA numbers.
+
+The [monorepo plugin](Plugins/monorepo.md) splits such a repository into sub-projects and
+computes per-sub-project Deployment Frequency and Lead Time for Changes.
+
+
+
## Try it Out
To create the DORA dashboard with your own toolchain, please look at the [configuration tutorial](Configuration/Tutorial.md) and [project organization documentation](Configuration/HowToOrganizeDevlakeProjects.md) for more details.
diff --git a/docs/DataModels/DevLakeDomainLayerSchema.md b/docs/DataModels/DevLakeDomainLayerSchema.md
index 4676005c75f..7f6cda28cb9 100644
--- a/docs/DataModels/DevLakeDomainLayerSchema.md
+++ b/docs/DataModels/DevLakeDomainLayerSchema.md
@@ -465,6 +465,7 @@ Pull requests are the abstraction of GitHub pull requests, GitLab merge requests
| `head_ref` | varchar | 255 | The branch name in the head repo that contains the changes that will be added to the base | |
| `base_commit_sha` | char | 40 | The base commit of this PR. | |
| `head_commit_sha` | char | 40 | The head commit of this PR. | |
+| `sub_project` | varchar | 100 | The monorepo sub-project this PR belongs to, matched by label. Written by the [monorepo plugin](../Plugins/monorepo.md); NULL when the project has no monorepo configuration. | |
#### pull_request_labels
@@ -489,6 +490,7 @@ The list is additive. This means if a rebase with commit squashing takes place a
| `commit_author_name` | varchar | 255 | The name of the person who authored the commit | |
| `commit_author_email` | varchar | 255 | The email address of the person who authored the commit. | |
| `commit_authored_date` | varchar | 255 | The date and time when the commit was authored. | |
+| `sub_project` | varchar | 100 | Mirrors the owning pull request's `sub_project`. | |
#### pull_request_comments
@@ -656,6 +658,20 @@ Normally, one deployment only deploy to one repo. But in some cases, one deploym
| `repo_url` | varchar | 191 | The url of the repo | |
| `prev_success_deployment_commit_id` | varchar | 255 | The last successful deployment_commit_id before this one in the same `cicd_scope`, `repo` and `environment`, which is used to calculate the new commits deployed by this deployment, thereby measuring [DORA - change lead time](../Metrics/LeadTimeForChanges.md). | |
+#### cicd_deployment_subprojects
+
+Maps a deployment to the monorepo sub-project(s) it deployed. Rows are written by the [monorepo plugin](../Plugins/monorepo.md), which matches the name of the deploying CI job against each sub-project's configured pattern.
+
+The relationship is many-to-many: a pipeline that runs the deploy jobs of several sub-projects produces one row per sub-project, so dashboards can `GROUP BY sub_project` without double counting.
+
+Projects without monorepo configuration have no rows in this table at all; dashboards treat a missing mapping as the single, implicit `All` group.
+
+| **field** | **type** | **length** | **description** | **key** |
+|:---------------------|:---------|:-----------|:------------------------------------------------------------------------------------------|:--------|
+| `project_name` | varchar | 100 | The project this mapping belongs to | PK |
+| `cicd_deployment_id` | varchar | 255 | The deployment, matching [table.cicd_deployment_commits](#cicd_deployment_commits).`cicd_deployment_id` | PK |
+| `sub_project` | varchar | 100 | The sub-project this deployment deployed | PK |
+
### Domain 5 - Code Quality
The names of tables in the 'Code Quality' domain will start with a prefix cq\_
@@ -929,6 +945,7 @@ metrics, such as _'No. of Issue closed by contributor', 'No. of commits by contr
| `deployment_id` | longtext | | The id of cicd_task which deploy the commits of this PR | |
| `pr_deploy_time` | bigint | | The time it takes from when a PR is merged to when it is deployed | |
| `pr_cycle_time` | bigint | | The total time from the first commit to when the PR is deployed | |
+| `sub_project` | varchar | 100 | The monorepo sub-project of this PR, copied from [table.pull_requests](#pull_requests) | |
#### project_issue_metrics
diff --git a/docs/Plugins/monorepo.md b/docs/Plugins/monorepo.md
new file mode 100644
index 00000000000..82b02748999
--- /dev/null
+++ b/docs/Plugins/monorepo.md
@@ -0,0 +1,147 @@
+---
+title: "Monorepo"
+description: >
+ Monorepo Plugin
+---
+
+## Summary
+
+DevLake's scope model is one scope per repository, so a repository that hosts several
+logically separate projects collapses into a single set of DORA numbers. The `monorepo`
+plugin splits such a repository into **sub-projects** and computes per-sub-project
+delivery metrics.
+
+Like [DORA](../DORA.md) and [Linker](./linker.md), it is a metric plugin: it works on the
+domain layer after data collection, and runs after `dora`.
+
+It attributes:
+
+- **Deployments** to a sub-project by matching the name of the CI job that performed the
+ deployment against a regular expression.
+- **Pull requests** to a sub-project by matching the PR's labels.
+
+From those two attributions it produces per-sub-project
+[Deployment Frequency](../Metrics/DeploymentFrequency.md) and
+[Lead Time for Changes](../Metrics/LeadTimeForChanges.md).
+
+[Change Failure Rate](../Metrics/CFR.md) and
+[Median Time to Restore Service](../Metrics/MTTR.md) are **not** computed per sub-project,
+because they require incident data and incidents are not attributed to sub-projects.
+
+## Supported Versions
+
+The monorepo plugin was added after the `v1.0.3-beta18` release and is available in all
+later releases.
+
+## Configuration
+
+The plugin is configured per project, on the project's settings page.
+
+1. Open **Projects** → your project → **Settings**.
+2. Tick **Enable Monorepo Sub-Projects**.
+3. Add one row per sub-project and fill in the three fields below.
+4. Click **Save**, then re-run the project's blueprint.
+
+| Field | Description | Example |
+|:--------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|:---------------------|
+| **Sub-project name** | Identifies the sub-project in the output tables and dashboards. Must be unique within the project. | `serviceA` |
+| **Labels** | Comma-separated pull request labels that mark a PR as belonging to this sub-project. Matching is **exact and case-sensitive**. | `serviceA,svc-a` |
+| **Deploy job pattern** | A [Go regular expression](https://pkg.go.dev/regexp/syntax) matched against `cicd_tasks.name` to recognise this sub-project's deploy jobs. | `^deploy-serviceA$` |
+
+`unattributed` and `All` are reserved and cannot be used as sub-project names.
+
+The same options can be set through the API or the Config UI's
+[advanced mode](../Configuration/AdvancedMode.md) as the project's `monorepo` metric
+plugin option:
+
+```json
+{
+ "pluginName": "monorepo",
+ "enable": true,
+ "pluginOption": {
+ "subProjects": [
+ {
+ "name": "serviceA",
+ "prLabels": ["serviceA", "svc-a"],
+ "deployJobPattern": "^deploy-serviceA$"
+ },
+ {
+ "name": "serviceB",
+ "prLabels": ["serviceB"],
+ "deployJobPattern": "^deploy-serviceB$"
+ }
+ ],
+ "includeUnattributed": true
+ }
+}
+```
+
+`includeUnattributed` (default `true`) controls what happens to pull requests and
+deployments that belong to the project but match none of the configured sub-projects:
+with `true` they are recorded under the sentinel sub-project `unattributed`, so you can
+see how much of the repository is not yet classified; with `false` they are left
+unclassified.
+
+## How attribution works
+
+### Deployments
+
+Every deployment of the project is joined to the CI/CD tasks of the pipeline that produced
+it, and each task of type `Deployment` has its name matched against each sub-project's
+`deployJobPattern`.
+
+- A pipeline that runs the deploy jobs of several sub-projects produces **one row per
+ sub-project**, so dashboards can `GROUP BY sub_project` without double counting.
+- Deployments imported directly from a provider's deployment API (rather than generated
+ from a pipeline) carry no job name and cannot be attributed.
+
+### Pull requests
+
+All pull requests of the project are attributed — open, closed and merged — by label.
+A pull request belongs to exactly one sub-project: when it carries the labels of more than
+one, the **first matching sub-project in the configured order wins**, so order the list
+from most to least specific.
+
+PR coding, pickup, review, deploy and cycle time are not recomputed by this plugin; they
+are taken from DORA's own `project_pr_metrics` rows and tagged with the sub-project.
+
+### Mismatch warnings
+
+After tagging, the plugin cross-checks each PR's label-derived sub-project against the
+sub-project of the deployment that DORA says actually shipped it. Disagreements are logged
+as warnings and never fail the pipeline — labels always win. They usually point at a
+`prLabels` or `deployJobPattern` entry that needs fixing, though a pipeline deploying
+several sub-projects at once will produce such warnings legitimately.
+
+## Output
+
+| Table | Description |
+|:------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------|
+| [`cicd_deployment_subprojects`](../DataModels/DevLakeDomainLayerSchema.md#cicd_deployment_subprojects) | Maps each deployment to the sub-project(s) it deployed. |
+| [`pull_requests.sub_project`](../DataModels/DevLakeDomainLayerSchema.md#pull_requests) | The sub-project a pull request belongs to. |
+| [`pull_request_commits.sub_project`](../DataModels/DevLakeDomainLayerSchema.md#pull_request_commits) | Mirrors the owning pull request's sub-project. |
+| [`project_pr_metrics.sub_project`](../DataModels/DevLakeDomainLayerSchema.md#project_pr_metrics) | Tags DORA's per-PR metrics with the sub-project. |
+
+A `NULL` `sub_project` means the row was never classified — single-repo projects, or rows
+the plugin has not processed. Dashboards render these as the single implicit group `All`
+via `COALESCE(sub_project, 'All')`.
+
+Attribution is rebuilt from scratch on every run, so changing the configuration and
+re-running the blueprint reclassifies existing data.
+
+:::note
+The plugin also populates `monorepo_subproject_deployments` and
+`monorepo_subproject_pr_metrics`. Both are deprecated and will be removed in a future
+release — query the tables above instead.
+:::
+
+## Dashboard
+
+A built-in Grafana dashboard, **Monorepo Sub-Projects**, visualises the output:
+
+- Deployment Count by Sub-Project
+- Deployment Frequency over Time
+- Change Lead Time Breakdown by Sub-Project
+- Unattributed PRs
+- Awaiting Deployment
+- Attributed Deployments