Skip to content

Commit

Permalink
chore(ci): added labeling and notification for published PRs; (axios#…
Browse files Browse the repository at this point in the history
  • Loading branch information
DigitalBrainJS committed Nov 8, 2023
1 parent dd465ab commit 37cbf92
Show file tree
Hide file tree
Showing 13 changed files with 373 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ jobs:
run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
###### NOTIFY & TAG published PRs ######
- name: Notify and tag published PRs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node ./bin/actions/notify_published.js --tag v${{ steps.package-version.outputs.current-version }}
119 changes: 119 additions & 0 deletions bin/GithubAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import util from "util";
import cp from "child_process";
import {parseVersion} from "./helpers/parser.js";
import githubAxios from "./githubAxios.js";
import memoize from 'memoizee';

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

export default class GithubAPI {
constructor(owner, repo) {
if (!owner) {
throw new Error('repo owner must be specified');
}

if (!repo) {
throw new Error('repo must be specified');
}

this.repo = repo;
this.owner = owner;
this.axios = githubAxios.create({
baseURL: `https://api.github.com/repos/${this.owner}/${this.repo}/`,
})
}

async createComment(issue, body) {
return (await this.axios.post(`/issues/${issue}/comments`, {body})).data;
}

async getComments(issue, {desc = false, per_page= 100, page = 1}) {
return (await this.axios.get(`/issues/${issue}/comments`, {params: {direction: desc ? 'desc' : 'asc', per_page, page}})).data;
}

async getComment(id) {
return (await this.axios.get(`/issues/comments/${id}`)).data;
}

async updateComment(id, body) {
return (await this.axios.patch(`/issues/comments/${id}`, {body})).data;
}

async appendLabels(issue, labels) {
return (await this.axios.post(`issues/${issue}/labels`, {labels})).data;
}

async getUser(user) {
return (await this.axios.get(`users/${user}`)).data;
}

async isCollaborator(user) {
try {
return (await this.axios.get(`/collaborators/${user}`)).status === 204;
} catch (e) {

}
}

async deleteLabel(issue, label) {
return (await this.axios.delete(`/issues/${issue}/labels/${label}`)).data;
}

async getIssue(issue) {
return (await this.axios.get(`/issues/${issue}`)).data;
}

async getPR(issue) {
return (await this.axios.get(`/pulls/${issue}`)).data;
}

async getIssues({state= 'open', labels, sort = 'created', desc = false, per_page = 100, page = 1}) {
return (await this.axios.get(`/issues`, {params: {state, labels, sort, direction: desc ? 'desc' : 'asc', per_page, page}})).data;
}

async updateIssue(issue, data) {
return (await this.axios.patch(`/issues/${issue}`, data)).data;
}

async closeIssue(issue) {
return this.updateIssue(issue, {
state: "closed"
})
}

async getReleases({per_page = 30, page= 1} = {}) {
return (await this.axios.get(`/releases`, {params: {per_page, page}})).data;
}

async getRelease(release = 'latest') {
return (await this.axios.get(parseVersion(release) ? `/releases/tags/${release}` : `/releases/${release}`)).data;
}

async getTags({per_page = 30, page= 1} = {}) {
return (await this.axios.get(`/tags`, {params: {per_page, page}})).data;
}

async reopenIssue(issue) {
return this.updateIssue(issue, {
state: "open"
})
}

static async getTagRef(tag) {
try {
return (await exec(`git show-ref --tags "refs/tags/${tag}"`)).stdout.split(' ')[0];
} catch (e) {
}
}
}

const {prototype} = GithubAPI;

['getUser', 'isCollaborator'].forEach(methodName => {
prototype[methodName] = memoize(prototype[methodName], { promise: true })
});

['get', 'post', 'put', 'delete', 'isAxiosError'].forEach((method) => prototype[method] = function(...args){
return this.axios[method](...args);
});

95 changes: 95 additions & 0 deletions bin/RepoBot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import GithubAPI from "./GithubAPI.js";
import api from './api.js';
import Handlebars from "handlebars";
import fs from "fs/promises";
import {colorize} from "./helpers/colorize.js";
import {getReleaseInfo} from "./contributors.js";

const normalizeTag = (tag) => tag.replace(/^v/, '');

class RepoBot {
constructor(options) {
const {
owner, repo,
templates
} = options || {};

this.templates = Object.assign({
published: '../templates/pr_published.hbs'
}, templates);

this.github = api || new GithubAPI(owner, repo);

this.owner = this.github.owner;
this.repo = this.github.repo;
}

async addComment(targetId, message) {
return this.github.createComment(targetId, message);
}

async notifyPRPublished(id, tag) {
const pr = await this.github.getPR(id);

tag = normalizeTag(tag);

const {merged, labels, user: {login, type}} = pr;

const isBot = type === 'Bot';

if (!merged) {
return false
}

await this.github.appendLabels(id, ['v' + tag]);

if (isBot || labels.find(({name}) => name === 'automated pr') || (await this.github.isCollaborator(login))) {
return false;
}

const author = await this.github.getUser(login);

author.isBot = isBot;

const message = await this.constructor.renderTemplate(this.templates.published, {
id,
author,
release: {
tag,
url: `https://github.com/${this.owner}/${this.repo}/releases/tag/v${tag}`
}
});

return await this.addComment(id, message);
}

async notifyPublishedPRs(tag) {
const release = await getReleaseInfo(tag);

if (!release) {
throw Error(colorize()`Can't get release info for ${tag}`);
}

const {merges} = release;

console.log(colorize()`Found ${merges.length} PRs in ${tag}:`);

let i = 0;

for (const pr of merges) {
try {
console.log(colorize()`${i++}) Notify PR #${pr.id}`)
const result = await this.notifyPRPublished(pr.id, tag);
console.log(result ? 'OK' : 'Skipped');
} catch (err) {
console.warn(colorize('green', 'red')` Failed notify PR ${pr.id}: ${err.message}`);
}
}
}

static async renderTemplate(template, data) {
return Handlebars.compile(String(await fs.readFile(template)))(data);
}
}

export default RepoBot;
22 changes: 22 additions & 0 deletions bin/actions/notify_published.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import minimist from "minimist";
import RepoBot from '../RepoBot.js';

const argv = minimist(process.argv.slice(2));
console.log(argv);

const tag = argv.tag;

if (!tag) {
throw new Error('tag must be specified');
}

const bot = new RepoBot();

(async() => {
try {
await bot.notifyPublishedPRs(tag);
} catch (err) {
console.warn('Error:', err.message);
}
})();

3 changes: 3 additions & 0 deletions bin/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import GithubAPI from "./GithubAPI.js";

export default new GithubAPI('axios', 'axios');
11 changes: 9 additions & 2 deletions bin/contributors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios from "./githubAPI.js";
import axios from "./githubAxios.js";
import util from "util";
import cp from "child_process";
import Handlebars from "handlebars";
Expand All @@ -7,6 +7,8 @@ import {colorize} from "./helpers/colorize.js";

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

const ONE_MB = 1024 * 1024;

const removeExtraLineBreaks = (str) => str.replace(/(?:\r\n|\r|\n){3,}/gm, '\r\n\r\n');

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

const release = JSON.parse((await exec(command)).stdout)[0];
console.log(command);

const {stdout} = await exec(command, {maxBuffer: 10 * ONE_MB});

const release = JSON.parse(stdout)[0];

if(release) {
const authors = {};
Expand Down Expand Up @@ -229,6 +235,7 @@ const getTagRef = async (tag) => {

export {
renderContributorsList,
getReleaseInfo,
renderPRsList,
getTagRef
}
File renamed without changes.
2 changes: 1 addition & 1 deletion bin/helpers/colorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chalk from 'chalk';

export const colorize = (...colors)=> {
if(!colors.length) {
colors = ['green', 'magenta', 'cyan', 'blue', 'yellow', 'red'];
colors = ['green', 'cyan', 'magenta', 'blue', 'yellow', 'red'];
}

const colorsCount = colors.length;
Expand Down
12 changes: 12 additions & 0 deletions bin/helpers/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const matchAll = (text, regexp, cb) => {
let match;
while((match = regexp.exec(text))) {
cb(match);
}
}

export const parseSection = (body, name, cb) => {
matchAll(body, new RegExp(`^(#+)\\s+${name}?(.*?)^\\1\\s+\\w+`, 'gims'), cb);
}

export const parseVersion = (rawVersion) => /^v?(\d+).(\d+).(\d+)/.exec(rawVersion);
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import gulp from 'gulp';
import fs from 'fs-extra';
import axios from './bin/githubAPI.js';
import axios from './bin/githubAxios.js';
import minimist from 'minimist'

const argv = minimist(process.argv.slice(2));
Expand Down
Loading

0 comments on commit 37cbf92

Please sign in to comment.