@beauraines/sprint-tracker 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +16 -0
- package/.github/dependabot.yml +21 -0
- package/CHANGELOG.md +52 -0
- package/R/HeloSprintBurndown.R +138 -0
- package/R/ProjectHealth.R +109 -0
- package/R/backlogByTeam.R +52 -0
- package/R/backlogHealth.R +163 -0
- package/R/burndDownChart.R +144 -0
- package/R/carryOverAdjustedVelocity.R +154 -0
- package/R/featureTeamBacklog.R +73 -0
- package/R/projectHealth.Rmd +111 -0
- package/R/sprintOutcomes.R +142 -0
- package/R/sprintOutcomesCli.R +103 -0
- package/R/sprintOutcomesFeatureTeam.R +143 -0
- package/R/timeInCodeReview.R +157 -0
- package/README.md +181 -0
- package/cli.js +55 -0
- package/cmds/addOutcomes.js +98 -0
- package/cmds/addProject.js +53 -0
- package/cmds/addSprint.js +117 -0
- package/cmds/config.js +33 -0
- package/cmds/getSprintDetails.js +86 -0
- package/cmds/visualizations.js +116 -0
- package/docker-compose.yml +11 -0
- package/migrations/20230222011333-create-health-table.sql +10 -0
- package/migrations/20230222011334-outcomes.sql +42 -0
- package/migrations/20231206093700-seed-outcomes-table.sql +9 -0
- package/migrations.js +20 -0
- package/package.json +50 -0
- package/scripts/generateBacklogByTeam.sh +20 -0
- package/scripts/generateBacklogHealth.sh +12 -0
- package/scripts/generateChart.sh +7 -0
- package/scripts/generateFeatureTeamBacklog.sh +16 -0
- package/scripts/generateSprintOutcomePlot.sh +12 -0
- package/scripts/sprintOutcomes.sh +29 -0
- package/sql/bugsOpenedDuringSprint.sql +14 -0
- package/sql/bugsOpenedDuringSprintFeature.sql +14 -0
- package/sql/capacityAdjustedVelocity.sql +48 -0
- package/sql/carryOver.sql +33 -0
- package/sql/commitment.sql +30 -0
- package/sql/commitmentMet.sql +60 -0
- package/sql/completed.sql +40 -0
- package/sql/descoped.sql +21 -0
- package/sql/pullForward.sql +19 -0
- package/sql/pullForwardFeature.sql +10 -0
- package/sql/sprintOutcomes.sql +25 -0
- package/sql/sprint_outcomes.sql +23 -0
- package/src/addOutcomes.js +94 -0
- package/src/addSprint.js +108 -0
- package/src/getSprintDetails.js +88 -0
- package/utils/display.js +68 -0
- package/utils/input.js +24 -0
- package/utils/readConfig.js +74 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const {homedir} = require('os');
|
|
4
|
+
const { database: db } = require('helpers')
|
|
5
|
+
const { prompt,promptForInput } = require('../utils/input.js')
|
|
6
|
+
const {readConfig} = require('../utils/readConfig.js')
|
|
7
|
+
const display = require('../utils/display.js')
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const command = 'addOutcome'
|
|
11
|
+
const desc = 'Menu driven process to add outcomes to a sprint'
|
|
12
|
+
const builder = {}
|
|
13
|
+
|
|
14
|
+
const handler = async function () {
|
|
15
|
+
// TODO add migrations check or index function
|
|
16
|
+
const filename = 'sprintTracker.json'
|
|
17
|
+
const configFile = path.join(homedir(),filename)
|
|
18
|
+
const config = await readConfig(configFile)
|
|
19
|
+
const database = config.database
|
|
20
|
+
|
|
21
|
+
// list projects
|
|
22
|
+
|
|
23
|
+
let statement = 'select * from projects;';
|
|
24
|
+
|
|
25
|
+
let projects = await db.query(database,statement)
|
|
26
|
+
|
|
27
|
+
for (const project of projects) {
|
|
28
|
+
console.log(project.id, project.name)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Select the project
|
|
32
|
+
let project_id
|
|
33
|
+
|
|
34
|
+
let text = 'Enter the project id: '
|
|
35
|
+
while (!project_id) {
|
|
36
|
+
project_id = prompt(text)
|
|
37
|
+
|
|
38
|
+
// validate the response
|
|
39
|
+
if (!projects.find(x => (x.id == project_id))) {
|
|
40
|
+
console.error('Invalid project id')
|
|
41
|
+
project_id = null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
console.log(project_id)
|
|
47
|
+
|
|
48
|
+
// Add new sprint
|
|
49
|
+
|
|
50
|
+
let sprints = await db.query(database,`select id, sprint_name AS name from sprints where project_id = ${project_id};`)
|
|
51
|
+
|
|
52
|
+
// FIXME - sprint_name isn't being displayed because the function is looking for name
|
|
53
|
+
console.log('If your sprint is not displayed, you will have to manually add it.')
|
|
54
|
+
let sprintId = promptForInput(sprints,'Enter the sprint id: ')
|
|
55
|
+
|
|
56
|
+
// enter outcomes
|
|
57
|
+
let sprintOutcomes = []
|
|
58
|
+
let outcomes = await db.query(database,'select * from outcomes;')
|
|
59
|
+
for (const outcome of outcomes) {
|
|
60
|
+
// TODO Fix output formatting
|
|
61
|
+
console.log(outcome.id, outcome.name, outcome.description)
|
|
62
|
+
let issue_count = prompt('Enter the number of stories or a blank line to skip: ')
|
|
63
|
+
if (issue_count == '' ) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
let story_points = prompt('Enter the number of story points: ')
|
|
67
|
+
let notes = prompt('Enter any notes: ')
|
|
68
|
+
|
|
69
|
+
sprintOutcomes.push({
|
|
70
|
+
sprint_id: sprintId,
|
|
71
|
+
sprint_name: sprints.filter(x => x.id == sprintId)[0].name,
|
|
72
|
+
outcome_id: outcome.id,
|
|
73
|
+
outcome_name: outcome.name,
|
|
74
|
+
outcome_description: outcome.description,
|
|
75
|
+
issue_count,
|
|
76
|
+
story_points,
|
|
77
|
+
notes
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
// validate sprint_outcomes
|
|
81
|
+
display.sprintOutcomes(sprintOutcomes)
|
|
82
|
+
|
|
83
|
+
// Save outcomes to DB
|
|
84
|
+
// Remove properties not in the table
|
|
85
|
+
for (const outcome of sprintOutcomes) {
|
|
86
|
+
delete outcome.sprint_name
|
|
87
|
+
delete outcome.outcome_name
|
|
88
|
+
delete outcome.outcome_description
|
|
89
|
+
}
|
|
90
|
+
let result = await db.insert(database,'sprint_outcomes',sprintOutcomes,['sprint_id','outcome_id'])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
command,
|
|
95
|
+
desc,
|
|
96
|
+
builder,
|
|
97
|
+
handler
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const {homedir} = require('os');
|
|
4
|
+
const { database: db } = require('helpers')
|
|
5
|
+
const { prompt } = require('../utils/input.js')
|
|
6
|
+
const {readConfig} = require('../utils/readConfig.js')
|
|
7
|
+
const display = require('../utils/display.js')
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const command = 'addProject'
|
|
11
|
+
const desc = 'Prompts to add a new project to the database'
|
|
12
|
+
const builder = {}
|
|
13
|
+
|
|
14
|
+
const handler = async function () {
|
|
15
|
+
|
|
16
|
+
const filename = 'sprintTracker.json'
|
|
17
|
+
const configFile = path.join(homedir(),filename)
|
|
18
|
+
const config = await readConfig(configFile)
|
|
19
|
+
const database = config.database
|
|
20
|
+
|
|
21
|
+
// Get new project information from user
|
|
22
|
+
let name = prompt('Enter the Project Name: ')
|
|
23
|
+
let pm_name = prompt('Enter the PM Name: ')
|
|
24
|
+
let pm_email = prompt('Enter the PM Email: ')
|
|
25
|
+
let client = prompt('Enter the Client: ')
|
|
26
|
+
let project_number = prompt('Enter the Project Number: ')
|
|
27
|
+
let start_date = prompt('Enter the start date (YYYY-MM-DD): ')
|
|
28
|
+
let end_date = prompt('Enter the end date (YYYY-MM-DD): ')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
let project = {
|
|
32
|
+
name,
|
|
33
|
+
pm_name,
|
|
34
|
+
pm_email,
|
|
35
|
+
client,
|
|
36
|
+
project_number,
|
|
37
|
+
start_date,
|
|
38
|
+
end_date
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Save project to DB
|
|
42
|
+
let result = await db.insert(database,'projects',[project])
|
|
43
|
+
|
|
44
|
+
project.id = result[0].lastID
|
|
45
|
+
display.project(project)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
command,
|
|
50
|
+
desc,
|
|
51
|
+
builder,
|
|
52
|
+
handler
|
|
53
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const {homedir} = require('os');
|
|
4
|
+
const { database: db } = require('helpers')
|
|
5
|
+
const { prompt } = require('../utils/input.js')
|
|
6
|
+
const {readConfig} = require('../utils/readConfig.js')
|
|
7
|
+
const display = require('../utils/display.js')
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const command = 'addSprint'
|
|
11
|
+
const desc = 'Prompts to add a new sprint to an existing project'
|
|
12
|
+
const builder = {}
|
|
13
|
+
|
|
14
|
+
const handler = async function () {
|
|
15
|
+
|
|
16
|
+
const filename = 'sprintTracker.json'
|
|
17
|
+
const configFile = path.join(homedir(),filename)
|
|
18
|
+
const config = await readConfig(configFile)
|
|
19
|
+
const database = config.database
|
|
20
|
+
|
|
21
|
+
// list projects
|
|
22
|
+
|
|
23
|
+
let statement = 'select * from projects;';
|
|
24
|
+
|
|
25
|
+
let projects = await db.query(database,statement)
|
|
26
|
+
|
|
27
|
+
for (const project of projects) {
|
|
28
|
+
console.log(project.id, project.name)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Select the project
|
|
32
|
+
let project_id
|
|
33
|
+
|
|
34
|
+
let text = 'Enter the project id: '
|
|
35
|
+
while (!project_id) {
|
|
36
|
+
project_id = prompt(text)
|
|
37
|
+
|
|
38
|
+
// validate the response
|
|
39
|
+
if (!projects.find(x => (x.id == project_id))) {
|
|
40
|
+
console.error('Invalid project id')
|
|
41
|
+
project_id = null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
console.log(project_id)
|
|
47
|
+
|
|
48
|
+
// Get new sprint information from user
|
|
49
|
+
|
|
50
|
+
let sprint_name = prompt('Enter the sprint name: ')
|
|
51
|
+
let start_date = prompt('Enter the sprint start date: ')
|
|
52
|
+
let end_date = prompt('Enter the sprint end date: ')
|
|
53
|
+
let non_working_days = prompt('Enter the number of non working days: ')
|
|
54
|
+
let developers = prompt('Enter the number of developers assigned: ')
|
|
55
|
+
let developer_pto_days = prompt('Enter the total number of developer PTO days: ')
|
|
56
|
+
|
|
57
|
+
let sprint = {
|
|
58
|
+
sprint_name,
|
|
59
|
+
project_id,
|
|
60
|
+
start_date,
|
|
61
|
+
end_date,
|
|
62
|
+
non_working_days,
|
|
63
|
+
developers,
|
|
64
|
+
developer_pto_days
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Save outcomes to DB
|
|
68
|
+
let result = await db.insert(database,'sprints',[sprint])
|
|
69
|
+
let sprintId = result[0].lastID
|
|
70
|
+
|
|
71
|
+
// Prompt for commitment
|
|
72
|
+
let sprintOutcomes = []
|
|
73
|
+
let outcomes = await db.query(database,`select * from outcomes where name = 'Commitment';`)
|
|
74
|
+
for (const outcome of outcomes) {
|
|
75
|
+
// TODO Fix output formatting
|
|
76
|
+
console.log(outcome.id, outcome.name, outcome.description)
|
|
77
|
+
let issue_count = prompt('Enter the number of stories or a blank line to skip: ')
|
|
78
|
+
if (issue_count == '' ) {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
let story_points = prompt('Enter the number of story points: ')
|
|
82
|
+
let notes = prompt('Enter any notes: ')
|
|
83
|
+
|
|
84
|
+
sprintOutcomes.push({
|
|
85
|
+
sprint_id: sprintId,
|
|
86
|
+
sprint_name,
|
|
87
|
+
outcome_id: outcome.id,
|
|
88
|
+
outcome_name: outcome.name,
|
|
89
|
+
outcome_description: outcome.description,
|
|
90
|
+
issue_count,
|
|
91
|
+
story_points,
|
|
92
|
+
notes
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
// validate sprint_outcomes
|
|
96
|
+
|
|
97
|
+
display.sprintOutcomes(sprintOutcomes)
|
|
98
|
+
|
|
99
|
+
if (sprintOutcomes.length != 0) {
|
|
100
|
+
// Save outcomes to DB
|
|
101
|
+
// Remove properties not in the table
|
|
102
|
+
for (const outcome of sprintOutcomes) {
|
|
103
|
+
delete outcome.sprint_name
|
|
104
|
+
delete outcome.outcome_name
|
|
105
|
+
delete outcome.outcome_description
|
|
106
|
+
}
|
|
107
|
+
result = await db.insert(database,'sprint_outcomes',sprintOutcomes,['sprint_id','outcome_id'])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
command,
|
|
114
|
+
desc,
|
|
115
|
+
builder,
|
|
116
|
+
handler
|
|
117
|
+
}
|
package/cmds/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { homedir } = require('os');
|
|
2
|
+
const { createConfig } = require('../utils/readConfig.js')
|
|
3
|
+
const { helpers } = require('helpers')
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const command = 'config'
|
|
8
|
+
const desc = 'Creates a config file, prompting for required inputs'
|
|
9
|
+
const builder = {}
|
|
10
|
+
|
|
11
|
+
const handler = async function () {
|
|
12
|
+
|
|
13
|
+
const filename = 'sprintTracker.json'
|
|
14
|
+
const configFile = path.join(homedir(), filename)
|
|
15
|
+
|
|
16
|
+
if (helpers.fileExists(configFile)) {
|
|
17
|
+
console.error(`Aborting. Existing configuration file ${configFile} found to not overwrite your values.`)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configProps = ['database']
|
|
22
|
+
await createConfig(configFile, configProps)
|
|
23
|
+
console.log(`Created ${configFile}`)
|
|
24
|
+
console.log(`Please edit the file and update the ${configProps} value(s)`)
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
command,
|
|
30
|
+
desc,
|
|
31
|
+
builder,
|
|
32
|
+
handler
|
|
33
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const {homedir} = require('os');
|
|
4
|
+
const { database: db } = require('helpers')
|
|
5
|
+
const { prompt,promptForInput } = require('../utils/input.js')
|
|
6
|
+
const {readConfig} = require('../utils/readConfig.js')
|
|
7
|
+
const display = require('../utils/display.js')
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const command = 'getSprint'
|
|
11
|
+
const desc = 'Gets sprint details'
|
|
12
|
+
const builder = {}
|
|
13
|
+
const handler = async function () {
|
|
14
|
+
|
|
15
|
+
const filename = 'sprintTracker.json'
|
|
16
|
+
const configFile = path.join(homedir(),filename)
|
|
17
|
+
const config = await readConfig(configFile)
|
|
18
|
+
const database = config.database
|
|
19
|
+
|
|
20
|
+
// list projects
|
|
21
|
+
|
|
22
|
+
let statement = 'select * from projects;';
|
|
23
|
+
|
|
24
|
+
let projects = await db.query(database,statement)
|
|
25
|
+
|
|
26
|
+
for (const project of projects) {
|
|
27
|
+
console.log(project.id, project.name)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Select the project
|
|
31
|
+
let project_id
|
|
32
|
+
|
|
33
|
+
let text = 'Enter the project id: '
|
|
34
|
+
while (!project_id) {
|
|
35
|
+
project_id = prompt(text)
|
|
36
|
+
|
|
37
|
+
// validate the response
|
|
38
|
+
if (!projects.find(x => (x.id == project_id))) {
|
|
39
|
+
console.error('Invalid project id')
|
|
40
|
+
project_id = null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
let sprints = await db.query(database,`select id, sprint_name AS name from sprints where project_id = ${project_id};`)
|
|
46
|
+
|
|
47
|
+
// FIXME - sprint_name isn't being displayed because the function is looking for name
|
|
48
|
+
console.log('If your sprint is not displayed, you will have to manually add it.')
|
|
49
|
+
let sprintId = promptForInput(sprints,'Enter the sprint id: ')
|
|
50
|
+
|
|
51
|
+
statement = `select
|
|
52
|
+
p.name,
|
|
53
|
+
s.sprint_name,
|
|
54
|
+
s.start_date,
|
|
55
|
+
s.end_date,
|
|
56
|
+
o.name AS outcome_name,
|
|
57
|
+
so.issue_count,
|
|
58
|
+
so.story_points,
|
|
59
|
+
so.notes,
|
|
60
|
+
o.description
|
|
61
|
+
|
|
62
|
+
from
|
|
63
|
+
sprint_outcomes so
|
|
64
|
+
JOIN sprints s on s.id = so.sprint_id
|
|
65
|
+
JOIN outcomes o on o.id = so.outcome_id
|
|
66
|
+
JOIN projects p on p.id = s.project_id
|
|
67
|
+
where
|
|
68
|
+
-- p.id = 4
|
|
69
|
+
-- and sprint_name = 'ACDC 15'
|
|
70
|
+
p.id = ${project_id}
|
|
71
|
+
and s.id = ${sprintId}`
|
|
72
|
+
|
|
73
|
+
let sprintDetails = await db.query(database,statement)
|
|
74
|
+
// console.log(sprintDetails)
|
|
75
|
+
display.sprintDetails(sprintDetails)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
command,
|
|
83
|
+
desc,
|
|
84
|
+
builder,
|
|
85
|
+
handler
|
|
86
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { AsyncParser } = require('@json2csv/node')
|
|
4
|
+
const {homedir, tmpdir} = require('os');
|
|
5
|
+
const { database: db } = require('helpers')
|
|
6
|
+
const { prompt } = require('../utils/input.js')
|
|
7
|
+
const {readConfig} = require('helpers').config
|
|
8
|
+
const exec = require('child_process').exec;
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const process = require('process');
|
|
12
|
+
|
|
13
|
+
const command = 'visualizations'
|
|
14
|
+
const desc = 'Creates Feature Team visualizations with R. Must have docker, rstudio-tidyverse container and more installed'
|
|
15
|
+
const builder = {}
|
|
16
|
+
|
|
17
|
+
const handler = async function () {
|
|
18
|
+
// Get Data from Database and write to file
|
|
19
|
+
console.log('Getting data')
|
|
20
|
+
|
|
21
|
+
const filename = 'sprintTracker.json'
|
|
22
|
+
const config = await readConfig(filename)
|
|
23
|
+
const database = config.database
|
|
24
|
+
|
|
25
|
+
// list projects
|
|
26
|
+
|
|
27
|
+
let statement = 'select * from projects;';
|
|
28
|
+
|
|
29
|
+
let projects = await db.query(database,statement)
|
|
30
|
+
|
|
31
|
+
for (const project of projects) {
|
|
32
|
+
console.log(project.id, project.name)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Select the project
|
|
36
|
+
let project_id
|
|
37
|
+
|
|
38
|
+
let text = 'Enter the project id: '
|
|
39
|
+
while (!project_id) {
|
|
40
|
+
project_id = prompt(text)
|
|
41
|
+
|
|
42
|
+
// validate the response
|
|
43
|
+
if (!projects.find(x => (x.id == project_id))) {
|
|
44
|
+
console.error('Invalid project id')
|
|
45
|
+
project_id = null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let query = `
|
|
50
|
+
SELECT
|
|
51
|
+
p.name project_name,
|
|
52
|
+
s.id sprint_id,
|
|
53
|
+
s.sprint_name,
|
|
54
|
+
s.end_date,
|
|
55
|
+
case
|
|
56
|
+
when o.name = 'Commitment' then 'A-Commitment'
|
|
57
|
+
when o.name = 'Delivered' then 'B-Delivered'
|
|
58
|
+
else o.name
|
|
59
|
+
end outcome,
|
|
60
|
+
ifnull(so.story_points, so.issue_count) AS story_points -- accounts for unpointed bug stories
|
|
61
|
+
|
|
62
|
+
from projects p
|
|
63
|
+
|
|
64
|
+
JOIN sprints s on s.project_id = p.id
|
|
65
|
+
JOIN sprint_outcomes so on so.sprint_id = s.id
|
|
66
|
+
JOIN outcomes o on o.id = so.outcome_id
|
|
67
|
+
|
|
68
|
+
WHERE
|
|
69
|
+
p.id = ${project_id};
|
|
70
|
+
`
|
|
71
|
+
|
|
72
|
+
let outcomes = await db.query(database,query)
|
|
73
|
+
|
|
74
|
+
// Convert JSON to CSV
|
|
75
|
+
const opts = {};
|
|
76
|
+
const transformOpts = {};
|
|
77
|
+
const asyncOpts = {};
|
|
78
|
+
const parser = new AsyncParser(opts, transformOpts, asyncOpts);
|
|
79
|
+
|
|
80
|
+
const csv = await parser.parse(outcomes).promise();
|
|
81
|
+
|
|
82
|
+
// Write to file
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const outputFile = path.join(tmpdir(),'outcomes.csv');
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(outputFile, csv);
|
|
88
|
+
// file written successfully
|
|
89
|
+
console.log(`Data written to ${outputFile}`)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(err);
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
console.log('Making visualizations')
|
|
97
|
+
// Run docker command
|
|
98
|
+
let PROJECT_DIR=path.join(homedir(),'projects')
|
|
99
|
+
let IMAGE='rocker/tidyverse'
|
|
100
|
+
const cmd = `docker run -t --rm -v ${PROJECT_DIR}:/home/rstudio/projects/ -v ${tmpdir()}:/tmp ${IMAGE} su -c 'Rscript /home/rstudio/projects/sprint-tracker/R/sprintOutcomesCli.R' rstudio`
|
|
101
|
+
exec(cmd, function(err, stdout, stderr) {
|
|
102
|
+
if (err) {
|
|
103
|
+
console.log(stderr);
|
|
104
|
+
console.log(err.message)
|
|
105
|
+
process.exit(err.code)
|
|
106
|
+
}
|
|
107
|
+
console.log(stdout);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
command,
|
|
113
|
+
desc,
|
|
114
|
+
builder,
|
|
115
|
+
handler
|
|
116
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
services:
|
|
2
|
+
rstudio:
|
|
3
|
+
image: rocker/tidyverse
|
|
4
|
+
ports:
|
|
5
|
+
- 8888:8787
|
|
6
|
+
volumes:
|
|
7
|
+
- ~/projects:/home/rstudio/projects
|
|
8
|
+
- ~/BurnDownStatus.db:/home/rstudio/BurnDownStatus.db
|
|
9
|
+
container_name: rstudio-server-sprint-tracker
|
|
10
|
+
environment:
|
|
11
|
+
PASSWORD: password
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS health (
|
|
2
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
|
+
project STRING NOT NULL,
|
|
4
|
+
date DATE NOT NULL,
|
|
5
|
+
bug_count INTEGER DEFAULT (0),
|
|
6
|
+
unestimated_count INTEGER DEFAULT (0),
|
|
7
|
+
blocked_count INTEGER DEFAULT (0),
|
|
8
|
+
open_count INTEGER DEFAULT (0),
|
|
9
|
+
total_count INTEGER DEFAULT (0)
|
|
10
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS outcomes (
|
|
2
|
+
id INTEGER PRIMARY KEY,
|
|
3
|
+
name STRING NOT NULL,
|
|
4
|
+
description TEXT NOT NULL
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
8
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
name STRING,
|
|
10
|
+
pm_name STRING,
|
|
11
|
+
pm_email STRING,
|
|
12
|
+
client STRING,
|
|
13
|
+
project_number STRING,
|
|
14
|
+
start_date DATE,
|
|
15
|
+
end_date DATE
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS sprint_outcomes (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
sprint_id INTEGER REFERENCES sprints (id) ON DELETE CASCADE
|
|
22
|
+
ON UPDATE CASCADE,
|
|
23
|
+
outcome_id INTEGER REFERENCES outcomes (id) ON DELETE SET DEFAULT
|
|
24
|
+
ON UPDATE RESTRICT,
|
|
25
|
+
issue_count INTEGER DEFAULT (0),
|
|
26
|
+
story_points INTEGER DEFAULT (0),
|
|
27
|
+
notes TEXT
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS sprints (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
sprint_name STRING,
|
|
33
|
+
project_id INTEGER REFERENCES projects (id) ON DELETE CASCADE
|
|
34
|
+
ON UPDATE CASCADE,
|
|
35
|
+
start_date DATE,
|
|
36
|
+
end_date DATE,
|
|
37
|
+
commited_points INTEGER DEFAULT (0),
|
|
38
|
+
committed_stories INTEGER DEFAULT (0),
|
|
39
|
+
non_working_days INTEGER DEFAULT (0),
|
|
40
|
+
developers INTEGER DEFAULT (1),
|
|
41
|
+
developer_pto_days INTEGER DEFAULT (0)
|
|
42
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
INSERT into OUTCOMES (name,description)
|
|
2
|
+
VALUES
|
|
3
|
+
("Commitment","Initial sprint commitment"),
|
|
4
|
+
("Delivered","Work completed this sprint"),
|
|
5
|
+
("Unplanned - Story","All unplanned work brought into the sprint, whether completed or not"),
|
|
6
|
+
("Unplanned - Bug","Unplanned work without a pre-existing story that was deemed critical enough to add to the sprint"),
|
|
7
|
+
("Descoped","Committed work, removed from the sprint"),
|
|
8
|
+
("Carryover","Incomplete, started or unstarted, work from this sprint, assumed that it will carryover to the next sprint"),
|
|
9
|
+
("Carryover - External","Work that was carried over due to blockers external to the scrum team. Use of this is optional, as an agile team should be fully self-sufficient.");
|
package/migrations.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
2
|
+
let {CommandsRunner, SQLite3Driver} = require('node-db-migration');
|
|
3
|
+
const {homedir} = require('os');
|
|
4
|
+
const {readConfig} = require('./utils/readConfig.js')
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const main = async () => {
|
|
8
|
+
const filename = 'sprintTracker.json'
|
|
9
|
+
const configFile = path.join(homedir(),filename)
|
|
10
|
+
const config = await readConfig(configFile)
|
|
11
|
+
const database = config.database
|
|
12
|
+
let db = new sqlite3.Database(database);
|
|
13
|
+
let migrations = new CommandsRunner({
|
|
14
|
+
driver: new SQLite3Driver(db),
|
|
15
|
+
directoryWithScripts: __dirname + '/migrations',
|
|
16
|
+
});
|
|
17
|
+
migrations.run(process.argv[2])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beauraines/sprint-tracker",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Database and R visualizations to track sprint performance",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sprint-tracker": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"addOutcomes": "node src/addOutcomes.js",
|
|
12
|
+
"addSprint": "node src/addSprint.js",
|
|
13
|
+
"getSprintDetails": "node src/getSprintDetails.js",
|
|
14
|
+
"generateSprintOutcomePlot": "sh scripts/generateSprintOutcomePlot.sh",
|
|
15
|
+
"database:init": "node migrations.js init",
|
|
16
|
+
"database:migrate": "node migrations.js migrate",
|
|
17
|
+
"database:upgrade": "node migrations.js migrate",
|
|
18
|
+
"should-release": "should-release",
|
|
19
|
+
"release": "standard-version",
|
|
20
|
+
"lint": "eslint '*/**.js'",
|
|
21
|
+
"lint:fix": "eslint '*/**.js' --fix"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+ssh://git@bitbucket.org/beauraines/sprint-tracker.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Beau Raines",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://bitbucket.org/beauraines/sprint-tracker/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://bitbucket.org/beauraines/sprint-tracker#readme",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"eslint": "^8.42.0",
|
|
35
|
+
"eslint-plugin-jest": "^27.2.1",
|
|
36
|
+
"jest": "^29.3.1"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@json2csv/node": "^7.0.1",
|
|
40
|
+
"cli-table3": "^0.6.3",
|
|
41
|
+
"helpers": "npm:@beauraines/node-helpers@^2.4.0",
|
|
42
|
+
"node-db-migration": "^1.4.0",
|
|
43
|
+
"prompt-sync": "^4.2.0",
|
|
44
|
+
"should-release": "^1.2.0",
|
|
45
|
+
"sqlite": "^4.1.2",
|
|
46
|
+
"sqlite3": "^5.1.2",
|
|
47
|
+
"standard-version": "^9.5.0",
|
|
48
|
+
"yargs": "^17.7.1"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
if ! docker info > /dev/null 2>&1; then
|
|
4
|
+
echo "This script uses docker, and it isn't running - please start docker and try again!"
|
|
5
|
+
exit 1
|
|
6
|
+
fi
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
QUERY_ID=b18a81c9-3e3a-4dc6-87ef-140caf5bcb6a
|
|
10
|
+
|
|
11
|
+
az boards query --organization https://dev.azure.com/teamsi --id ${QUERY_ID} |
|
|
12
|
+
jq -r '[.[]| {team:.fields."System.AreaPath",type:.fields."System.WorkItemType",id,estimate: .fields."Microsoft.VSTS.Scheduling.StoryPoints" }]' |
|
|
13
|
+
json2csv -o ~/projects/backlog.csv
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# PROJECT_DIR must have trailing slash
|
|
17
|
+
PROJECT_DIR=/Users/beauraines/projects/
|
|
18
|
+
IMAGE=rocker/tidyverse
|
|
19
|
+
|
|
20
|
+
docker run -it --rm -v ${PROJECT_DIR}:/home/rstudio/projects/ ${IMAGE} su -c 'Rscript /home/rstudio/projects/sprint-tracker/R/backlogByTeam.R 2>/dev/null' rstudio
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
if ! docker info > /dev/null 2>&1; then
|
|
4
|
+
echo "This script uses docker, and it isn't running - please start docker and try again!"
|
|
5
|
+
exit 1
|
|
6
|
+
fi
|
|
7
|
+
|
|
8
|
+
# PROJECT_DIR must have trailing slash
|
|
9
|
+
PROJECT_DIR=/Users/beauraines/projects/
|
|
10
|
+
IMAGE=rocker/tidyverse
|
|
11
|
+
|
|
12
|
+
docker run -it --rm -v ${PROJECT_DIR}:/home/rstudio/projects/ ${IMAGE} su -c 'Rscript /home/rstudio/projects/sprint-tracker/R/backlogHealth.R 2>/dev/null' rstudio
|