-
Notifications
You must be signed in to change notification settings - Fork 1
/
action.js
317 lines (297 loc) · 11.7 KB
/
action.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/**
* @type {typeof import('@actions/core')}
*/
let coreGlob
/**
* @param {import('@octokit/core').Octokit & {rest : import('@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types').RestEndpointMethods }} github
* @param {import('@actions/github').context} context
* @param {typeof import('@actions/core')} core
* @param {string} organization GitHub organization name
* @param {number} projectNumber project ID as seen in project board URL
* @param {string} statusName status field name to be set
* @param {string} prStatusValue PR status name to be assigned
* @param {string} issueStatusValue Issue status name to be assigned
* @param {Boolean} includeEffort if true, set effort
* @param {string} effortName effort field name to be set
* @param {string} effortMapping JSON effort name - days map
* @param {string} monthlyMilestoneName monthly milestone field name to be set
* @param {string} quarterlyMilestoneName quarterly milestone field name to be set
* @param {string} basePath base action path on runner FS
*/
module.exports = async (
github,
context,
core,
organization = '',
projectNumber,
statusName = 'status',
prStatusValue = 'todo',
issueStatusValue = 'todo',
includeEffort = true,
effortName = 'effort',
effortMapping = '[{"pattern": "two days", "value": 2},{"pattern": "the longest one", "value": 1e1000}]',
monthlyMilestoneName = 'monthly milestone',
quarterlyMilestoneName = 'quarterly milestone',
basePath = '.'
) => {
coreGlob = core;
if (typeof projectNumber !== 'number')
bail("missing params");
const fs = require('fs');
// get project data
const projectDataQuery = fs.readFileSync(`${basePath}/graphql/projectData.gql`, 'utf8');
const projectDataParams = {
owner: organization ? organization : context.repo.owner,
number: projectNumber
};
let projectData;
try {
projectData = await github.graphql(projectDataQuery, projectDataParams);
} catch (error) {
bail(error.message);
};
if (!projectData.organization.projectV2.fields.nodes)
bail("couldn't retrieve project fields");
const projectFieldOptions = projectData.organization.projectV2.fields.nodes;
if (!projectData.organization.projectV2.id)
bail("couldn't retrieve project graphql id");
const projectId = projectData.organization.projectV2.id;
// get todo status
let statusFieldId;
let prStatusValueId;
let issueStatusValueId;
projectFieldOptions.forEach(field => {
if (field.name === statusName) {
statusFieldId = field.id;
field.options.forEach(status => {
if (status.name.toLowerCase().includes(prStatusValue.toLowerCase()))
prStatusValueId = status.id;
if (status.name.toLowerCase().includes(issueStatusValue.toLowerCase()))
issueStatusValueId = status.id;
});
if (!prStatusValueId)
bail("cannot find PR target status")
if (!issueStatusValueId)
bail("cannot find Issue target status")
};
});
// get monthly milestone
let monthlyMilestoneFieldId;
let monthlyMilestoneValueId;
projectFieldOptions.forEach(field => {
if (field.name === monthlyMilestoneName) {
monthlyMilestoneFieldId = field.id;
monthlyMilestoneValueId = getCurrentIteration(field.configuration.iterations);
};
});
// get quarterly milestone
let quarterlyMilestoneFieldId;
let quarterlyMilestoneValueId;
projectFieldOptions.forEach(field => {
if (field.name === quarterlyMilestoneName) {
quarterlyMilestoneFieldId = field.id;
quarterlyMilestoneValueId = getCurrentIteration(field.configuration.iterations);
};
});
// is it a PR or an Issue
let isPr = false;
if (context.eventName === 'pull_request')
isPr = true;
// get PR / Issue id
const prIssueId = await getPrIssueId(github, context);
if (!prIssueId)
bail("couldn't get ID of PR/Issue");
// move Issue to project
let projectItemId;
if (!isPr) {
const assignItemQuery = fs.readFileSync(`${basePath}/graphql/projectAssignPrIssue.gql`, 'utf8');
const assignItemParams = {
project: projectId,
id: prIssueId
};
try {
({ addProjectV2ItemById: { item: { id: projectItemId } } } = await github.graphql(assignItemQuery, assignItemParams));
} catch (error) {
bail(error.message);
};
};
let effortMessage;
let isDraftPr;
if (isPr) {
// assign author if a PR
const assigneeData = await github.rest.users.getByUsername({
// not implemented in ('@actions/github').context
// deserialized JSON from GITHUB_EVENT_PATH (/github/workflow/event.json)
// https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=opened#pull_request
username: context.payload.pull_request.user.login
});
const assignPrToUserQuery = fs.readFileSync(`${basePath}/graphql/prAssignUser.gql`, 'utf8');
const assignPrToUserParams = {
assignee: assigneeData.data.node_id,
id: prIssueId
};
try {
await github.graphql(assignPrToUserQuery, assignPrToUserParams);
} catch (error) {
bail(error.message);
};
// get PR data
const prCommitDataQuery = fs.readFileSync(`${basePath}/graphql/prCommitData.gql`, 'utf8');
const prCommitDataParams = {
owner: context.repo.owner,
name: context.repo.repo,
number: context.payload.pull_request.number
};
let prCommitData;
try {
let prAllData = await github.graphql(prCommitDataQuery, prCommitDataParams);
prCommitData = prAllData.repository.pullRequest.commits.nodes;
isDraftPr = prAllData.repository.pullRequest.isDraft;
} catch (error) {
bail(error.message);
};
// leave drafts alone
if (isDraftPr) {
coreGlob.info("detected PR draft, skipping project assignment");
return;
}
// move PR to project
const assignItemQuery = fs.readFileSync(`${basePath}/graphql/projectAssignPrIssue.gql`, 'utf8');
const assignItemParams = {
project: projectId,
id: prIssueId
};
try {
({ addProjectV2ItemById: { item: { id: projectItemId } } } = await github.graphql(assignItemQuery, assignItemParams));
} catch (error) {
bail(error.message);
};
// comment on effort if a PR
if (includeEffort) {
let effortMappingObj = JSON.parse(effortMapping);
effortMessage = `Please set effort on project item card:\n\n`;
// list human-readable efforts
projectFieldOptions.forEach(field => {
if (field.name === effortName) {
field.options.forEach(effort => {
effortMappingObj.forEach(element => {
if (effort.name.toLowerCase() === element.pattern.toLowerCase()) {
if (element.value !== Infinity) {
effortMessage += ` - ${effort.name}: ${element.value} day(s) or less,\n`;
} else {
effortMessage += ` - ${effort.name}: longer than any of above.\n`;
};
}
});
});
};
});
};
};
if (isPr) { // set status, milestones & notify about effort if a PR
const assignProjectFieldsQuery = fs.readFileSync(`${basePath}/graphql/projectNoEffortItemAssignFields.gql`, 'utf8');
const assignProjectFieldsParams = {
project: projectId,
item: projectItemId,
status_field: statusFieldId,
status_value: prStatusValueId,
primary_milestone_field: monthlyMilestoneFieldId,
primary_milestone_value: monthlyMilestoneValueId,
secondary_milestone_field: quarterlyMilestoneFieldId,
secondary_milestone_value: quarterlyMilestoneValueId
};
try {
await github.graphql(assignProjectFieldsQuery, assignProjectFieldsParams);
} catch (error) {
bail(error.message);
};
if (includeEffort) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: effortMessage
});
coreGlob.info("set project fields suggesting effort");
} else {
coreGlob.info("set project fields omitting effort");
}
} else { // set status if an Issue
const assignProjectFieldsQuery = fs.readFileSync(`${basePath}/graphql/projectIssueItemAssignFields.gql`, 'utf8');
const assignProjectFieldsParams = {
project: projectId,
item: projectItemId,
status_field: statusFieldId,
status_value: issueStatusValueId,
};
try {
await github.graphql(assignProjectFieldsQuery, assignProjectFieldsParams);
} catch (error) {
bail(error.message);
};
coreGlob.info("set project fields omitting effort & milestones");
}
}
/**
* @param {string} msg
*/
function bail(msg) {
coreGlob.setFailed(msg);
throw new Error(msg);
}
/**
* @param {Date} startDate date to count from
* @returns {number} number of working days since input date
*/
function countWorkingDaysSince(startDate) {
const currentDate = new Date();
let workingDaysCount = 0;
startDate.setHours(0, 0, 0, 0);
for (; startDate <= currentDate; startDate.setDate(startDate.getDate() + 1)) {
const dayOfWeek = startDate.getDay();
if (dayOfWeek != 0 && dayOfWeek != 6) {
workingDaysCount++;
};
};
return workingDaysCount - 1;
}
/**
* @param {{startDate: string, duration: number, id: string}[]} iterations list of Iterations from GH API
* @returns {string} node_id of iteration entry matching current date
*/
function getCurrentIteration(iterations) {
let monthlyMilestoneValueId;
iterations.forEach(iteration => {
const now = new Date();
const startDate = new Date(iteration.startDate);
let endDate = new Date(iteration.startDate);
endDate.setDate(endDate.getDate() + iteration.duration);
if (startDate < now && now < endDate)
monthlyMilestoneValueId = iteration.id;
});
return monthlyMilestoneValueId;
}
/**
* @param {import('@octokit/core').Octokit & {rest : import('@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types').RestEndpointMethods }} github
* @param {import('@actions/github').context} context
* @returns {Promise<string>} node_id of PR or Issue that triggered calling workflow
*/
async function getPrIssueId(github, context) {
if (context.payload.pull_request) {
const apiPullRequest = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});
return apiPullRequest.data.node_id;
}
if (context.payload.issue) {
const apiIssue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
});
return apiIssue.data.node_id;
}
bail("couldn't get ID");
}