diff --git a/.github/port-labeler.yml b/.github/port-labeler.yml new file mode 100644 index 0000000000..571d9154f5 --- /dev/null +++ b/.github/port-labeler.yml @@ -0,0 +1,3 @@ +# Add 'repo' label to any root file changes +Port Request: +- '**/*' diff --git a/build/actions/.eslintrc.json b/build/actions/.eslintrc.json new file mode 100644 index 0000000000..d097bf7db2 --- /dev/null +++ b/build/actions/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "env": { + "commonjs": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + } +} \ No newline at end of file diff --git a/build/actions/.gitignore b/build/actions/.gitignore new file mode 100644 index 0000000000..7a11968072 --- /dev/null +++ b/build/actions/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.js.map diff --git a/build/actions/api/api.js b/build/actions/api/api.js new file mode 100644 index 0000000000..bda3e6b964 --- /dev/null +++ b/build/actions/api/api.js @@ -0,0 +1,6 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/actions/api/api.ts b/build/actions/api/api.ts new file mode 100644 index 0000000000..4ddc126e8d --- /dev/null +++ b/build/actions/api/api.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface GitHub { + query(query: Query): AsyncIterableIterator + + hasWriteAccess(user: User): Promise + + repoHasLabel(label: string): Promise + createLabel(label: string, color: string, description: string): Promise + deleteLabel(label: string): Promise + + readConfig(path: string): Promise + + createIssue(owner: string, repo: string, title: string, body: string): Promise + + releaseContainsCommit(release: string, commit: string): Promise +} + +export interface GitHubIssue extends GitHub { + getIssue(): Promise + + postComment(body: string): Promise + deleteComment(id: number): Promise + getComments(last?: boolean): AsyncIterableIterator + + closeIssue(): Promise + lockIssue(): Promise + + setMilestone(milestoneId: number): Promise + + addLabel(label: string): Promise + removeLabel(label: string): Promise + + addAssignee(assignee: string): Promise + + getClosingInfo(): Promise<{ hash: string | undefined; timestamp: number } | undefined> +} + +type SortVar = + | 'comments' + | 'reactions' + | 'reactions-+1' + | 'reactions--1' + | 'reactions-smile' + | 'reactions-thinking_face' + | 'reactions-heart' + | 'reactions-tada' + | 'interactions' + | 'created' + | 'updated' +type SortOrder = 'asc' | 'desc' +export type Reactions = { + '+1': number + '-1': number + laugh: number + hooray: number + confused: number + heart: number + rocket: number + eyes: number +} + +export interface User { + name: string + isGitHubApp?: boolean +} +export interface Comment { + author: User + body: string + id: number + timestamp: number +} +export interface Issue { + author: User + body: string + title: string + labels: string[] + open: boolean + locked: boolean + number: number + numComments: number + reactions: Reactions + milestoneId: number | null + assignee?: string + createdAt: number + updatedAt: number + closedAt?: number +} +export interface Query { + q: string + sort?: SortVar + order?: SortOrder +} diff --git a/build/actions/api/octokit.js b/build/actions/api/octokit.js new file mode 100644 index 0000000000..6b119c77d3 --- /dev/null +++ b/build/actions/api/octokit.js @@ -0,0 +1,293 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@actions/core"); +const github_1 = require("@actions/github"); +const child_process_1 = require("child_process"); +const utils_1 = require("../utils/utils"); +class OctoKit { + constructor(token, params, options = { readonly: false }) { + this.token = token; + this.params = params; + this.options = options; + // when in readonly mode, record labels just-created so at to not throw unneccesary errors + this.mockLabels = new Set(); + this.writeAccessCache = {}; + this.octokit = new github_1.GitHub(token); + } + async *query(query) { + const q = query.q + ` repo:${this.params.owner}/${this.params.repo}`; + console.log(`Querying for ${q}:`); + const options = this.octokit.search.issuesAndPullRequests.endpoint.merge({ + ...query, + q, + per_page: 100, + headers: { Accept: 'application/vnd.github.squirrel-girl-preview+json' }, + }); + let pageNum = 0; + const timeout = async () => { + if (pageNum < 2) { + /* pass */ + } + else if (pageNum < 4) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + else { + await new Promise((resolve) => setTimeout(resolve, 30000)); + } + }; + for await (const pageResponse of this.octokit.paginate.iterator(options)) { + await timeout(); + await utils_1.logRateLimit(this.token); + const page = pageResponse.data; + console.log(`Page ${++pageNum}: ${page.map(({ number }) => number).join(' ')}`); + yield page.map((issue) => new OctoKitIssue(this.token, this.params, this.octokitIssueToIssue(issue))); + } + } + async createIssue(owner, repo, title, body) { + core_1.debug(`Creating issue \`${title}\` on ${owner}/${repo}`); + if (!this.options.readonly) + await this.octokit.issues.create({ owner, repo, title, body }); + } + octokitIssueToIssue(issue) { + var _a, _b, _c, _d, _e, _f; + return { + author: { name: issue.user.login, isGitHubApp: issue.user.type === 'Bot' }, + body: issue.body, + number: issue.number, + title: issue.title, + labels: issue.labels.map((label) => label.name), + open: issue.state === 'open', + locked: issue.locked, + numComments: issue.comments, + reactions: issue.reactions, + assignee: (_b = (_a = issue.assignee) === null || _a === void 0 ? void 0 : _a.login) !== null && _b !== void 0 ? _b : (_d = (_c = issue.assignees) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.login, + milestoneId: (_f = (_e = issue.milestone) === null || _e === void 0 ? void 0 : _e.number) !== null && _f !== void 0 ? _f : null, + createdAt: +new Date(issue.created_at), + updatedAt: +new Date(issue.updated_at), + closedAt: issue.closed_at ? +new Date(issue.closed_at) : undefined, + }; + } + async hasWriteAccess(user) { + if (user.name in this.writeAccessCache) { + core_1.debug('Got permissions from cache for ' + user); + return this.writeAccessCache[user.name]; + } + core_1.debug('Fetching permissions for ' + user); + const permissions = (await this.octokit.repos.getCollaboratorPermissionLevel({ + ...this.params, + username: user.name, + })).data.permission; + return (this.writeAccessCache[user.name] = permissions === 'admin' || permissions === 'write'); + } + async repoHasLabel(name) { + try { + await this.octokit.issues.getLabel({ ...this.params, name }); + return true; + } + catch (err) { + if (err.status === 404) { + return this.options.readonly && this.mockLabels.has(name); + } + throw err; + } + } + async createLabel(name, color, description) { + core_1.debug('Creating label ' + name); + if (!this.options.readonly) + await this.octokit.issues.createLabel({ ...this.params, color, description, name }); + else + this.mockLabels.add(name); + } + async deleteLabel(name) { + core_1.debug('Deleting label ' + name); + try { + if (!this.options.readonly) + await this.octokit.issues.deleteLabel({ ...this.params, name }); + } + catch (err) { + if (err.status === 404) { + return; + } + throw err; + } + } + async readConfig(path) { + core_1.debug('Reading config at ' + path); + const repoPath = `.github/${path}.json`; + const data = (await this.octokit.repos.getContents({ ...this.params, path: repoPath })).data; + if ('type' in data && data.type === 'file') { + if (data.encoding === 'base64' && data.content) { + return JSON.parse(Buffer.from(data.content, 'base64').toString('utf-8')); + } + throw Error(`Could not read contents "${data.content}" in encoding "${data.encoding}"`); + } + throw Error('Found directory at config path when expecting file' + JSON.stringify(data)); + } + async releaseContainsCommit(release, commit) { + if (utils_1.getInput('commitReleasedDebuggingOverride')) { + return true; + } + return new Promise((resolve, reject) => child_process_1.exec(`git -C ./repo merge-base --is-ancestor ${commit} ${release}`, (err) => !err || err.code === 1 ? resolve(!err) : reject(err))); + } +} +exports.OctoKit = OctoKit; +class OctoKitIssue extends OctoKit { + constructor(token, params, issueData, options = { readonly: false }) { + super(token, params, options); + this.params = params; + this.issueData = issueData; + } + async addAssignee(assignee) { + core_1.debug('Adding assignee ' + assignee + ' to ' + this.issueData.number); + if (!this.options.readonly) { + await this.octokit.issues.addAssignees({ + ...this.params, + issue_number: this.issueData.number, + assignees: [assignee], + }); + } + } + async closeIssue() { + core_1.debug('Closing issue ' + this.issueData.number); + if (!this.options.readonly) + await this.octokit.issues.update({ + ...this.params, + issue_number: this.issueData.number, + state: 'closed', + }); + } + async lockIssue() { + core_1.debug('Locking issue ' + this.issueData.number); + if (!this.options.readonly) + await this.octokit.issues.lock({ ...this.params, issue_number: this.issueData.number }); + } + async getIssue() { + if (isIssue(this.issueData)) { + core_1.debug('Got issue data from query result ' + this.issueData.number); + return this.issueData; + } + console.log('Fetching issue ' + this.issueData.number); + const issue = (await this.octokit.issues.get({ + ...this.params, + issue_number: this.issueData.number, + mediaType: { previews: ['squirrel-girl'] }, + })).data; + return (this.issueData = this.octokitIssueToIssue(issue)); + } + async postComment(body) { + core_1.debug(`Posting comment ${body} on ${this.issueData.number}`); + if (!this.options.readonly) + await this.octokit.issues.createComment({ + ...this.params, + issue_number: this.issueData.number, + body, + }); + } + async deleteComment(id) { + core_1.debug(`Deleting comment ${id} on ${this.issueData.number}`); + if (!this.options.readonly) + await this.octokit.issues.deleteComment({ + owner: this.params.owner, + repo: this.params.repo, + comment_id: id, + }); + } + async setMilestone(milestoneId) { + core_1.debug(`Setting milestone for ${this.issueData.number} to ${milestoneId}`); + if (!this.options.readonly) + await this.octokit.issues.update({ + ...this.params, + issue_number: this.issueData.number, + milestone: milestoneId, + }); + } + async *getComments(last) { + core_1.debug('Fetching comments for ' + this.issueData.number); + const response = this.octokit.paginate.iterator(this.octokit.issues.listComments.endpoint.merge({ + ...this.params, + issue_number: this.issueData.number, + per_page: 100, + ...(last ? { per_page: 1, page: (await this.getIssue()).numComments } : {}), + })); + for await (const page of response) { + yield page.data.map((comment) => ({ + author: { name: comment.user.login, isGitHubApp: comment.user.type === 'Bot' }, + body: comment.body, + id: comment.id, + timestamp: +new Date(comment.created_at), + })); + } + } + async addLabel(name) { + core_1.debug(`Adding label ${name} to ${this.issueData.number}`); + if (!(await this.repoHasLabel(name))) { + throw Error(`Action could not execute becuase label ${name} is not defined.`); + } + if (!this.options.readonly) + await this.octokit.issues.addLabels({ + ...this.params, + issue_number: this.issueData.number, + labels: [name], + }); + } + async removeLabel(name) { + core_1.debug(`Removing label ${name} from ${this.issueData.number}`); + try { + if (!this.options.readonly) + await this.octokit.issues.removeLabel({ + ...this.params, + issue_number: this.issueData.number, + name, + }); + } + catch (err) { + if (err.status === 404) { + console.log(`Label ${name} not found on issue`); + return; + } + throw err; + } + } + async getClosingInfo() { + var _a; + if ((await this.getIssue()).open) { + return; + } + const options = this.octokit.issues.listEventsForTimeline.endpoint.merge({ + ...this.params, + issue_number: this.issueData.number, + }); + let closingCommit; + for await (const event of this.octokit.paginate.iterator(options)) { + const timelineEvents = event.data; + for (const timelineEvent of timelineEvents) { + if (timelineEvent.event === 'closed') { + closingCommit = { + hash: (_a = timelineEvent.commit_id) !== null && _a !== void 0 ? _a : undefined, + timestamp: +new Date(timelineEvent.created_at), + }; + } + } + } + console.log(`Got ${closingCommit} as closing commit of ${this.issueData.number}`); + return closingCommit; + } +} +exports.OctoKitIssue = OctoKitIssue; +function isIssue(object) { + const isIssue = 'author' in object && + 'body' in object && + 'title' in object && + 'labels' in object && + 'open' in object && + 'locked' in object && + 'number' in object && + 'numComments' in object && + 'reactions' in object && + 'milestoneId' in object; + return isIssue; +} diff --git a/build/actions/api/octokit.ts b/build/actions/api/octokit.ts new file mode 100644 index 0000000000..a9561397f5 --- /dev/null +++ b/build/actions/api/octokit.ts @@ -0,0 +1,336 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { debug } from '@actions/core' +import { GitHub as GitHubAPI } from '@actions/github' +import { Octokit } from '@octokit/rest' +import { exec } from 'child_process' +import { getInput, logRateLimit } from '../utils/utils' +import { Comment, GitHub, GitHubIssue, Issue, Query, User } from './api' + +export class OctoKit implements GitHub { + protected octokit: GitHubAPI + // when in readonly mode, record labels just-created so at to not throw unneccesary errors + protected mockLabels: Set = new Set() + + constructor( + private token: string, + protected params: { repo: string; owner: string }, + protected options: { readonly: boolean } = { readonly: false }, + ) { + this.octokit = new GitHubAPI(token) + } + + async *query(query: Query): AsyncIterableIterator { + const q = query.q + ` repo:${this.params.owner}/${this.params.repo}` + console.log(`Querying for ${q}:`) + + const options = this.octokit.search.issuesAndPullRequests.endpoint.merge({ + ...query, + q, + per_page: 100, + headers: { Accept: 'application/vnd.github.squirrel-girl-preview+json' }, + }) + + let pageNum = 0 + + const timeout = async () => { + if (pageNum < 2) { + /* pass */ + } else if (pageNum < 4) { + await new Promise((resolve) => setTimeout(resolve, 3000)) + } else { + await new Promise((resolve) => setTimeout(resolve, 30000)) + } + } + + for await (const pageResponse of this.octokit.paginate.iterator(options)) { + await timeout() + await logRateLimit(this.token) + const page: Array = pageResponse.data + console.log(`Page ${++pageNum}: ${page.map(({ number }) => number).join(' ')}`) + yield page.map( + (issue) => new OctoKitIssue(this.token, this.params, this.octokitIssueToIssue(issue)), + ) + } + } + + async createIssue(owner: string, repo: string, title: string, body: string): Promise { + debug(`Creating issue \`${title}\` on ${owner}/${repo}`) + if (!this.options.readonly) await this.octokit.issues.create({ owner, repo, title, body }) + } + + protected octokitIssueToIssue( + issue: Octokit.IssuesGetResponse | Octokit.SearchIssuesAndPullRequestsResponseItemsItem, + ): Issue { + return { + author: { name: issue.user.login, isGitHubApp: issue.user.type === 'Bot' }, + body: issue.body, + number: issue.number, + title: issue.title, + labels: (issue.labels as Octokit.IssuesGetLabelResponse[]).map((label) => label.name), + open: issue.state === 'open', + locked: (issue as any).locked, + numComments: issue.comments, + reactions: (issue as any).reactions, + assignee: issue.assignee?.login ?? (issue as any).assignees?.[0]?.login, + milestoneId: issue.milestone?.number ?? null, + createdAt: +new Date(issue.created_at), + updatedAt: +new Date(issue.updated_at), + closedAt: issue.closed_at ? +new Date((issue.closed_at as unknown) as string) : undefined, + } + } + + private writeAccessCache: Record = {} + async hasWriteAccess(user: User): Promise { + if (user.name in this.writeAccessCache) { + debug('Got permissions from cache for ' + user) + return this.writeAccessCache[user.name] + } + debug('Fetching permissions for ' + user) + const permissions = ( + await this.octokit.repos.getCollaboratorPermissionLevel({ + ...this.params, + username: user.name, + }) + ).data.permission + return (this.writeAccessCache[user.name] = permissions === 'admin' || permissions === 'write') + } + + async repoHasLabel(name: string): Promise { + try { + await this.octokit.issues.getLabel({ ...this.params, name }) + return true + } catch (err) { + if (err.status === 404) { + return this.options.readonly && this.mockLabels.has(name) + } + throw err + } + } + + async createLabel(name: string, color: string, description: string): Promise { + debug('Creating label ' + name) + if (!this.options.readonly) + await this.octokit.issues.createLabel({ ...this.params, color, description, name }) + else this.mockLabels.add(name) + } + + async deleteLabel(name: string): Promise { + debug('Deleting label ' + name) + try { + if (!this.options.readonly) await this.octokit.issues.deleteLabel({ ...this.params, name }) + } catch (err) { + if (err.status === 404) { + return + } + throw err + } + } + + async readConfig(path: string): Promise { + debug('Reading config at ' + path) + const repoPath = `.github/${path}.json` + const data = (await this.octokit.repos.getContents({ ...this.params, path: repoPath })).data + + if ('type' in data && data.type === 'file') { + if (data.encoding === 'base64' && data.content) { + return JSON.parse(Buffer.from(data.content, 'base64').toString('utf-8')) + } + throw Error(`Could not read contents "${data.content}" in encoding "${data.encoding}"`) + } + throw Error('Found directory at config path when expecting file' + JSON.stringify(data)) + } + + async releaseContainsCommit(release: string, commit: string): Promise { + if (getInput('commitReleasedDebuggingOverride')) { + return true + } + return new Promise((resolve, reject) => + exec(`git -C ./repo merge-base --is-ancestor ${commit} ${release}`, (err) => + !err || err.code === 1 ? resolve(!err) : reject(err), + ), + ) + } +} + +export class OctoKitIssue extends OctoKit implements GitHubIssue { + constructor( + token: string, + protected params: { repo: string; owner: string }, + private issueData: { number: number } | Issue, + options: { readonly: boolean } = { readonly: false }, + ) { + super(token, params, options) + } + + async addAssignee(assignee: string): Promise { + debug('Adding assignee ' + assignee + ' to ' + this.issueData.number) + if (!this.options.readonly) { + await this.octokit.issues.addAssignees({ + ...this.params, + issue_number: this.issueData.number, + assignees: [assignee], + }) + } + } + + async closeIssue(): Promise { + debug('Closing issue ' + this.issueData.number) + if (!this.options.readonly) + await this.octokit.issues.update({ + ...this.params, + issue_number: this.issueData.number, + state: 'closed', + }) + } + + async lockIssue(): Promise { + debug('Locking issue ' + this.issueData.number) + if (!this.options.readonly) + await this.octokit.issues.lock({ ...this.params, issue_number: this.issueData.number }) + } + + async getIssue(): Promise { + if (isIssue(this.issueData)) { + debug('Got issue data from query result ' + this.issueData.number) + return this.issueData + } + + console.log('Fetching issue ' + this.issueData.number) + const issue = ( + await this.octokit.issues.get({ + ...this.params, + issue_number: this.issueData.number, + mediaType: { previews: ['squirrel-girl'] }, + }) + ).data + return (this.issueData = this.octokitIssueToIssue(issue)) + } + + async postComment(body: string): Promise { + debug(`Posting comment ${body} on ${this.issueData.number}`) + if (!this.options.readonly) + await this.octokit.issues.createComment({ + ...this.params, + issue_number: this.issueData.number, + body, + }) + } + + async deleteComment(id: number): Promise { + debug(`Deleting comment ${id} on ${this.issueData.number}`) + if (!this.options.readonly) + await this.octokit.issues.deleteComment({ + owner: this.params.owner, + repo: this.params.repo, + comment_id: id, + }) + } + + async setMilestone(milestoneId: number) { + debug(`Setting milestone for ${this.issueData.number} to ${milestoneId}`) + if (!this.options.readonly) + await this.octokit.issues.update({ + ...this.params, + issue_number: this.issueData.number, + milestone: milestoneId, + }) + } + + async *getComments(last?: boolean): AsyncIterableIterator { + debug('Fetching comments for ' + this.issueData.number) + + const response = this.octokit.paginate.iterator( + this.octokit.issues.listComments.endpoint.merge({ + ...this.params, + issue_number: this.issueData.number, + per_page: 100, + ...(last ? { per_page: 1, page: (await this.getIssue()).numComments } : {}), + }), + ) + + for await (const page of response) { + yield (page.data as Octokit.IssuesListCommentsResponseItem[]).map((comment) => ({ + author: { name: comment.user.login, isGitHubApp: comment.user.type === 'Bot' }, + body: comment.body, + id: comment.id, + timestamp: +new Date(comment.created_at), + })) + } + } + + async addLabel(name: string): Promise { + debug(`Adding label ${name} to ${this.issueData.number}`) + if (!(await this.repoHasLabel(name))) { + throw Error(`Action could not execute becuase label ${name} is not defined.`) + } + if (!this.options.readonly) + await this.octokit.issues.addLabels({ + ...this.params, + issue_number: this.issueData.number, + labels: [name], + }) + } + + async removeLabel(name: string): Promise { + debug(`Removing label ${name} from ${this.issueData.number}`) + try { + if (!this.options.readonly) + await this.octokit.issues.removeLabel({ + ...this.params, + issue_number: this.issueData.number, + name, + }) + } catch (err) { + if (err.status === 404) { + console.log(`Label ${name} not found on issue`) + return + } + throw err + } + } + + async getClosingInfo(): Promise<{ hash: string | undefined; timestamp: number } | undefined> { + if ((await this.getIssue()).open) { + return + } + + const options = this.octokit.issues.listEventsForTimeline.endpoint.merge({ + ...this.params, + issue_number: this.issueData.number, + }) + let closingCommit: { hash: string | undefined; timestamp: number } | undefined + for await (const event of this.octokit.paginate.iterator(options)) { + const timelineEvents = event.data as Octokit.IssuesListEventsForTimelineResponseItem[] + for (const timelineEvent of timelineEvents) { + if (timelineEvent.event === 'closed') { + closingCommit = { + hash: timelineEvent.commit_id ?? undefined, + timestamp: +new Date(timelineEvent.created_at), + } + } + } + } + console.log(`Got ${closingCommit} as closing commit of ${this.issueData.number}`) + return closingCommit + } +} + +function isIssue(object: any): object is Issue { + const isIssue = + 'author' in object && + 'body' in object && + 'title' in object && + 'labels' in object && + 'open' in object && + 'locked' in object && + 'number' in object && + 'numComments' in object && + 'reactions' in object && + 'milestoneId' in object + + return isIssue +} diff --git a/build/actions/api/testbed.js b/build/actions/api/testbed.js new file mode 100644 index 0000000000..10b74b478e --- /dev/null +++ b/build/actions/api/testbed.js @@ -0,0 +1,123 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +class Testbed { + constructor(config) { + var _a, _b, _c, _d, _e; + this.config = { + globalLabels: (_a = config === null || config === void 0 ? void 0 : config.globalLabels) !== null && _a !== void 0 ? _a : [], + configs: (_b = config === null || config === void 0 ? void 0 : config.configs) !== null && _b !== void 0 ? _b : {}, + writers: (_c = config === null || config === void 0 ? void 0 : config.writers) !== null && _c !== void 0 ? _c : [], + releasedCommits: (_d = config === null || config === void 0 ? void 0 : config.releasedCommits) !== null && _d !== void 0 ? _d : [], + queryRunner: (_e = config === null || config === void 0 ? void 0 : config.queryRunner) !== null && _e !== void 0 ? _e : async function* () { + yield []; + }, + }; + } + async *query(query) { + for await (const page of this.config.queryRunner(query)) { + yield page.map((issue) => issue instanceof TestbedIssue ? issue : new TestbedIssue(this.config, issue)); + } + } + async createIssue(_owner, _repo, _title, _body) { + // pass... + } + async readConfig(path) { + return JSON.parse(JSON.stringify(this.config.configs[path])); + } + async hasWriteAccess(user) { + return this.config.writers.includes(user.name); + } + async repoHasLabel(label) { + return this.config.globalLabels.includes(label); + } + async createLabel(label, _color, _description) { + this.config.globalLabels.push(label); + } + async deleteLabel(labelToDelete) { + this.config.globalLabels = this.config.globalLabels.filter((label) => label !== labelToDelete); + } + async releaseContainsCommit(_release, commit) { + return this.config.releasedCommits.includes(commit); + } +} +exports.Testbed = Testbed; +class TestbedIssue extends Testbed { + constructor(globalConfig, issueConfig) { + var _a, _b, _c; + super(globalConfig); + issueConfig = issueConfig !== null && issueConfig !== void 0 ? issueConfig : {}; + issueConfig.comments = (_a = issueConfig === null || issueConfig === void 0 ? void 0 : issueConfig.comments) !== null && _a !== void 0 ? _a : []; + issueConfig.labels = (_b = issueConfig === null || issueConfig === void 0 ? void 0 : issueConfig.labels) !== null && _b !== void 0 ? _b : []; + issueConfig.issue = { + author: { name: 'JacksonKearl' }, + body: 'issue body', + locked: false, + numComments: ((_c = issueConfig === null || issueConfig === void 0 ? void 0 : issueConfig.comments) === null || _c === void 0 ? void 0 : _c.length) || 0, + number: 1, + open: true, + title: 'issue title', + assignee: undefined, + reactions: { + '+1': 0, + '-1': 0, + confused: 0, + eyes: 0, + heart: 0, + hooray: 0, + laugh: 0, + rocket: 0, + }, + closedAt: undefined, + createdAt: +new Date(), + updatedAt: +new Date(), + ...issueConfig.issue, + }; + this.issueConfig = issueConfig; + } + async addAssignee(assignee) { + this.issueConfig.issue.assignee = assignee; + } + async setMilestone(milestoneId) { + this.issueConfig.issue.milestoneId = milestoneId; + } + async getIssue() { + const labels = [...this.issueConfig.labels]; + return { ...this.issueConfig.issue, labels }; + } + async postComment(body, author) { + this.issueConfig.comments.push({ + author: { name: author !== null && author !== void 0 ? author : 'bot' }, + body, + id: Math.random(), + timestamp: +new Date(), + }); + } + async deleteComment(id) { + this.issueConfig.comments = this.issueConfig.comments.filter((comment) => comment.id !== id); + } + async *getComments(last) { + yield last + ? [this.issueConfig.comments[this.issueConfig.comments.length - 1]] + : this.issueConfig.comments; + } + async addLabel(label) { + this.issueConfig.labels.push(label); + } + async removeLabel(labelToDelete) { + this.issueConfig.labels = this.issueConfig.labels.filter((label) => label !== labelToDelete); + } + async closeIssue() { + this.issueConfig.issue.open = false; + } + async lockIssue() { + this.issueConfig.issue.locked = true; + } + async getClosingInfo() { + return this.issueConfig.closingCommit; + } +} +exports.TestbedIssue = TestbedIssue; diff --git a/build/actions/api/testbed.ts b/build/actions/api/testbed.ts new file mode 100644 index 0000000000..7d476626d4 --- /dev/null +++ b/build/actions/api/testbed.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Comment, GitHub, GitHubIssue, Issue, Query, User } from './api' + +type TestbedConfig = { + globalLabels: string[] + configs: Record + writers: string[] + releasedCommits: string[] + queryRunner: (query: Query) => AsyncIterableIterator<(TestbedIssueConstructorArgs | TestbedIssue)[]> +} + +export type TestbedConstructorArgs = Partial + +export class Testbed implements GitHub { + public config: TestbedConfig + + constructor(config?: TestbedConstructorArgs) { + this.config = { + globalLabels: config?.globalLabels ?? [], + configs: config?.configs ?? {}, + writers: config?.writers ?? [], + releasedCommits: config?.releasedCommits ?? [], + queryRunner: + config?.queryRunner ?? + async function* () { + yield [] + }, + } + } + + async *query(query: Query): AsyncIterableIterator { + for await (const page of this.config.queryRunner(query)) { + yield page.map((issue) => + issue instanceof TestbedIssue ? issue : new TestbedIssue(this.config, issue), + ) + } + } + + async createIssue(_owner: string, _repo: string, _title: string, _body: string): Promise { + // pass... + } + + async readConfig(path: string): Promise { + return JSON.parse(JSON.stringify(this.config.configs[path])) + } + + async hasWriteAccess(user: User): Promise { + return this.config.writers.includes(user.name) + } + + async repoHasLabel(label: string): Promise { + return this.config.globalLabels.includes(label) + } + + async createLabel(label: string, _color: string, _description: string): Promise { + this.config.globalLabels.push(label) + } + + async deleteLabel(labelToDelete: string): Promise { + this.config.globalLabels = this.config.globalLabels.filter((label) => label !== labelToDelete) + } + + async releaseContainsCommit(_release: string, commit: string): Promise { + return this.config.releasedCommits.includes(commit) + } +} + +type TestbedIssueConfig = { + issue: Omit + comments: Comment[] + labels: string[] + closingCommit: { hash: string | undefined; timestamp: number } | undefined +} + +export type TestbedIssueConstructorArgs = Partial> & { + issue?: Partial> +} + +export class TestbedIssue extends Testbed implements GitHubIssue { + public issueConfig: TestbedIssueConfig + + constructor(globalConfig?: TestbedConstructorArgs, issueConfig?: TestbedIssueConstructorArgs) { + super(globalConfig) + issueConfig = issueConfig ?? {} + issueConfig.comments = issueConfig?.comments ?? [] + issueConfig.labels = issueConfig?.labels ?? [] + issueConfig.issue = { + author: { name: 'JacksonKearl' }, + body: 'issue body', + locked: false, + numComments: issueConfig?.comments?.length || 0, + number: 1, + open: true, + title: 'issue title', + assignee: undefined, + reactions: { + '+1': 0, + '-1': 0, + confused: 0, + eyes: 0, + heart: 0, + hooray: 0, + laugh: 0, + rocket: 0, + }, + closedAt: undefined, + createdAt: +new Date(), + updatedAt: +new Date(), + ...issueConfig.issue, + } + + this.issueConfig = issueConfig as TestbedIssueConfig + } + + async addAssignee(assignee: string): Promise { + this.issueConfig.issue.assignee = assignee + } + + async setMilestone(milestoneId: number): Promise { + this.issueConfig.issue.milestoneId = milestoneId + } + + async getIssue(): Promise { + const labels = [...this.issueConfig.labels] + return { ...this.issueConfig.issue, labels } + } + + async postComment(body: string, author?: string): Promise { + this.issueConfig.comments.push({ + author: { name: author ?? 'bot' }, + body, + id: Math.random(), + timestamp: +new Date(), + }) + } + + async deleteComment(id: number): Promise { + this.issueConfig.comments = this.issueConfig.comments.filter((comment) => comment.id !== id) + } + + async *getComments(last?: boolean): AsyncIterableIterator { + yield last + ? [this.issueConfig.comments[this.issueConfig.comments.length - 1]] + : this.issueConfig.comments + } + + async addLabel(label: string): Promise { + this.issueConfig.labels.push(label) + } + + async removeLabel(labelToDelete: string): Promise { + this.issueConfig.labels = this.issueConfig.labels.filter((label) => label !== labelToDelete) + } + + async closeIssue(): Promise { + this.issueConfig.issue.open = false + } + + async lockIssue(): Promise { + this.issueConfig.issue.locked = true + } + + async getClosingInfo(): Promise<{ hash: string | undefined; timestamp: number } | undefined> { + return this.issueConfig.closingCommit + } +} diff --git a/build/actions/auto-labeler/action.yml b/build/actions/auto-labeler/action.yml new file mode 100644 index 0000000000..f1e9816935 --- /dev/null +++ b/build/actions/auto-labeler/action.yml @@ -0,0 +1,12 @@ +name: 'PR Labeler' +description: 'Automatically add a Label to a PR' +inputs: + token: + description: GitHub token with issue, comment, and label read/write permissions + default: ${{ github.token }} + label: + description: Github label to add to the PR + required: true +runs: + using: 'node12' + main: 'index.js' diff --git a/build/actions/auto-labeler/index.js b/build/actions/auto-labeler/index.js new file mode 100644 index 0000000000..8147413f82 --- /dev/null +++ b/build/actions/auto-labeler/index.js @@ -0,0 +1,22 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const core = require("@actions/core"); +const github_1 = require("@actions/github"); +const octokit_1 = require("../api/octokit"); +const utils_1 = require("../utils/utils"); +const token = utils_1.getRequiredInput('token'); +const label = utils_1.getRequiredInput('label'); +async function main() { + const pr = new octokit_1.OctoKitIssue(token, github_1.context.repo, { number: github_1.context.issue.number }); + pr.addLabel(label); +} +main() + .then(() => utils_1.logRateLimit(token)) + .catch(async (error) => { + core.setFailed(error.message); + await utils_1.logErrorToIssue(error.message, true, token); +}); diff --git a/build/actions/auto-labeler/index.ts b/build/actions/auto-labeler/index.ts new file mode 100644 index 0000000000..fcab003a1e --- /dev/null +++ b/build/actions/auto-labeler/index.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as core from '@actions/core' +import { context } from '@actions/github' +import { OctoKitIssue } from '../api/octokit' +import { getRequiredInput, logErrorToIssue, logRateLimit } from '../utils/utils' + +const token = getRequiredInput('token'); +const label = getRequiredInput('label'); + +async function main() { + + const pr = new OctoKitIssue(token, context.repo, { number: context.issue.number }); + + pr.addLabel(label); +} + +main() + .then(() => logRateLimit(token)) + .catch(async (error) => { + core.setFailed(error.message) + await logErrorToIssue(error.message, true, token) + }) diff --git a/build/actions/package.json b/build/actions/package.json new file mode 100644 index 0000000000..711f9bcf20 --- /dev/null +++ b/build/actions/package.json @@ -0,0 +1,24 @@ +{ + "name": "github-actions", + "version": "1.0.0", + "description": "GitHub Actions", + "scripts": { + "test": "mocha -r ts-node/register **/*.test.ts", + "build": "tsc -p ./tsconfig.json", + "lint": "eslint -c .eslintrc --fix --ext .ts .", + "watch-typecheck": "tsc --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/azuredatastudio.git" + }, + "keywords": [], + "author": "", + "dependencies": { + "@actions/core": "^1.2.6", + "@actions/github": "^2.1.1", + "axios": "^0.21.4", + "ts-node": "^8.6.2", + "typescript": "^3.8.3" + } +} diff --git a/build/actions/tsconfig.json b/build/actions/tsconfig.json new file mode 100644 index 0000000000..5190eca692 --- /dev/null +++ b/build/actions/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2019", + "strict": true, + "module": "commonjs", + "moduleResolution": "node", + "removeComments": false, + "resolveJsonModule": true, + "lib": [ + "es2020" + ], + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/build/actions/utils/utils.js b/build/actions/utils/utils.js new file mode 100644 index 0000000000..91a70b6e6d --- /dev/null +++ b/build/actions/utils/utils.js @@ -0,0 +1,72 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const core = require("@actions/core"); +const github_1 = require("@actions/github"); +const axios_1 = require("axios"); +const octokit_1 = require("../api/octokit"); +exports.getInput = (name) => core.getInput(name) || undefined; +exports.getRequiredInput = (name) => core.getInput(name, { required: true }); +exports.normalizeIssue = (issue) => { + const { body, title } = issue; + const isBug = body.includes('bug_report_template') || /Issue Type:.*Bug.*/.test(body); + const isFeatureRequest = body.includes('feature_request_template') || /Issue Type:.*Feature Request.*/.test(body); + const cleanse = (str) => str + .toLowerCase() + .replace(//gu, '') + .replace(/.* version: .*/gu, '') + .replace(/issue type: .*/gu, '') + .replace(/
(.|\s)*?<\/details>/gu, '') + .replace(/vs ?code/gu, '') + .replace(/we have written.*please paste./gu, '') + .replace(/steps to reproduce:/gu, '') + .replace(/does this issue occur when all extensions are disabled.*/gu, '') + .replace(/```(.|\s)*?```/gu, '') + .replace(/!?\[.*?\]\(.*?\)/gu, '') + .replace(/\s+/gu, ' '); + return { + body: cleanse(body), + title: cleanse(title), + issueType: isBug ? 'bug' : isFeatureRequest ? 'feature_request' : 'unknown', + }; +}; +exports.loadLatestRelease = async (quality) => (await axios_1.default.get(`https://vscode-update.azurewebsites.net/api/update/darwin/${quality}/latest`)).data; +exports.daysAgoToTimestamp = (days) => +new Date(Date.now() - days * 24 * 60 * 60 * 1000); +exports.daysAgoToHumanReadbleDate = (days) => new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().replace(/\.\d{3}\w$/, ''); +exports.logRateLimit = async (token) => { + const usageData = (await new github_1.GitHub(token).rateLimit.get()).data.resources; + ['core', 'graphql', 'search'].forEach(async (category) => { + const usage = 1 - usageData[category].remaining / usageData[category].limit; + const message = `Usage at ${usage} for ${category}`; + if (usage > 0) { + console.log(message); + } + if (usage > 0.5) { + await exports.logErrorToIssue(message, false, token); + } + }); +}; +exports.logErrorToIssue = async (message, ping, token) => { + // Attempt to wait out abuse detection timeout if present + await new Promise((resolve) => setTimeout(resolve, 10000)); + const dest = github_1.context.repo.repo === 'vscode-internalbacklog' + ? { repo: 'vscode-internalbacklog', issue: 974 } + : { repo: 'vscode', issue: 93814 }; + return new octokit_1.OctoKitIssue(token, { owner: 'Microsoft', repo: dest.repo }, { number: dest.issue }) + .postComment(` +Workflow: ${github_1.context.workflow} + +Error: ${message} + +Issue: ${ping ? `${github_1.context.repo.owner}/${github_1.context.repo.repo}#` : ''}${github_1.context.issue.number} + +Repo: ${github_1.context.repo.owner}/${github_1.context.repo.repo} + +/gu, '--@>')} +--> +`); +}; diff --git a/build/actions/utils/utils.ts b/build/actions/utils/utils.ts new file mode 100644 index 0000000000..409710111b --- /dev/null +++ b/build/actions/utils/utils.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as core from '@actions/core' +import { context, GitHub } from '@actions/github' +import axios from 'axios' +import { OctoKitIssue } from '../api/octokit' +import { Issue } from '../api/api' + +export const getInput = (name: string) => core.getInput(name) || undefined +export const getRequiredInput = (name: string) => core.getInput(name, { required: true }) + +export const normalizeIssue = ( + issue: Issue, +): { body: string; title: string; issueType: 'bug' | 'feature_request' | 'unknown' } => { + const { body, title } = issue + + const isBug = body.includes('bug_report_template') || /Issue Type:.*Bug.*/.test(body) + const isFeatureRequest = + body.includes('feature_request_template') || /Issue Type:.*Feature Request.*/.test(body) + + const cleanse = (str: string) => + str + .toLowerCase() + .replace(//gu, '') + .replace(/.* version: .*/gu, '') + .replace(/issue type: .*/gu, '') + .replace(/
(.|\s)*?<\/details>/gu, '') + .replace(/vs ?code/gu, '') + .replace(/we have written.*please paste./gu, '') + .replace(/steps to reproduce:/gu, '') + .replace(/does this issue occur when all extensions are disabled.*/gu, '') + .replace(/```(.|\s)*?```/gu, '') + .replace(/!?\[.*?\]\(.*?\)/gu, '') + .replace(/\s+/gu, ' ') + + return { + body: cleanse(body), + title: cleanse(title), + issueType: isBug ? 'bug' : isFeatureRequest ? 'feature_request' : 'unknown', + } +} + +export interface Release { + productVersion: string + timestamp: number + version: string +} + +export const loadLatestRelease = async (quality: 'stable' | 'insider'): Promise => + (await axios.get(`https://vscode-update.azurewebsites.net/api/update/darwin/${quality}/latest`)).data + +export const daysAgoToTimestamp = (days: number): number => +new Date(Date.now() - days * 24 * 60 * 60 * 1000) + +export const daysAgoToHumanReadbleDate = (days: number) => + new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().replace(/\.\d{3}\w$/, '') + +export const logRateLimit = async (token: string) => { + const usageData = (await new GitHub(token).rateLimit.get()).data.resources + ;(['core', 'graphql', 'search'] as const).forEach(async (category) => { + const usage = 1 - usageData[category].remaining / usageData[category].limit + const message = `Usage at ${usage} for ${category}` + if (usage > 0) { + console.log(message) + } + if (usage > 0.5) { + await logErrorToIssue(message, false, token) + } + }) +} + +export const logErrorToIssue = async (message: string, ping: boolean, token: string): Promise => { + // Attempt to wait out abuse detection timeout if present + await new Promise((resolve) => setTimeout(resolve, 10000)) + const dest = + context.repo.repo === 'vscode-internalbacklog' + ? { repo: 'vscode-internalbacklog', issue: 974 } + : { repo: 'vscode', issue: 93814 } + return new OctoKitIssue(token, { owner: 'Microsoft', repo: dest.repo }, { number: dest.issue }) + .postComment(` +Workflow: ${context.workflow} + +Error: ${message} + +Issue: ${ping ? `${context.repo.owner}/${context.repo.repo}#` : ''}${context.issue.number} + +Repo: ${context.repo.owner}/${context.repo.repo} + +/gu, '--@>')} +--> +`) +} diff --git a/build/actions/yarn.lock b/build/actions/yarn.lock new file mode 100644 index 0000000000..f338e7ca69 --- /dev/null +++ b/build/actions/yarn.lock @@ -0,0 +1,421 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@actions/core@^1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.2.6.tgz#a78d49f41a4def18e88ce47c2cac615d5694bf09" + integrity sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA== + +"@actions/github@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-2.1.1.tgz#bcabedff598196d953f58ba750d5e75549a75142" + integrity sha512-kAgTGUx7yf5KQCndVeHSwCNZuDBvPyxm5xKTswW2lofugeuC1AZX73nUUVDNaysnM9aKFMHv9YCdVJbg7syEyA== + dependencies: + "@actions/http-client" "^1.0.3" + "@octokit/graphql" "^4.3.1" + "@octokit/rest" "^16.43.1" + +"@actions/http-client@^1.0.3": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.8.tgz#8bd76e8eca89dc8bcf619aa128eba85f7a39af45" + integrity sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA== + dependencies: + tunnel "0.0.6" + +"@octokit/auth-token@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f" + integrity sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg== + dependencies: + "@octokit/types" "^2.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.1.tgz#16d5c0e7a83e3a644d1ddbe8cded6c3d038d31d7" + integrity sha512-pOPHaSz57SFT/m3R5P8MUu4wLPszokn5pXcB/pzavLTQf2jbU+6iayTvzaY6/BiotuRS0qyEUkx3QglT4U958A== + dependencies: + "@octokit/types" "^2.11.1" + is-plain-object "^3.0.0" + universal-user-agent "^5.0.0" + +"@octokit/graphql@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.3.1.tgz#9ee840e04ed2906c7d6763807632de84cdecf418" + integrity sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^2.0.0" + universal-user-agent "^4.0.0" + +"@octokit/plugin-paginate-rest@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" + integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== + dependencies: + "@octokit/types" "^2.0.1" + +"@octokit/plugin-request-log@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" + integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== + +"@octokit/plugin-rest-endpoint-methods@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" + integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== + dependencies: + "@octokit/types" "^2.0.1" + deprecation "^2.3.1" + +"@octokit/request-error@^1.0.2": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" + integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== + dependencies: + "@octokit/types" "^2.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request-error@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.0.tgz#94ca7293373654400fbb2995f377f9473e00834b" + integrity sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw== + dependencies: + "@octokit/types" "^2.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.2.0", "@octokit/request@^5.3.0": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.2.tgz#74f8e5bbd39dc738a1b127629791f8ad1b3193ee" + integrity sha512-zKdnGuQ2TQ2vFk9VU8awFT4+EYf92Z/v3OlzRaSh4RIP0H6cvW1BFPXq4XYvNez+TPQjqN+0uSkCYnMFFhcFrw== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^2.11.1" + deprecation "^2.0.0" + is-plain-object "^3.0.0" + node-fetch "^2.3.0" + once "^1.4.0" + universal-user-agent "^5.0.0" + +"@octokit/rest@^16.43.1": + version "16.43.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b" + integrity sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/plugin-paginate-rest" "^1.1.1" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "2.4.0" + "@octokit/request" "^5.2.0" + "@octokit/request-error" "^1.0.2" + atob-lite "^2.0.0" + before-after-hook "^2.0.0" + btoa-lite "^1.0.0" + deprecation "^2.0.0" + lodash.get "^4.4.2" + lodash.set "^4.3.2" + lodash.uniq "^4.5.0" + octokit-pagination-methods "^1.1.0" + once "^1.4.0" + universal-user-agent "^4.0.0" + +"@octokit/types@^2.0.0", "@octokit/types@^2.0.1", "@octokit/types@^2.11.1": + version "2.12.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.12.1.tgz#4a26b4a85ec121043d3b0745b5798f9d8fd968ca" + integrity sha512-LRLR1tjbcCfAmUElvTmMvLEzstpx6Xt/aQVTg2xvd+kHA2Ekp1eWl5t+gU7bcwjXHYEAzh4hH4WH+kS3vh+wRw== + dependencies: + "@types/node" ">= 8" + +"@types/node@>= 8": + version "13.13.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.2.tgz#160d82623610db590a64e8ca81784e11117e5a54" + integrity sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +atob-lite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" + integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= + +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + +before-after-hook@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + +btoa-lite@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" + integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +follow-redirects@^1.14.0: + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +is-plain-object@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928" + integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg== + dependencies: + isobject "^4.0.0" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" + integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +macos-release@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" + integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch@^2.3.0: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +octokit-pagination-methods@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" + integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +os-name@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" + integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== + dependencies: + macos-release "^2.2.0" + windows-release "^3.1.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +source-map-support@^0.5.17: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +ts-node@^8.6.2: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.0.tgz#d7bf7272dcbecd3a2aa18bd0b96c7d2f270c15d4" + integrity sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + +universal-user-agent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" + integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== + dependencies: + os-name "^3.1.0" + +universal-user-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" + integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q== + dependencies: + os-name "^3.1.0" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +windows-release@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.0.tgz#dce167e9f8be733f21c849ebd4d03fe66b29b9f0" + integrity sha512-2HetyTg1Y+R+rUgrKeUEhAG/ZuOmTrI1NBb3ZyAGQMYmOJjBBPe4MTodghRkmLJZHwkuPi02anbeGP+Zf401LQ== + dependencies: + execa "^1.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==