forked from axios/axios
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #205 from axios/v1.x
Create a new pull request by comparing changes across two branches
- Loading branch information
Showing
30 changed files
with
1,185 additions
and
588 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
name: notify | ||
|
||
on: | ||
#workflow_run: | ||
# workflows: ["publish"] | ||
# types: | ||
# - completed | ||
#repository_dispatch: | ||
# types: [ notify ] | ||
release: | ||
types: [ published ] | ||
workflow_dispatch: | ||
inputs: | ||
tag: | ||
required: true | ||
jobs: | ||
notify: | ||
runs-on: ubuntu-latest | ||
#if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} | ||
steps: | ||
#- name: Dump GitHub context | ||
# env: | ||
# GITHUB_CONTEXT: ${{ toJson(github) }} | ||
# run: echo "$GITHUB_CONTEXT" | ||
- uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 0 | ||
- name: git config | ||
run: | | ||
git config user.name "${GITHUB_ACTOR}" | ||
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" | ||
- name: Setup node | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: 18 | ||
cache: npm | ||
- run: npm ci | ||
- name: Notify published PRs | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: node ./bin/actions/notify_published.js --tag ${{ github.event.inputs.tag || github.event.release.tag_name }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
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 githubAxios.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) { | ||
} | ||
} | ||
|
||
static async getLatestTag() { | ||
try{ | ||
const {stdout} = await exec(`git for-each-ref refs/tags --sort=-taggerdate --format='%(refname)' --count=1`); | ||
|
||
return stdout.split('/').pop(); | ||
} catch (e) {} | ||
} | ||
|
||
static normalizeTag(tag){ | ||
return tag ? 'v' + tag.replace(/^v/, '') : ''; | ||
} | ||
} | ||
|
||
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); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
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 = Object.assign({ | ||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import minimist from "minimist"; | ||
import RepoBot from '../RepoBot.js'; | ||
import fs from 'fs/promises'; | ||
|
||
const argv = minimist(process.argv.slice(2)); | ||
console.log(argv); | ||
|
||
let {tag} = argv; | ||
|
||
(async() => { | ||
if (!tag || tag === true) { | ||
const {version} = JSON.parse((await fs.readFile('./package.json')).toString()); | ||
|
||
tag = 'v' + version; | ||
} else if (typeof tag !== 'string') { | ||
|
||
throw new Error('tag must be a string'); | ||
} | ||
|
||
const bot = new RepoBot(); | ||
|
||
try { | ||
await bot.notifyPublishedPRs(tag); | ||
} catch (err) { | ||
console.warn('Error:', err.message); | ||
} | ||
})(); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
Oops, something went wrong.