Skip to content

Commit 21ea61c

Browse files
feat(jira): persist Sprint Report snapshot for accurate committed/completed velocity (apache#8967) (apache#9010)
1 parent 20179fa commit 21ea61c

15 files changed

Lines changed: 878 additions & 22 deletions

File tree

backend/core/models/domainlayer/ticket/sprint.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ limitations under the License.
1818
package ticket
1919

2020
import (
21+
"time"
22+
2123
"github.com/apache/incubator-devlake/core/models/common"
2224
"github.com/apache/incubator-devlake/core/models/domainlayer"
23-
"time"
2425
)
2526

2627
var (
@@ -31,13 +32,15 @@ var (
3132

3233
type Sprint struct {
3334
domainlayer.DomainEntity
34-
Name string `gorm:"type:varchar(255)"`
35-
Url string `gorm:"type:varchar(255)"`
36-
Status string `gorm:"type:varchar(100)"`
37-
StartedDate *time.Time
38-
EndedDate *time.Time
39-
CompletedDate *time.Time
40-
OriginalBoardID string `gorm:"type:varchar(255)"`
35+
Name string `gorm:"type:varchar(255)"`
36+
Url string `gorm:"type:varchar(255)"`
37+
Status string `gorm:"type:varchar(100)"`
38+
StartedDate *time.Time
39+
EndedDate *time.Time
40+
CompletedDate *time.Time
41+
OriginalBoardID string `gorm:"type:varchar(255)"`
42+
CommittedStoryPoint *float64
43+
CompletedStoryPoint *float64
4144
}
4245

4346
func (Sprint) TableName() string {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 migrationscripts
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/context"
22+
"github.com/apache/incubator-devlake/core/errors"
23+
"github.com/apache/incubator-devlake/core/plugin"
24+
)
25+
26+
var _ plugin.MigrationScript = (*addSprintVelocityFields)(nil)
27+
28+
type sprint20260722 struct {
29+
CommittedStoryPoint *float64
30+
CompletedStoryPoint *float64
31+
}
32+
33+
func (sprint20260722) TableName() string {
34+
return "sprints"
35+
}
36+
37+
type addSprintVelocityFields struct{}
38+
39+
func (script *addSprintVelocityFields) Up(basicRes context.BasicRes) errors.Error {
40+
return basicRes.GetDal().AutoMigrate(new(sprint20260722))
41+
}
42+
43+
func (*addSprintVelocityFields) Version() uint64 {
44+
return 20260722100000
45+
}
46+
47+
func (*addSprintVelocityFields) Name() string {
48+
return "add committed_story_point/completed_story_point to sprints"
49+
}

backend/core/models/migrationscripts/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,6 @@ func All() []plugin.MigrationScript {
148148
new(changeIssueComponentToText),
149149
new(changeCqIssueCodeBlocksComponentToText),
150150
new(addCqProjectMetricsHistory),
151+
new(addSprintVelocityFields),
151152
}
152153
}

backend/plugins/jira/impl/impl.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func (p Jira) GetTablesInfo() []dal.Tabler {
8585
&models.JiraServerInfo{},
8686
&models.JiraSprint{},
8787
&models.JiraSprintIssue{},
88+
&models.JiraSprintReport{},
8889
&models.JiraStatus{},
8990
&models.JiraWorklog{},
9091
&models.JiraIssueComment{},
@@ -138,6 +139,9 @@ func (p Jira) SubTaskMetas() []plugin.SubTaskMeta {
138139
tasks.CollectSprintsMeta,
139140
tasks.ExtractSprintsMeta,
140141

142+
tasks.CollectSprintReportMeta,
143+
tasks.ExtractSprintReportMeta,
144+
141145
tasks.CollectEpicsMeta,
142146
tasks.ExtractEpicsMeta,
143147

@@ -153,6 +157,7 @@ func (p Jira) SubTaskMetas() []plugin.SubTaskMeta {
153157

154158
tasks.ConvertSprintsMeta,
155159
tasks.ConvertSprintIssuesMeta,
160+
tasks.ConvertSprintReportMeta,
156161

157162
tasks.CollectDevelopmentPanelMeta,
158163
tasks.ExtractDevelopmentPanelMeta,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 migrationscripts
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/context"
22+
"github.com/apache/incubator-devlake/core/errors"
23+
"github.com/apache/incubator-devlake/helpers/migrationhelper"
24+
)
25+
26+
type jiraSprintReport20260722 struct {
27+
ConnectionId uint64 `gorm:"primaryKey"`
28+
BoardId uint64 `gorm:"primaryKey"`
29+
SprintId uint64 `gorm:"primaryKey"`
30+
IssueId uint64 `gorm:"primaryKey"`
31+
32+
IssueKey string `gorm:"type:varchar(255)"`
33+
Bucket string `gorm:"type:varchar(32);index"`
34+
Done bool
35+
StoryPointsAtSprintStart *float64
36+
StoryPointsAtSprintEnd *float64
37+
}
38+
39+
func (jiraSprintReport20260722) TableName() string {
40+
return "_tool_jira_sprint_reports"
41+
}
42+
43+
type addSprintReportTable struct{}
44+
45+
func (script *addSprintReportTable) Up(basicRes context.BasicRes) errors.Error {
46+
return migrationhelper.AutoMigrateTables(basicRes, &jiraSprintReport20260722{})
47+
}
48+
49+
func (*addSprintReportTable) Version() uint64 {
50+
return 20260722000000
51+
}
52+
53+
func (*addSprintReportTable) Name() string {
54+
return "add _tool_jira_sprint_reports table to persist Jira's frozen Sprint Report snapshot"
55+
}

backend/plugins/jira/models/migrationscripts/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,6 @@ func All() []plugin.MigrationScript {
5858
new(addSubQueryToBoards),
5959
new(changeFixVersionsToText20260707),
6060
new(addExtraJQLToScopeConfig),
61+
new(addSprintReportTable),
6162
}
6263
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 models
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/models/common"
22+
)
23+
24+
// Sprint Report bucket values, mirroring the four buckets returned by
25+
// GET rest/greenhopper/1.0/rapid/charts/sprintreport. Jira computes these
26+
// once, at sprint close, so persisting them (rather than reconstructing
27+
// from resolution_date) is what makes committed/completed velocity exact.
28+
const (
29+
SprintReportBucketCompleted = "completed"
30+
SprintReportBucketNotCompleted = "notCompleted"
31+
SprintReportBucketPunted = "punted"
32+
SprintReportBucketCompletedInOtherSprint = "completedInOtherSprint"
33+
)
34+
35+
// JiraSprintReport is a frozen, per-(board, sprint, issue) snapshot taken
36+
// from Jira's Sprint Report at sprint close. Unlike JiraSprintIssue (which
37+
// is derived from each issue's live resolution_date and therefore
38+
// mis-attributes carryover issues), this table stores Jira's own
39+
// point-in-time bucketing, so it doesn't drift.
40+
type JiraSprintReport struct {
41+
common.NoPKModel
42+
ConnectionId uint64 `gorm:"primaryKey"`
43+
BoardId uint64 `gorm:"primaryKey"`
44+
SprintId uint64 `gorm:"primaryKey"`
45+
IssueId uint64 `gorm:"primaryKey"`
46+
47+
IssueKey string `gorm:"type:varchar(255)"`
48+
// Bucket is one of the SprintReportBucket* constants above.
49+
Bucket string `gorm:"type:varchar(32);index"`
50+
Done bool
51+
52+
// StoryPointsAtSprintStart is estimateStatistic.statFieldValue.value in
53+
// Jira's response ("BOS points") — the estimate as it stood when the
54+
// sprint began, i.e. what should be summed for *committed* velocity.
55+
StoryPointsAtSprintStart *float64
56+
// StoryPointsAtSprintEnd is currentEstimateStatistic.statFieldValue.value
57+
// ("EOS points") — the estimate as of sprint close, i.e. what should be
58+
// summed (for Bucket == completed) for *completed* velocity.
59+
StoryPointsAtSprintEnd *float64
60+
}
61+
62+
func (JiraSprintReport) TableName() string {
63+
return "_tool_jira_sprint_reports"
64+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 apiv2models
19+
20+
import "time"
21+
22+
// SprintReportInput drives the per-(board, sprint) Sprint Report collector.
23+
// It's what gets iterated over via the DAL cursor, and re-attached to each
24+
// raw row so the extractor knows which board/sprint a response belongs to.
25+
type SprintReportInput struct {
26+
BoardId uint64 `json:"board_id"`
27+
SprintId uint64 `json:"sprint_id"`
28+
// UpdateTime is the sprint's CompleteDate; used as the incremental-sync
29+
// watermark since a sprint report only exists/changes once a sprint closes.
30+
UpdateTime *time.Time `json:"update_time"`
31+
}
32+
33+
// SprintReportStatFieldValue mirrors Jira's
34+
// {statFieldValue: {value, text}} shape used for per-issue point estimates.
35+
type SprintReportStatFieldValue struct {
36+
Value *float64 `json:"value"`
37+
Text string `json:"text"`
38+
}
39+
40+
type SprintReportStatistic struct {
41+
StatFieldValue SprintReportStatFieldValue `json:"statFieldValue"`
42+
}
43+
44+
// SprintReportIssue is one entry inside any of the four bucket lists
45+
// (completedIssues, issuesNotCompletedInCurrentSprint, puntedIssues,
46+
// issuesCompletedInAnotherSprint) in the Sprint Report response.
47+
type SprintReportIssue struct {
48+
Id uint64 `json:"id"`
49+
Key string `json:"key"`
50+
TypeName string `json:"typeName"`
51+
Done bool `json:"done"`
52+
// EstimateStatistic is the issue's estimate as of sprint *start*
53+
// ("BOS points" / committed).
54+
EstimateStatistic SprintReportStatistic `json:"estimateStatistic"`
55+
// CurrentEstimateStatistic is the issue's estimate as of sprint *close*
56+
// ("EOS points" / completed).
57+
CurrentEstimateStatistic SprintReportStatistic `json:"currentEstimateStatistic"`
58+
}
59+
60+
// SprintReportContents is the "contents" object of the Sprint Report
61+
// response — the frozen snapshot Jira takes at sprint close.
62+
type SprintReportContents struct {
63+
CompletedIssues []SprintReportIssue `json:"completedIssues"`
64+
IssuesNotCompletedInCurrentSprint []SprintReportIssue `json:"issuesNotCompletedInCurrentSprint"`
65+
PuntedIssues []SprintReportIssue `json:"puntedIssues"`
66+
IssuesCompletedInAnotherSprint []SprintReportIssue `json:"issuesCompletedInAnotherSprint"`
67+
}
68+
69+
// SprintReport is the top-level response of
70+
// GET rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=&sprintId=
71+
type SprintReport struct {
72+
Contents SprintReportContents `json:"contents"`
73+
}

0 commit comments

Comments
 (0)