Created
September 13, 2016 22:41
-
-
Save ricejasonf/f94c6577934767df0a4d7bc0a8f54d10 to your computer and use it in GitHub Desktop.
Generate Markdown for Weekly Plan from PivotalTracker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "pivotal-tools", | |
"version": "1.0.0", | |
"description": "", | |
"main": "weekly_plan.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "Jason Rice", | |
"license": "ISC", | |
"dependencies": { | |
"bluebird": "^3.4.6", | |
"ramda": "^0.22.1" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Bluebird = require('bluebird'); | |
const process = require('process'); | |
const https = require('https'); | |
const R = require('ramda'); | |
const util = require('util'); | |
const api_token = process.env.PIVOTAL_API_TOKEN; | |
const project_id = process.env.PIVOTAL_API_PROJECT_ID; | |
const epic_label = 'concepts-notifications' | |
if (!project_id) throw "PIVOTAL_API_PROJECT_ID not set."; | |
const inspect = function(obj) | |
{ | |
console.log(util.inspect(obj, false, null)); | |
} | |
const tap_inspect = R.tap(inspect); | |
const daily_point_quota = 5; | |
const day_labels = [ | |
'Tuesday', | |
'Wednesday', | |
'Thursday', | |
'Friday' | |
]; | |
const requestGet = function(path) | |
{ | |
return new Bluebird(function(resolve, reject) | |
{ | |
https.request( | |
{ | |
host: 'www.pivotaltracker.com', | |
//port: '443', | |
path: '/services/v5' + path, | |
method: 'GET', | |
headers: { | |
'X-TrackerToken': api_token | |
} | |
}, | |
function(res) | |
{ | |
let response_data = ''; | |
res.on('error', (err) => reject(err)); | |
if (res.statusCode > 500) return reject(res.statusMessage); | |
let finalize = res.statusCode == 200 ? resolve : reject; | |
res.setEncoding('utf8'); | |
res.on('data', (chunk) => response_data += chunk); | |
res.on('end', () => resolve(JSON.parse(response_data))); | |
}).end(); | |
}); | |
} | |
Bluebird.all([ | |
requestGet('/projects/' + project_id + '/stories' | |
+ '?date_format=millis' | |
+ '&fields=id%2Curl%2Cname%2Cowners%2Cestimate' | |
+ '&with_label=' + epic_label | |
+ '&with_state=started' | |
+ '&with_story_type=feature' | |
) | |
, requestGet('/projects/' + project_id + '/stories' | |
+ '?date_format=millis' | |
+ '&fields=id%2Curl%2Cname%2Cowners%2Cestimate' | |
+ '&with_label=' + epic_label | |
+ '&with_state=unstarted' | |
+ '&with_story_type=feature' | |
) | |
]) | |
.spread((result1, result2) => renderWeeklyPlan(R.concat(result1, result2))) | |
.catch(fail) | |
; | |
/* | |
# This Week | |
## Tuesday | |
| Points | Assignee | Story | | |
|-------:|---------:|-------------------------------------------------------------------| | |
| 1 | Jason | [#129882877](https://www.pivotaltracker.com/story/show/129882877) | | |
| 1 | Jason | [#129882877](https://www.pivotaltracker.com/story/show/129882877) | | |
## Wednesday | |
## Thursday | |
## Friday | |
*/ | |
function fail(error) | |
{ | |
console.log(error); | |
process.exit(1); | |
} | |
function renderWeeklyPlan(result) | |
{ | |
const daily_tasks = getDailyTasks(result); | |
const story_rows = R.map(R.map(renderStoryRow))(daily_tasks); | |
const renderDay_ = renderDay(daily_tasks); | |
const output = [ | |
renderHeader() | |
, renderDay_(0) | |
, renderDay_(1) | |
, renderDay_(2) | |
, renderDay_(3) | |
]; | |
process.stdout.write(R.join('')(output)); | |
process.stdout.write('\n'); | |
} | |
function renderHeader() | |
{ | |
return '# Team Early Birds\n\n'; | |
} | |
function renderDay(daily_tasks) | |
{ | |
return function(i) | |
{ | |
return R.compose( | |
R.join('') | |
, R.prepend(renderDayHeader(i)) | |
, R.prepend(renderHeaderRow()) | |
, R.map(renderStoryRow) | |
, R.nth(i) | |
)(daily_tasks) | |
}; | |
} | |
function renderDayHeader(i) | |
{ | |
return '\n\n## ' + day_labels[i] + '\n\n'; | |
} | |
function renderHeaderRow() | |
{ | |
return '| Points | Assignee | Id | Name |\n' | |
+ '|-------:|---------:|----|------|\n' | |
; | |
} | |
function renderStoryRow(story) | |
{ | |
const components = [ | |
story.estimate | |
, story.assignee | |
, `[#${story.id}](${story.url})` | |
, story.name | |
]; | |
return '|' + components.join('|') + '|\n'; | |
} | |
function getDailyTasks(stories) | |
{ | |
const groups = groupStories(stories); | |
let daily_tasks = []; | |
for (let i = 0; i < day_labels.length; i++) | |
{ | |
const tasks = R.compose( | |
R.unnest | |
, R.reject(R.isNil) | |
, R.map(R.nth(i)) | |
)(groups); | |
if (R.isEmpty(tasks)) | |
break; | |
daily_tasks.push(tasks); | |
} | |
return daily_tasks; | |
} | |
function selectStoriesForDay(groups) | |
{ | |
return function(day) | |
{ | |
return R.map(R.nth(day))(groups); | |
} | |
} | |
function groupStories(stories) | |
{ | |
return R.compose( | |
R.map(R.dropLast(1)) | |
, R.map(R.until( | |
R.compose( | |
lastIsEmpty | |
) | |
, splitAtQuota(daily_point_quota) | |
)) | |
, R.map(R.of) | |
, groupByOwners | |
)(stories); | |
} | |
function groupByOwners(stories) | |
{ | |
return R.compose( | |
R.map(R.last) | |
, R.toPairs | |
, R.groupBy(getOwnerInitials) | |
, R.map(setAssignee) | |
)(stories); | |
} | |
function splitAtQuota(quota) | |
{ | |
return function(stories) | |
{ | |
return R.compose( | |
R.concat(R.dropLast(1, stories)) | |
, R.compose( | |
R.apply(R.splitAt) | |
, R.adjust(findIndexOfQuota(quota), 0) | |
, R.ap([mapSums, R.identity]) | |
, R.of | |
) | |
, R.last | |
)(stories) | |
} | |
} | |
function findIndexOfQuota(quota) | |
{ | |
return function(sums) | |
{ | |
return R.compose( | |
R.clamp(0, 1000) | |
, R.ifElse(R.equals(-1), R.always(sums.length), R.identity) | |
, R.findIndex(R.lt(quota)) | |
)(sums); | |
} | |
} | |
function mapSums(stories) | |
{ | |
return R.compose( | |
R.addIndex(R.reduce)( | |
function(state, x, i, xs) | |
{ | |
return R.append(R.sum(R.take(i+1, xs)), state); | |
}, | |
[] | |
) | |
, R.map(R.compose(R.defaultTo(0), R.prop('estimate'))) | |
)(stories); | |
} | |
function setAssignee(story) | |
{ | |
return R.assoc('assignee', getOwnerInitials(story))(story); | |
} | |
function getOwnerInitials(story) | |
{ | |
return R.compose( | |
R.propOr('ANON', 'initials') | |
, R.last | |
, R.prop('owners') | |
)(story); | |
} | |
function lastIsEmpty(list) | |
{ | |
return R.compose( | |
R.isEmpty | |
, R.last | |
)(list); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment