Skip to content

Commit cec4286

Browse files
feat: add Linear (linear.app) data source plugin (#8900)
* feat(linear): add tool-layer models and init migration Add the Linear plugin's tool-layer data models (connection, team scope, scope config, account, issue, comment, issue label, workflow state, cycle, issue history) and the initial schema migration with archived snapshots. The connection authenticates with a personal API key passed verbatim in the Authorization header (Linear uses no Bearer prefix). Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): add plugin skeleton, connection API and GraphQL client Wire the Linear plugin entry point and implement all required plugin interfaces (meta, init, task, api, model, source, migration, blueprint v200, closeable). Add connection/scope/scope-config CRUD via the data-source helper, a test-connection endpoint that runs a GraphQL viewer query, and a rate-limited async GraphQL client that injects the API key via a bare Authorization header. SubTaskMetas is intentionally empty; collectors are added per entity in following commits. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): collect, extract and convert users to accounts Add the users GraphQL collector (paginated), extractor to _tool_linear_accounts, and convertor to the domain crossdomain.Account table, wired as the first three subtasks. Includes an e2e dataflow test with raw fixtures and verified snapshots. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): collect and extract workflow states Add the team-scoped workflow states GraphQL collector and extractor into _tool_linear_workflow_states. These states (backlog/unstarted/started/ completed/canceled) drive deterministic issue status mapping. Includes an e2e test covering all five state types. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): collect, extract and convert issues Add the team-scoped issues GraphQL collector (incremental via updatedAt ordering, inline labels), extractor to _tool_linear_issues and _tool_linear_issue_labels, and convertor to domain ticket.Issue and ticket.BoardIssue. Status maps deterministically from Linear's WorkflowState.type (backlog/unstarted->TODO, started->IN_PROGRESS, completed/canceled->DONE); priority maps to its label; lead time falls back to resolution minus creation. Includes an e2e test spanning all state types, unassigned issues, issues without a cycle, and multi-label issues. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): collect, extract and convert issue comments Add a per-issue comments GraphQL collector (driven by an input iterator over collected issues, with pagination), an extractor that recovers the owning issue id from the raw input column, and a convertor to domain ticket.IssueComment. Includes an e2e dataflow test. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): convert issue labels to domain layer Add the convertor from _tool_linear_issue_labels (populated inline by the issue extractor) into the domain ticket.IssueLabel table. Includes an e2e test covering issues with multiple labels and with none. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): collect cycles and convert to sprints Add the team-scoped cycles GraphQL collector and extractor, plus convertors producing domain ticket.Sprint and ticket.BoardSprint (status derived from completedAt), and ticket.SprintIssue linking issues to their cycle. Includes an e2e dataflow test covering closed/active cycles and issues with/without a cycle. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): collect issue history and convert to changelogs Add a per-issue history GraphQL collector (input iterator over issues, with pagination), an extractor capturing state transitions including state types, and a convertor to domain ticket.IssueChangelogs with mapped from/to status values. Lead time is already derived from the issue's native startedAt/completedAt. Includes an e2e test of a full backlog->started->completed lifecycle. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * test(linear): add blueprint v200 scope generation tests Cover makeScopesV200: a team scope with the ticket entity produces the expected domain board scope id, and a scope without the ticket entity produces none. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * docs(linear): add plugin README Document the Linear plugin: supported entities, tool/domain mapping tables, deterministic status mapping, priority/type/lead-time handling, API-key auth, connection/scope/pipeline setup examples, rate limiting, and the roadmap (OAuth, label-based type mapping, config-ui integration). Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): guard lead-time fallback against resolution before creation A resolution timestamp (completedAt/canceledAt) earlier than createdAt — from clock skew or migrated/imported issues — produced a negative duration that, cast to uint, yields platform-dependent garbage (0 on arm64, ~1.8e19 on amd64). Skip the fallback unless the resolution is after creation so lead time stays unset instead. Adds an isolated e2e dataflow test with a fixture whose canceledAt precedes createdAt, asserting lead_time_minutes is empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): map Linear triage state type to TODO The WorkflowState.type 'triage' (the inbox state issues land in before being accepted) previously fell through to OTHER, contradicting the documented total mapping and silently mislabeling triage issues. Map it to TODO; keep OTHER as the fallback for genuinely unrecognized types so unexpected API values surface. Adds a unit test covering every documented state type plus triage and an unknown value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * refactor(linear): remove unused GraphqlInlineAccount struct The struct was documented as the shared inline-user shape but was never referenced; each collector declares its own inline user struct. Removing it avoids misleading a maintainer into editing a type nothing reads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * perf(linear): raise issue collector page size to 100 Every other Linear collector uses a page size of 100; issues used 50, which doubled the number of issue-page round-trips and the iterator size that drives the per-issue comment/history collectors. Linear permits first: 250. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): populate issue assignee/creator names and issue_assignees The issue convertor set only assignee_id/creator_id, leaving the denormalized assignee_name/creator_name columns blank and writing no issue_assignees rows, so dashboards reading those columns or joining through issue_assignees showed blank names. Preload account display names (matching the account convertor's displayName-then-name rule) and emit an IssueAssignee per assigned issue. The issue dataflow test now loads accounts before conversion and asserts the names plus issue_assignees; the lead-time test flushes accounts to stay order-independent on the shared test DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): clear stale sprint_issues when issues leave their cycle Sprint membership is derived from each issue's cycle_id, and the batch divider only deletes outdated rows when it produces at least one row of the type. When every issue is moved out of its cycle the convertor emits nothing, so the divider never fires and prior sprint_issues rows linger, leaving issues shown in sprints they no longer belong to. Delete the team's sprint_issues up front so the result is correct regardless of how many issues remain in a cycle. Adds a two-run e2e test that empties every issue's cycle and asserts sprint_issues is empty afterward. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): derive issue lead time from state-transition history The LinearIssue.LeadTimeMinutes field was never populated, so lead time always fell back to the coarse createdAt -> resolutionDate span. Derive it instead from the recorded history: the span from an issue's first transition into an in-progress state to its first transition into a done state thereafter (active cycle time), which is the value that genuinely requires history. ConvertIssues still seeds the fallback; ConvertIssueHistory now overrides it when the transitions exist, and issues lacking them keep the fallback. Adds an e2e test asserting issue-1 (started 05-02, completed 05-03) resolves to 1440 minutes from history rather than its 2880-minute created->resolved span. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): add remote-scopes endpoints to enumerate teams Other first-party ticket plugins (jira, asana, github) expose connections/:connectionId/remote-scopes so the config UI can browse and select scopes from the API. Linear had none, forcing users to hand-craft a PUT /scopes with raw team UUIDs they had no in-product way to discover. Wire the standard DsRemoteApiProxyHelper + DsRemoteApiScopeListHelper and a lister that queries the GraphQL teams connection (flat list, cursor-paginated) through the connection's authenticated client. Adds unit tests for the response->scope-entry mapping, the pagination cursor, and the route registration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * perf(linear): make comment and history collection incremental Both child collectors used a plain GraphqlCollector and swept every issue in the team on every run, issuing one request per issue with no since filter - tens of thousands of requests per run on a large team against Linear's ~1500 req/hour budget. Switch them to a stateful collector and restrict the driving cursor to issues updated since the last successful collection, so steady-state runs scale with the change delta rather than the whole backlog. A full sync (since == nil) still sweeps every issue. Adds a unit test for the incremental cursor-clause builder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): filter issues server-side by updatedAt for incremental sync Incremental collection relied on the issues query returning newest-first and a client-side early-stop, but the query pinned no sort direction (Linear's orderBy is a scalar enum with no direction operand). If the server default were ascending, the early-stop would fire on the first (oldest) row and collect almost nothing. Pass a server-side IssueFilter { updatedAt: { gt: since } } instead and drop the early-stop, so correctness no longer depends on an undocumented default ordering. A full sync passes an empty filter (match all). Adds a unit test pinning the filter's JSON shape to Linear's IssueFilter input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * refactor(linear): drop dead LeadTimeMinutes tool-layer field The _tool_linear_issues.lead_time_minutes column was never populated (the collector never requested it and no extractor set it). Now that lead time is derived into the domain ticket.Issue directly -- from state-transition history when available, otherwise the createdAt->resolutionDate fallback in the issue convertor -- the tool-layer field is pure dead weight. Remove it from the model, the init migration's archived model, the convertor, and the extractor snapshot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(config-ui): register Linear plugin Adds the Linear data source to config-ui so it appears in the connection picker: connection form (endpoint + personal API key + proxy + rate limit), a flat Teams data-scope backed by the plugin's remote-scopes endpoint, and the Linear logo. No scope-config transformation — Linear's status mapping is deterministic. Wired into the plugin registry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(config-ui): map Linear scope id to teamId getPluginScopeId fell through to the default (scope.id) for Linear, but a LinearTeam scope is keyed by teamId and has no id field — so the blueprint referenced an undefined scopeId and patching failed with 'LinearTeam not found'. Add a linear case returning scope.teamId. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): add Grafana dashboard Adds grafana/dashboards/Linear.json (cloned from the Asana ticket-dashboard template) so Linear ships a per-tool dashboard like every other ticket plugin. Its board picker is scoped to Linear (boards id like 'linear%'); the 13 panels (throughput, lead/cycle time, status distribution, delivery rate, sprints) read the shared domain tables. Auto-loaded via Grafana file provisioning. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): convert team scope to a domain board board_issues and sprint_issues referenced a board_id (boardIdGen over LinearTeam), but nothing ever created the ticket.Board row itself, so the domain boards table stayed empty. Board-scoped dashboards (whose board picker is 'boards where id like linear%') and any board join therefore returned no data. Add a ConvertTeams subtask that converts the team scope in _tool_linear_teams into a ticket.Board keyed identically to those references. Adds an e2e test asserting the board is produced with the matching id. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): widen issue title/url columns to avoid truncation _tool_linear_issues.title and .url were varchar(255), but Linear titles can exceed 255 chars (and the issue URL embeds a title slug), so extraction failed with 'Error 1406: Data too long for column title'. Drop the varchar limit so both are longtext, matching the domain issues.title and jira's tool summary. Adds an e2e test extracting a 300-char title without truncation. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * fix(linear): recover owning issue id for comments and history The GraphQL collector stores the query variables (which carry issueId) in the raw row's input column, but the comment and history extractors parsed it as {"Id":...} (SimpleLinearIssue.Id), so the owning issue id came out empty and the convertor joins produced zero domain comments/changelogs on real data. The e2e fixtures hand-wrote {"Id":...}, masking it. Parse issueId (with an Id fallback) and update the fixtures to the real collector shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * test: flush LinearIssueLabel before issue extraction in e2e tests The issue extractor writes both _tool_linear_issues and _tool_linear_issue_labels, but comment_test, cycle_test and issue_history_test only flushed LinearIssue before running ExtractIssuesMeta. On a clean database (as in CI) the table _tool_linear_issue_labels was never auto-migrated, so the extractor's DELETE on that table panicked and aborted the whole package. Flush LinearIssueLabel too, matching the other linear e2e tests and the jira plugin convention. Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * test: register linear plugin in Test_GetPluginTablesInfo The plugin count check failed in CI (actual 41 vs tested 40) because the linear plugin was not listed in table_info_test.go. Add its import and FeedIn call so every Go plugin is covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> * feat(linear): map issues to ticket types via label-based scope config Linear has no native issue type, so the issue convertor previously hardcoded every issue to REQUIREMENT. Add optional regex fields to the Linear scope config (issueTypeIncident/Bug/Requirement) matched against an issue's label names, with precedence INCIDENT > BUG > REQUIREMENT and a REQUIREMENT default when nothing matches. This lets DORA classify Linear bugs as incidents for change-failure-rate / time-to-restore. Also fix two gaps that made scope configs unusable for Linear: - thread the scope's ScopeConfigId into the pipeline task options so the convertor can load the config at runtime - register the standard scope-config/:scopeConfigId/projects route that every other plugin serves (config-ui 404'd on the scope config page) Tested: new e2e TestLinearIssueIncidentMapping (Bug label -> INCIDENT, other issues REQUIREMENT) plus a blueprint plan test asserting ScopeConfigId is passed through; full plugins/linear suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> --------- Signed-off-by: Eduardo Rodrigues <2961314+eduardoarantes@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 65cbe82 commit cec4286

