forked from gautamkrishnar/blog-post-workflow
-
Notifications
You must be signed in to change notification settings - Fork 0
/
blog-post-workflow.js
360 lines (331 loc) · 12.2 KB
/
blog-post-workflow.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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
const process = require('process');
let Parser = require('rss-parser');
const core = require('@actions/core');
const fs = require('fs');
const dateFormat = require('dateformat');
const exec = require('./exec');
const rand = require('random-seed');
/**
* Builds the new readme by replacing the readme's <!-- BLOG-POST-LIST:START --><!-- BLOG-POST-LIST:END --> tags
* @param previousContent {string}: actual readme content
* @param newContent {string}: content to add
* @return {string}: content after combining previousContent and newContent
*/
const buildReadme = (previousContent, newContent) => {
const tagNameInput = core.getInput('comment_tag_name');
const tagToLookFor = tagNameInput ? `<!-- ${tagNameInput}:` : `<!-- BLOG-POST-LIST:`;
const closingTag = '-->';
const tagNewlineFlag = core.getInput('tag_post_pre_newline');
const startOfOpeningTagIndex = previousContent.indexOf(
`${tagToLookFor}START`,
);
const endOfOpeningTagIndex = previousContent.indexOf(
closingTag,
startOfOpeningTagIndex,
);
const startOfClosingTagIndex = previousContent.indexOf(
`${tagToLookFor}END`,
endOfOpeningTagIndex,
);
if (
startOfOpeningTagIndex === -1 ||
endOfOpeningTagIndex === -1 ||
startOfClosingTagIndex === -1
) {
// Exit with error if comment is not found on the readme
core.error(
`Cannot find the comment tag on the readme:\n<!-- ${tagToLookFor}:START -->\n<!-- ${tagToLookFor}:END -->`
);
process.exit(1);
}
return [
previousContent.slice(0, endOfOpeningTagIndex + closingTag.length),
tagNewlineFlag ? '\n' : '',
newContent,
tagNewlineFlag ? '\n' : '',
previousContent.slice(startOfClosingTagIndex),
].join('');
};
/**
* Code to do git commit
* @return {Promise<void>}
*/
const commitReadme = async () => {
// Getting config
const committerUsername = core.getInput('committer_username');
const committerEmail = core.getInput('committer_email');
const commitMessage = core.getInput('commit_message');
// Doing commit and push
await exec('git', [
'config',
'--global',
'user.email',
committerEmail,
]);
if (GITHUB_TOKEN) {
// git remote set-url origin
await exec('git', ['remote', 'set-url', 'origin',
`https://${GITHUB_TOKEN}@github.com/${process.env.GITHUB_REPOSITORY}.git`]);
}
await exec('git', ['config', '--global', 'user.name', committerUsername]);
await exec('git', ['add', README_FILE_PATH]);
await exec('git', ['commit', '-m', commitMessage]);
await exec('git', ['push']);
core.info('Readme updated successfully in the upstream repository');
// Making job fail if one of the source fails
process.exit(jobFailFlag ? 1 : 0);
};
// Blog workflow code
const userAgent = core.getInput('user_agent');
const acceptHeader = core.getInput('accept_header');
// Total no of posts to display on readme, all sources combined, default: 5
const TOTAL_POST_COUNT = Number.parseInt(core.getInput('max_post_count'));
// Title trimming parameter, default: ""
const TITLE_MAX_LENGTH = core.getInput('title_max_length') ?
Number.parseInt(core.getInput('title_max_length')) : null;
// Advanced content modification parameter, default: ""
const ITEM_EXEC = core.getInput('item_exec');
// Readme path, default: ./README.md
const README_FILE_PATH = core.getInput('readme_path');
const GITHUB_TOKEN = core.getInput('gh_token');
// Filter parameters
const FILTER_PARAMS = {
stackoverflow: 'Comment by $author',
stackexchange: 'Comment by $author',
};
// Custom tags
const CUSTOM_TAGS = {};
/**
* Compound parameter parser, Updates obj with compound parameters and returns item name
* @param sourceWithParam filter source with compound param eg: stackoverflow/Comment by $author/
* @param obj object to update
* @return {string} actual source name eg: stackoverflow
*/
const updateAndParseCompoundParams = (sourceWithParam, obj) => {
const param = sourceWithParam.split('/'); // Reading params ['stackoverflow','Comment by $author', '']
if (param.length === 3) {
Object.assign(obj, {[param[0]]: param[1]});
return param[0];// Returning source name
} else {
return sourceWithParam;
}
};
/**
* Returns parsed parameterised templates as array or return null
* @param template
* @param keyName
* @return {null|string[]}
*/
const getParameterisedTemplate = (template, keyName) => {
if (template.indexOf('$' + keyName) > -1 && template.match(new RegExp('\\$' + keyName + '\\((.)*\\)', 'g'))) {
return template.match(new RegExp('\\$' + keyName + '\\((.)*\\)', 'g'))[0]
.replace('$'+ keyName +'(','')
.replace(')','')
.split(',')
.map(item=> item.trim());
} else {
return null;
}
};
core.setSecret(GITHUB_TOKEN);
const COMMENT_FILTERS = core
.getInput('filter_comments')
.trim()
.split(',')
.map((item)=>{
item = item.trim();
if (item.startsWith('stackoverflow') || item.startsWith('stackexchange')) {
return updateAndParseCompoundParams(item, FILTER_PARAMS);
} else {
return item;
}
});
core.getInput('custom_tags')
.trim()
.split(',')
.forEach((item)=> {
item = item.trim();
updateAndParseCompoundParams(item, CUSTOM_TAGS); // Creates custom tag object
});
const promiseArray = []; // Runner
const runnerNameArray = []; // To show the error/success message
let postsArray = []; // Array to store posts
let jobFailFlag = false; // Job status flag
const feedObjString = core.getInput('feed_list').trim();
// Reading feed list from the workflow input
let feedList = feedObjString.split(',').map(item => item.trim());
if (feedList.length === 0) {
core.error('Please double check the value of feed_list');
process.exit(1);
}
// filters out every medium comment (PR #4)
const ignoreMediumComments = (item) => !(COMMENT_FILTERS.indexOf('medium') !== -1 &&
item.link && item.link.includes('medium.com') &&
item.categories === undefined);
// filters out stackOverflow comments (#16)
const ignoreStackOverflowComments = (item) => !(COMMENT_FILTERS.indexOf('stackoverflow') !== -1 &&
item.link && item.link.includes('stackoverflow.com') &&
item.title.startsWith(FILTER_PARAMS.stackoverflow.replace(/\$author/g, item.author)));
// filters out stackOverflow comments (#16)
const ignoreStackExchangeComments = (item) => !(COMMENT_FILTERS.indexOf('stackexchange') !== -1 &&
item.link && item.link.includes('stackexchange.com') &&
item.title.startsWith(FILTER_PARAMS.stackexchange.replace(/\$author/g, item.author)));
const customTagArgs = Object.keys(CUSTOM_TAGS).map(
item => [CUSTOM_TAGS[item], item]);
let parser = new Parser({
'headers': {
'User-Agent': userAgent,
'Accept': acceptHeader
},
customFields: {
item: [...customTagArgs]
}
});
feedList.forEach((siteUrl) => {
runnerNameArray.push(siteUrl);
promiseArray.push(new Promise((resolve, reject) => {
parser.parseURL(siteUrl).then((data) => {
if (!data.items) {
reject('Cannot read response->item');
} else {
const responsePosts = data.items;
const posts = responsePosts
.filter(ignoreMediumComments)
.filter(ignoreStackOverflowComments)
.filter(ignoreStackExchangeComments)
.map((item) => {
// Validating keys to avoid errors
if (!item.pubDate) {
reject('Cannot read response->item->pubDate');
}
if (!item.title) {
reject('Cannot read response->item->title');
}
if (!item.link) {
reject('Cannot read response->item->link');
}
// Custom tags
let customTags = {};
Object.keys(CUSTOM_TAGS).forEach((tag)=> {
if (item[tag]) {
Object.assign(customTags, {[tag]: item[tag]});
}
});
let post = {
title: item.title.trim(),
url: item.link.trim(),
date: new Date(item.pubDate.trim()),
...customTags
};
// Advanced content manipulation using javascript code
if (ITEM_EXEC) {
try {
eval(ITEM_EXEC);
} catch (e) {
core.error('Failure in executing `item_exec` parameter');
core.error(e);
process.exit(1);
}
}
if (TITLE_MAX_LENGTH && post && post.title) {
// Trimming the title
post.title = post.title.trim().slice(0, TITLE_MAX_LENGTH) === post.title.trim() ?
post.title.trim() : post.title.trim().slice(0, TITLE_MAX_LENGTH) + '...';
}
return post;
});
resolve(posts);
}
}).catch(reject);
}));
});
// Processing the generated promises
Promise.allSettled(promiseArray).then((results) => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
// Succeeded
core.info(runnerNameArray[index] + ' runner succeeded. Post count: ' + result.value.length);
postsArray.push(...result.value);
} else {
jobFailFlag = true;
// Rejected
core.error(runnerNameArray[index] + ' runner failed, please verify the configuration. Error:');
core.error(result.reason);
}
});
}).finally(() => {
// Ignore null items, allows you to ignore items by setting null in post via `item_exec`
postsArray = postsArray.filter(item => item !== null);
// Sorting posts based on date
if (core.getInput('disable_sort') === 'false') {
postsArray.sort(function (a, b) {
return b.date - a.date;
});
}
// Slicing with the max count
postsArray = postsArray.slice(0, TOTAL_POST_COUNT);
if (postsArray.length > 0) {
try {
if (!process.env.TEST_MODE) {
exec('git', ['config','pull.rebase', 'true']);
exec('git',['pull']); // Pulling the latest changes from upstream
}
const readmeData = fs.readFileSync(README_FILE_PATH, 'utf8');
const template = core.getInput('template');
const randEmojiArr = getParameterisedTemplate(template, 'randomEmoji');
const constEmojiArr = getParameterisedTemplate(template, 'emojiKey');
const postListMarkdown = postsArray.reduce((acc, cur, index) => {
if (template === 'default') {
// Default template: - [$title]($url)
return acc + `\n- [${cur.title}](${cur.url})` + (((index + 1) === postsArray.length) ? '\n' : '');
} else {
// Building with custom template
const date = dateFormat(cur.date, core.getInput('date_format')); // Formatting date
let content = template
.replace(/\$title\b/g, cur.title)
.replace(/\$url\b/g, cur.url)
.replace(/\$date\b/g, date)
.replace(/\$newline/g, '\n');
// Setting Custom tags to the template
Object.keys(CUSTOM_TAGS).forEach((tag)=> {
const replaceValue = cur[tag] ? cur[tag] : '';
content = content.replace(new RegExp('\\$' + tag + '\\b', 'g'), replaceValue);
});
// Emoji implementation: Random
if (randEmojiArr) {
// For making randomness unique for each repos
const seed = (process.env.GITHUB_REPOSITORY && !process.env.TEST_MODE ?
process.env.GITHUB_REPOSITORY : 'example') + index;
const emoji = randEmojiArr[rand.create(seed).range(randEmojiArr.length)];
content = content.replace(/\$randomEmoji\((\S)*\)/g, emoji);
}
// Emoji implementation: Static
if (constEmojiArr) {
// using modulus
content = content.replace(/\$emojiKey\((\S)*\)/g, constEmojiArr[index % constEmojiArr.length]);
}
return acc + content;
}
}, '');
const newReadme = buildReadme(readmeData, postListMarkdown);
// if there's change in readme file update it
if (newReadme !== readmeData) {
core.info('Writing to ' + README_FILE_PATH);
fs.writeFileSync(README_FILE_PATH, newReadme);
if (!process.env.TEST_MODE) {
// noinspection JSIgnoredPromiseFromCall
commitReadme();
}
} else {
core.info('No change detected, skipping');
process.exit(0);
}
} catch (e) {
core.error(e);
process.exit(1);
}
} else {
core.info('0 blog posts fetched');
process.exit(jobFailFlag ? 1 : 0);
}
});