Skip to content

Commit 37cbf92

Browse files
chore(ci): added labeling and notification for published PRs; (#6059)
1 parent dd465ab commit 37cbf92

13 files changed

Lines changed: 373 additions & 5 deletions

File tree

.github/workflows/publish.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,8 @@ jobs:
5353
run: npm publish
5454
env:
5555
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
56+
###### NOTIFY & TAG published PRs ######
57+
- name: Notify and tag published PRs
58+
env:
59+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
run: node ./bin/actions/notify_published.js --tag v${{ steps.package-version.outputs.current-version }}

bin/GithubAPI.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import util from "util";
2+
import cp from "child_process";
3+
import {parseVersion} from "./helpers/parser.js";
4+
import githubAxios from "./githubAxios.js";
5+
import memoize from 'memoizee';
6+
7+
const exec = util.promisify(cp.exec);
8+
9+
export default class GithubAPI {
10+
constructor(owner, repo) {
11+
if (!owner) {
12+
throw new Error('repo owner must be specified');
13+
}
14+
15+
if (!repo) {
16+
throw new Error('repo must be specified');
17+
}
18+
19+
this.repo = repo;
20+
this.owner = owner;
21+
this.axios = githubAxios.create({
22+
baseURL: `https://api.github.com/repos/${this.owner}/${this.repo}/`,
23+
})
24+
}
25+
26+
async createComment(issue, body) {
27+
return (await this.axios.post(`/issues/${issue}/comments`, {body})).data;
28+
}
29+
30+
async getComments(issue, {desc = false, per_page= 100, page = 1}) {
31+
return (await this.axios.get(`/issues/${issue}/comments`, {params: {direction: desc ? 'desc' : 'asc', per_page, page}})).data;
32+
}
33+
34+
async getComment(id) {
35+
return (await this.axios.get(`/issues/comments/${id}`)).data;
36+
}
37+
38+
async updateComment(id, body) {
39+
return (await this.axios.patch(`/issues/comments/${id}`, {body})).data;
40+
}
41+
42+
async appendLabels(issue, labels) {
43+
return (await this.axios.post(`issues/${issue}/labels`, {labels})).data;
44+
}
45+
46+
async getUser(user) {
47+
return (await this.axios.get(`users/${user}`)).data;
48+
}
49+
50+
async isCollaborator(user) {
51+
try {
52+
return (await this.axios.get(`/collaborators/${user}`)).status === 204;
53+
} catch (e) {
54+
55+
}
56+
}
57+
58+
async deleteLabel(issue, label) {
59+
return (await this.axios.delete(`/issues/${issue}/labels/${label}`)).data;
60+
}
61+
62+
async getIssue(issue) {
63+
return (await this.axios.get(`/issues/${issue}`)).data;
64+
}
65+
66+
async getPR(issue) {
67+
return (await this.axios.get(`/pulls/${issue}`)).data;
68+
}
69+
70+
async getIssues({state= 'open', labels, sort = 'created', desc = false, per_page = 100, page = 1}) {
71+
return (await this.axios.get(`/issues`, {params: {state, labels, sort, direction: desc ? 'desc' : 'asc', per_page, page}})).data;
72+
}
73+
74+
async updateIssue(issue, data) {
75+
return (await this.axios.patch(`/issues/${issue}`, data)).data;
76+
}
77+
78+
async closeIssue(issue) {
79+
return this.updateIssue(issue, {
80+
state: "closed"
81+
})
82+
}
83+
84+
async getReleases({per_page = 30, page= 1} = {}) {
85+
return (await this.axios.get(`/releases`, {params: {per_page, page}})).data;
86+
}
87+
88+
async getRelease(release = 'latest') {
89+
return (await this.axios.get(parseVersion(release) ? `/releases/tags/${release}` : `/releases/${release}`)).data;
90+
}
91+
92+
async getTags({per_page = 30, page= 1} = {}) {
93+
return (await this.axios.get(`/tags`, {params: {per_page, page}})).data;
94+
}
95+
96+
async reopenIssue(issue) {
97+
return this.updateIssue(issue, {
98+
state: "open"
99+
})
100+
}
101+
102+
static async getTagRef(tag) {
103+
try {
104+
return (await exec(`git show-ref --tags "refs/tags/${tag}"`)).stdout.split(' ')[0];
105+
} catch (e) {
106+
}
107+
}
108+
}
109+
110+
const {prototype} = GithubAPI;
111+
112+
['getUser', 'isCollaborator'].forEach(methodName => {
113+
prototype[methodName] = memoize(prototype[methodName], { promise: true })
114+
});
115+
116+
['get', 'post', 'put', 'delete', 'isAxiosError'].forEach((method) => prototype[method] = function(...args){
117+
return this.axios[method](...args);
118+
});
119+

bin/RepoBot.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import GithubAPI from "./GithubAPI.js";
2+
import api from './api.js';
3+
import Handlebars from "handlebars";
4+
import fs from "fs/promises";
5+
import {colorize} from "./helpers/colorize.js";
6+
import {getReleaseInfo} from "./contributors.js";
7+
8+
const normalizeTag = (tag) => tag.replace(/^v/, '');
9+
10+
class RepoBot {
11+
constructor(options) {
12+
const {
13+
owner, repo,
14+
templates
15+
} = options || {};
16+
17+
this.templates = Object.assign({
18+
published: '../templates/pr_published.hbs'
19+
}, templates);
20+
21+
this.github = api || new GithubAPI(owner, repo);
22+
23+
this.owner = this.github.owner;
24+
this.repo = this.github.repo;
25+
}
26+
27+
async addComment(targetId, message) {
28+
return this.github.createComment(targetId, message);
29+
}
30+
31+
async notifyPRPublished(id, tag) {
32+
const pr = await this.github.getPR(id);
33+
34+
tag = normalizeTag(tag);
35+
36+
const {merged, labels, user: {login, type}} = pr;
37+
38+
const isBot = type === 'Bot';
39+
40+
if (!merged) {
41+
return false
42+
}
43+
44+
await this.github.appendLabels(id, ['v' + tag]);
45+
46+
if (isBot || labels.find(({name}) => name === 'automated pr') || (await this.github.isCollaborator(login))) {
47+
return false;
48+
}
49+
50+
const author = await this.github.getUser(login);
51+
52+
author.isBot = isBot;
53+
54+
const message = await this.constructor.renderTemplate(this.templates.published, {
55+
id,
56+
author,
57+
release: {
58+
tag,
59+
url: `https://github.com/${this.owner}/${this.repo}/releases/tag/v${tag}`
60+
}
61+
});
62+
63+
return await this.addComment(id, message);
64+
}
65+
66+
async notifyPublishedPRs(tag) {
67+
const release = await getReleaseInfo(tag);
68+
69+
if (!release) {
70+
throw Error(colorize()`Can't get release info for ${tag}`);
71+
}
72+
73+
const {merges} = release;
74+
75+
console.log(colorize()`Found ${merges.length} PRs in ${tag}:`);
76+
77+
let i = 0;
78+
79+
for (const pr of merges) {
80+
try {
81+
console.log(colorize()`${i++}) Notify PR #${pr.id}`)
82+
const result = await this.notifyPRPublished(pr.id, tag);
83+
console.log(result ? 'OK' : 'Skipped');
84+
} catch (err) {
85+
console.warn(colorize('green', 'red')` Failed notify PR ${pr.id}: ${err.message}`);
86+
}
87+
}
88+
}
89+
90+
static async renderTemplate(template, data) {
91+
return Handlebars.compile(String(await fs.readFile(template)))(data);
92+
}
93+
}
94+
95+
export default RepoBot;

bin/actions/notify_published.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import minimist from "minimist";
2+
import RepoBot from '../RepoBot.js';
3+
4+
const argv = minimist(process.argv.slice(2));
5+
console.log(argv);
6+
7+
const tag = argv.tag;
8+
9+
if (!tag) {
10+
throw new Error('tag must be specified');
11+
}
12+
13+
const bot = new RepoBot();
14+
15+
(async() => {
16+
try {
17+
await bot.notifyPublishedPRs(tag);
18+
} catch (err) {
19+
console.warn('Error:', err.message);
20+
}
21+
})();
22+

bin/api.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import GithubAPI from "./GithubAPI.js";
2+
3+
export default new GithubAPI('axios', 'axios');

bin/contributors.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from "./githubAPI.js";
1+
import axios from "./githubAxios.js";
22
import util from "util";
33
import cp from "child_process";
44
import Handlebars from "handlebars";
@@ -7,6 +7,8 @@ import {colorize} from "./helpers/colorize.js";
77

88
const exec = util.promisify(cp.exec);
99

10+
const ONE_MB = 1024 * 1024;
11+
1012
const removeExtraLineBreaks = (str) => str.replace(/(?:\r\n|\r|\n){3,}/gm, '\r\n\r\n');
1113

1214
const cleanTemplate = template => template
@@ -108,7 +110,11 @@ const getReleaseInfo = ((releaseCache) => async (tag) => {
108110
version ? '--starting-version ' + version + ' --ending-version ' + version : ''
109111
} --stdout --commit-limit false --template json`;
110112

111-
const release = JSON.parse((await exec(command)).stdout)[0];
113+
console.log(command);
114+
115+
const {stdout} = await exec(command, {maxBuffer: 10 * ONE_MB});
116+
117+
const release = JSON.parse(stdout)[0];
112118

113119
if(release) {
114120
const authors = {};
@@ -229,6 +235,7 @@ const getTagRef = async (tag) => {
229235

230236
export {
231237
renderContributorsList,
238+
getReleaseInfo,
232239
renderPRsList,
233240
getTagRef
234241
}
File renamed without changes.

bin/helpers/colorize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import chalk from 'chalk';
22

33
export const colorize = (...colors)=> {
44
if(!colors.length) {
5-
colors = ['green', 'magenta', 'cyan', 'blue', 'yellow', 'red'];
5+
colors = ['green', 'cyan', 'magenta', 'blue', 'yellow', 'red'];
66
}
77

88
const colorsCount = colors.length;

bin/helpers/parser.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const matchAll = (text, regexp, cb) => {
2+
let match;
3+
while((match = regexp.exec(text))) {
4+
cb(match);
5+
}
6+
}
7+
8+
export const parseSection = (body, name, cb) => {
9+
matchAll(body, new RegExp(`^(#+)\\s+${name}?(.*?)^\\1\\s+\\w+`, 'gims'), cb);
10+
}
11+
12+
export const parseVersion = (rawVersion) => /^v?(\d+).(\d+).(\d+)/.exec(rawVersion);

gulpfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import gulp from 'gulp';
22
import fs from 'fs-extra';
3-
import axios from './bin/githubAPI.js';
3+
import axios from './bin/githubAxios.js';
44
import minimist from 'minimist'
55

66
const argv = minimist(process.argv.slice(2));

0 commit comments

Comments
 (0)