102 files changed

Lines changed: 6851 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/plugins/linear/README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
# Linear
19+
20+
## Summary
21+
22+
This plugin collects data from [Linear](https://linear.app) through its
23+
[GraphQL API](https://linear.app/developers/graphql) and maps it into DevLake's
24+
standardized `ticket` domain, so Linear issues appear in DevLake dashboards
25+
(throughput, lead/cycle time, sprint burndown, etc.).
26+
27+
The selectable **scope** is a Linear **Team**, which maps to a domain `Board`.
28+
29+
## Supported data
30+
31+
| Linear entity | Tool-layer table | Domain-layer table |
32+
|-----------------|-----------------------------------|--------------------------------------------|
33+
| Team | `_tool_linear_teams` (scope) | `boards` |
34+
| User | `_tool_linear_accounts` | `accounts` |
35+
| Workflow state | `_tool_linear_workflow_states` | (drives issue status mapping) |
36+
| Issue | `_tool_linear_issues` | `issues`, `board_issues` |
37+
| Label | `_tool_linear_issue_labels` | `issue_labels` |
38+
| Comment | `_tool_linear_comments` | `issue_comments` |
39+
| Cycle | `_tool_linear_cycles` | `sprints`, `board_sprints`, `sprint_issues`|
40+
| Issue history | `_tool_linear_issue_history` | `issue_changelogs` |
41+
42+
### Field mapping highlights
43+
44+
- **Status** — derived deterministically from Linear's `WorkflowState.type`
45+
(no manual mapping needed, unlike Jira):
46+
- `backlog`, `unstarted``TODO`
47+
- `started``IN_PROGRESS`
48+
- `completed`, `canceled``DONE`
49+
- **Priority** — Linear's integer priority maps to a label: `0` No priority,
50+
`1` Urgent, `2` High, `3` Medium, `4` Low.
51+
- **Type** — Linear has no native issue type, so issues default to `REQUIREMENT`.
52+
- **Lead time**`completedAt − createdAt` (Linear provides `startedAt`/`completedAt`
53+
natively; the history changelog captures every status transition).
54+
- **Story points** — Linear's `estimate`.
55+
56+
## Authentication
57+
58+
The plugin uses a Linear **personal API key**, passed verbatim in the
59+
`Authorization` header (no `Bearer` prefix). Create one under
60+
**Settings → Security & access → Personal API keys** in Linear.
61+
62+
## Configuration
63+
64+
Create a connection:
65+
66+
```
67+
curl 'http://localhost:8080/api/plugins/linear/connections' \
68+
--header 'Content-Type: application/json' \
69+
--data-raw '{
70+
"name": "linear",
71+
"endpoint": "https://api.linear.app/graphql",
72+
"token": "<YOUR_LINEAR_API_KEY>",
73+
"rateLimitPerHour": 1500
74+
}'
75+
```
76+
77+
Add a team scope (the team id is the Linear team UUID):
78+
79+
```
80+
curl 'http://localhost:8080/api/plugins/linear/connections/<CONNECTION_ID>/scopes' \
81+
--header 'Content-Type: application/json' \
82+
--data-raw '{
83+
"data": [{ "connectionId": <CONNECTION_ID>, "teamId": "<TEAM_ID>", "name": "Engineering" }]
84+
}'
85+
```
86+
87+
## Collecting data
88+
89+
```
90+
curl 'http://localhost:8080/api/pipelines' \
91+
--header 'Content-Type: application/json' \
92+
--data-raw '{
93+
"name": "linear pipeline",
94+
"plan": [[{
95+
"plugin": "linear",
96+
"options": { "connectionId": <CONNECTION_ID>, "teamId": "<TEAM_ID>" }
97+
}]]
98+
}'
99+
```
100+
101+
## Rate limiting
102+
103+
Linear enforces a per-API-key request budget (1,500 requests/hour) plus a
104+
complexity budget. The collector paces requests against the configured
105+
`rateLimitPerHour` (default 1500). Issues are collected incrementally using
106+
`updatedAt` ordering so re-runs only fetch changes.
107+
108+
## Limitations / roadmap
109+
110+
- Authentication is personal API key only; OAuth2 is a planned follow-up.
111+
- Issue type defaults to `REQUIREMENT`; label-based type mapping via the scope
112+
config is a planned follow-up.
113+
- config-ui integration (connection form + team picker) and the website
114+
documentation page are planned follow-ups; for now connections and scopes are
115+
managed via the API calls shown above.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/errors"
22+
coreModels "github.com/apache/incubator-devlake/core/models"
23+
"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
24+
"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
25+
"github.com/apache/incubator-devlake/core/plugin"
26+
"github.com/apache/incubator-devlake/core/utils"
27+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
28+
"github.com/apache/incubator-devlake/helpers/srvhelper"
29+
"github.com/apache/incubator-devlake/plugins/linear/models"
30+
"github.com/apache/incubator-devlake/plugins/linear/tasks"
31+
)
32+
33+
func MakePipelinePlanV200(
34+
subtaskMetas []plugin.SubTaskMeta,
35+
connectionId uint64,
36+
bpScopes []*coreModels.BlueprintScope,
37+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
38+
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
39+
if err != nil {
40+
return nil, nil, err
41+
}
42+
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection)
47+
if err != nil {
48+
return nil, nil, err
49+
}
50+
scopes, err := makeScopesV200(scopeDetails, connection)
51+
return plan, scopes, err
52+
}
53+
54+
func makePipelinePlanV200(
55+
subtaskMetas []plugin.SubTaskMeta,
56+
scopeDetails []*srvhelper.ScopeDetail[models.LinearTeam, models.LinearScopeConfig],
57+
connection *models.LinearConnection,
58+
) (coreModels.PipelinePlan, errors.Error) {
59+
plan := make(coreModels.PipelinePlan, len(scopeDetails))
60+
for i, scopeDetail := range scopeDetails {
61+
stage := plan[i]
62+
if stage == nil {
63+
stage = coreModels.PipelineStage{}
64+
}
65+
scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
66+
task, err := helper.MakePipelinePlanTask(
67+
"linear",
68+
subtaskMetas,
69+
scopeConfig.Entities,
70+
tasks.LinearOptions{
71+
ConnectionId: connection.ID,
72+
TeamId: scope.TeamId,
73+
ScopeConfigId: scope.ScopeConfigId,
74+
},
75+
)
76+
if err != nil {
77+
return nil, err
78+
}
79+
stage = append(stage, task)
80+
plan[i] = stage
81+
}
82+
return plan, nil
83+
}
84+
85+
func makeScopesV200(
86+
scopeDetails []*srvhelper.ScopeDetail[models.LinearTeam, models.LinearScopeConfig],
87+
connection *models.LinearConnection,
88+
) ([]plugin.Scope, errors.Error) {
89+
scopes := make([]plugin.Scope, 0, len(scopeDetails))
90+
idgen := didgen.NewDomainIdGenerator(&models.LinearTeam{})
91+
for _, scopeDetail := range scopeDetails {
92+
scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
93+
id := idgen.Generate(connection.ID, scope.TeamId)
94+
if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) {
95+
scopes = append(scopes, ticket.NewBoard(id, scope.Name))
96+
}
97+
}
98+
return scopes, nil
99+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"testing"
22+
23+
"github.com/apache/incubator-devlake/core/models/common"
24+
"github.com/apache/incubator-devlake/core/plugin"
25+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
26+
"github.com/apache/incubator-devlake/helpers/srvhelper"
27+
mockplugin "github.com/apache/incubator-devlake/mocks/core/plugin"
28+
"github.com/apache/incubator-devlake/plugins/linear/models"
29+
"github.com/stretchr/testify/assert"
30+
)
31+
32+
func mockLinearPlugin(t *testing.T) {
33+
mockMeta := mockplugin.NewPluginMeta(t)
34+
mockMeta.On("RootPkgPath").Return("github.com/apache/incubator-devlake/plugins/linear")
35+
mockMeta.On("Name").Return("linear").Maybe()
36+
_ = plugin.RegisterPlugin("linear", mockMeta)
37+
}
38+
39+
func TestMakeScopesV200(t *testing.T) {
40+
mockLinearPlugin(t)
41+
42+
const connectionId uint64 = 1
43+
const teamId = "team-1"
44+
const expectDomainScopeId = "linear:LinearTeam:1:team-1"
45+
46+
scopes, err := makeScopesV200(
47+
[]*srvhelper.ScopeDetail[models.LinearTeam, models.LinearScopeConfig]{
48+
{
49+
Scope: models.LinearTeam{
50+
Scope: common.Scope{ConnectionId: connectionId},
51+
TeamId: teamId,
52+
Name: "Engineering",
53+
},
54+
ScopeConfig: &models.LinearScopeConfig{
55+
ScopeConfig: common.ScopeConfig{Entities: []string{plugin.DOMAIN_TYPE_TICKET}},
56+
},
57+
},
58+
},
59+
&models.LinearConnection{
60+
BaseConnection: helper.BaseConnection{Model: common.Model{ID: connectionId}},
61+
},
62+
)
63+
assert.Nil(t, err)
64+
assert.Equal(t, 1, len(scopes))
65+
assert.Equal(t, expectDomainScopeId, scopes[0].ScopeId())
66+
}
67+
68+
func TestMakePipelinePlanV200PassesScopeConfigId(t *testing.T) {
69+
const scopeConfigId uint64 = 42
70+
subtaskMetas := []plugin.SubTaskMeta{
71+
{Name: "convertIssues", EnabledByDefault: true, DomainTypes: []string{plugin.DOMAIN_TYPE_TICKET}},
72+
}
73+
74+
plan, err := makePipelinePlanV200(
75+
subtaskMetas,
76+
[]*srvhelper.ScopeDetail[models.LinearTeam, models.LinearScopeConfig]{
77+
{
78+
Scope: models.LinearTeam{
79+
Scope: common.Scope{ConnectionId: 1, ScopeConfigId: scopeConfigId},
80+
TeamId: "team-1",
81+
Name: "Engineering",
82+
},
83+
ScopeConfig: &models.LinearScopeConfig{
84+
ScopeConfig: common.ScopeConfig{Entities: []string{plugin.DOMAIN_TYPE_TICKET}},
85+
},
86+
},
87+
},
88+
&models.LinearConnection{
89+
BaseConnection: helper.BaseConnection{Model: common.Model{ID: 1}},
90+
},
91+
)
92+
assert.Nil(t, err)
93+
assert.Equal(t, 1, len(plan))
94+
assert.Equal(t, 1, len(plan[0]))
95+
// the scope's scopeConfigId must be threaded into the task options so the
96+
// convertor can resolve label-based issue-type mapping at runtime.
97+
assert.EqualValues(t, scopeConfigId, plan[0][0].Options["scopeConfigId"])
98+
}
99+
100+
func TestMakeScopesV200WithoutTicketEntity(t *testing.T) {
101+
mockLinearPlugin(t)
102+
103+
scopes, err := makeScopesV200(
104+
[]*srvhelper.ScopeDetail[models.LinearTeam, models.LinearScopeConfig]{
105+
{
106+
Scope: models.LinearTeam{
107+
Scope: common.Scope{ConnectionId: 1},
108+
TeamId: "team-1",
109+
},
110+
ScopeConfig: &models.LinearScopeConfig{
111+
ScopeConfig: common.ScopeConfig{Entities: []string{}},
112+
},
113+
},
114+
},
115+
&models.LinearConnection{
116+
BaseConnection: helper.BaseConnection{Model: common.Model{ID: 1}},
117+
},
118+
)
119+
assert.Nil(t, err)
120+
// no ticket entity selected => no domain board scope produced
121+
assert.Equal(t, 0, len(scopes))
122+
}

0 commit comments

Comments
 (0)