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'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const NOTIFY_PR_TEMPLATE = path.resolve(__dirname, '../templates/pr_published.hbs'); const normalizeTag = (tag) => (tag ? 'v' + tag.replace(/^v/, '') : ''); const GITHUB_BOT_LOGIN = 'github-actions[bot]'; const skipCollaboratorPRs = true; class RepoBot { constructor(options) { const { owner, repo, templates } = options || {}; this.templates = { published: NOTIFY_PR_TEMPLATE, ...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) { let pr; try { pr = await this.github.getPR(id); } catch (err) { if (err.response?.status === 404) { throw new Error(`PR #${id} not found (404)`); } throw err; } tag = normalizeTag(tag); const { merged, labels, user: { login, type }, } = pr; const isBot = type === 'Bot'; if (!merged) { return false; } await this.github.appendLabels(id, [tag]); if ( isBot || labels.find(({ name }) => name === 'automated pr') || (skipCollaboratorPRs && (await this.github.isCollaborator(login))) ) { return false; } const comments = await this.github.getComments(id, { desc: true }); const comment = comments.find( ({ body, user }) => user.login === GITHUB_BOT_LOGIN && body.indexOf('published in') >= 0 ); if (comment) { console.log(colorize()`Release comment [${comment.html_url}] already exists in #${pr.id}`); 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/${tag}`, }, }); return await this.addComment(id, message); } async notifyPublishedPRs(tag) { tag = normalizeTag(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 ? 'Label, comment' : 'Label'); } 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;