-
Notifications
You must be signed in to change notification settings - Fork 442
/
deliver-estimation-report.js
176 lines (143 loc) · 9.51 KB
/
deliver-estimation-report.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
module.exports = {
friendlyName: 'Deliver estimation report',
description: 'Send estimation report to Slack.',
exits: {
success: {
description: 'It worked. The estimation report was sent to Slack.'
},
},
fn: async function () {
// ██████╗ ███████╗████████╗ ██████╗ ███████╗██████╗ ██████╗ ██████╗ ████████╗
// ██╔════╝ ██╔════╝╚══██╔══╝ ██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝
// ██║ ███╗█████╗ ██║ ██████╔╝█████╗ ██████╔╝██║ ██║██████╔╝ ██║
// ██║ ██║██╔══╝ ██║ ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██╔══██╗ ██║
// ╚██████╔╝███████╗ ██║ ██║ ██║███████╗██║ ╚██████╔╝██║ ██║ ██║
// ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
//
sails.log('Getting estimation report...');
if (!sails.config.custom.githubAccessToken) {
throw new Error('No GitHub access token configured! (Please set `sails.config.custom.githubAccessToken`.)');
}//•
let baseHeaders = {
'User-Agent': 'fleet story points',
'Authorization': `token ${sails.config.custom.githubAccessToken}`
};
let estimationReport = {};
// Fetch projects
let projects = await sails.helpers.http.get(`https://api.github.com/orgs/fleetdm/projects`, {}, baseHeaders).retry();// let projects = [];// « hack if you get rate limited and want to test beta projets
// This nasty little hack mixes in new "beta" projects that are part of Github Projects 2.0 (beta)
// but makes them look like normal projects from the actually-documented GitHub REST API.
// > [?] https://docs.github.com/en/enterprise-cloud@latest/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projects#finding-the-node-id-of-a-field
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// PS. In case you have to do anything with graphql ever again, try uncommenting this.
// ```
// console.log(
// require('util').inspect(
// await sails.helpers.http.post(`https://api.github.com/graphql`,{
// query:'{organization(login: "fleetdm") {projectsNext(first: 20) {nodes {id databaseId title fields(first: 20) {nodes {id name settings}} items(first: 20) {nodes{title id fieldValues(first: 8) {nodes{value projectField{name}}} content{...on Issue {repository{name} labels(first:20) {nodes{name}} assignees(first: 10) {nodes{login}}}}}} }}}}'
// }, baseHeaders),
// {depth:null}
// )
// );
// console.log();
// console.log();
// console.log('-0--------------');
// console.log();
// // return;
// ```
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
let graphqlHairball = await sails.helpers.http.post(`https://api.github.com/graphql`,{
query:'{organization(login: "fleetdm") {projectsNext(first: 20) {nodes {id databaseId title fields(first: 20) {nodes {id name settings}} items(first: 20) {nodes{title id fieldValues(first: 8) {nodes{value projectField{name}}} content{...on Issue {repository{name} labels(first:20) {nodes{name}} assignees(first: 10) {nodes{login}}}}}} }}}}'
}, baseHeaders).retry();
projects = projects.concat(
graphqlHairball.data.organization.projectsNext.nodes.map((betaProject) => ({
_isBetaProject: true,// « we need this because some APIs only work for one kind of project or the other
_betaProjectColumns: JSON.parse(_.find(betaProject.fields.nodes, {name: 'Status'}).settings).options.map((betaColumn) => ({
_isBetaColumn: true,
_betaStatusId: betaColumn.id,
name: betaColumn.name,
})),
_betaProjectCards: betaProject.items.nodes.filter((betaCard) => (
betaCard.content && betaCard.content.labels && betaCard.content.labels.nodes &&
betaCard.fieldValues && _.find(betaCard.fieldValues.nodes, (fieldValueNode) => fieldValueNode.projectField.name === 'Status')
)).map((betaCard) => ({
_isBetaCard: true,
_betaStatusId: _.find(betaCard.fieldValues.nodes, (fieldValueNode) => fieldValueNode.projectField.name === 'Status').value,
labels: betaCard.content.labels.nodes
})),
name: betaProject.title,// « it's been renamed for some reason
node_id: betaProject.id,// eslint-disable-line camelcase
id: betaProject.databaseId// « the good ole ID for the rest of us ("node_id" is the graphql ID)
}))
);// </hack>
// console.log(require('util').inspect(projects, {depth:null}));
// return;
await sails.helpers.flow.simultaneouslyForEach(projects, async(project)=>{
estimationReport[project.name] = {};
let columns;
if (!project._isBetaProject) {
columns = await sails.helpers.http.get(`https://api.github.com/projects/${project.id}/columns`, {}, baseHeaders).retry();
} else {
columns = project._betaProjectColumns;// [?] https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/objects#projectnextitem
}
await sails.helpers.flow.simultaneouslyForEach(columns, async(column)=>{
estimationReport[project.name][column.name] = 0;
let cards;
if (!project._isBetaProject) {
cards = await sails.helpers.http.get(`https://api.github.com/projects/columns/${column.id}/cards`, {}, baseHeaders).retry();
} else {
cards = project._betaProjectCards.filter((betaCard) => betaCard._betaStatusId === column._betaStatusId);
}
await sails.helpers.flow.simultaneouslyForEach(cards, async(card)=>{
// Get the number of story points associated with this card.
let numPoints = 0;
let labels;
if (!project._isBetaProject) {
if (!card.content_url) {
// ignore "notes" (FUTURE: Maybe add some kind of sniffing for a prefix like "[5]")
labels = [];
} else {
let issue = await sails.helpers.http.get(card.content_url, {}, baseHeaders).retry();
labels = issue.labels;
}
} else {
labels = card.labels;
}
let pointLabels = labels.filter((label)=> Number(label.name) >= 1 && Number(label.name) < Infinity);
if (pointLabels.length >= 2) { throw new Error(`Cannot have more than one story point label, but this card ${require('util').inspect(card, {depth:null})} seems to have more than one: ${_.pluck(pointLabels,'name')}`); }
if (pointLabels.length === 0) {
numPoints = 0;
} else {
numPoints = Number(pointLabels[0].name);
}
estimationReport[project.name][column.name] += numPoints;
});//∞
});//∞
});//∞
// ██████╗ ██████╗ ███████╗████████╗ ████████╗ ██████╗
// ██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝ ╚══██╔══╝██╔═══██╗
// ██████╔╝██║ ██║███████╗ ██║ ██║ ██║ ██║
// ██╔═══╝ ██║ ██║╚════██║ ██║ ██║ ██║ ██║
// ██║ ╚██████╔╝███████║ ██║ ██║ ╚██████╔╝
// ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝
//
// ███████╗██╗ █████╗ ██████╗██╗ ██╗
// ██╔════╝██║ ██╔══██╗██╔════╝██║ ██╔╝
// ███████╗██║ ███████║██║ █████╔╝
// ╚════██║██║ ██╔══██║██║ ██╔═██╗
// ███████║███████╗██║ ██║╚██████╗██║ ██╗
// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
//
sails.log('Delivering estimation report to Slack...');
if (!sails.config.custom.slackWebhookUrlForGithubEstimates) {
throw new Error(
'Estimation report not delivered: slackWebhookUrlForGithubEstimates needs to be configured in sails.config.custom. Here\'s the undelivered report: ' +
`${require('util').inspect(estimationReport, {depth:null})}`
);
} else {
await sails.helpers.http.post(sails.config.custom.slackWebhookUrlForGithubEstimates, {
text: `New estimation report:\n${require('util').inspect(estimationReport, {depth:null})}`
}).retry();
}
}
};