diff --git a/README.md b/README.md index b30e269..dcd4852 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,34 @@ It uses GitHub webhooks to scale across repositories without needing to add a Gi GitHub apps are used for authentication to limit the required permissions. +## Configuration + +The app can be configured with a `.github/comment-ops.yml` file in the main branch of the repository. + +This can also be applied organization wide by creating it in the organization's `.github` repository. + +The order of configuration is: + +`default config` -> `organization` -> `repository` + +Default configuration: + +```yaml +commands: + label: + allowedLabels: [] # any label is allowed + enabled: true + removeLabel: + allowedLabels: [] # any label is allowed + enabled: true + reopen: + enabled: true + reviewer: + enabled: true + transfer: + enabled: true +``` + ## Getting started First you will need to create a GitHub app. Add the permissions required for the commands you are using (see next section), and tick "Subscribe to events" > "Issue comment" diff --git a/app/command-enabled.js b/app/command-enabled.js new file mode 100644 index 0000000..fc30fc9 --- /dev/null +++ b/app/command-enabled.js @@ -0,0 +1,87 @@ +const enabled = { + enabled: true, +}; + +function notEnabled(command) { + return { + enabled: false, + error: `The \`${command}\` is not enabled for this repository`, + }; +} + +export function transferEnabled(config) { + if (!config.commands.transfer.enabled) { + return notEnabled("transfer"); + } + + return enabled; +} + +export function labelEnabled(config, labels) { + const labelConfig = config.commands.label; + + if (!labelConfig.enabled) { + return notEnabled("label"); + } + + if ( + // if length is = 0 then all labels are allowed + labelConfig.allowedLabels.length > 0 && + !labels.every((label) => labelConfig.allowedLabels.includes(label)) + ) { + return { + enabled: false, + error: `${labels} doesn't match the allowed labels \`${labelConfig.allowedLabels.join( + "," + )}\``, + }; + } + + return enabled; +} + +export function closeEnabled(config) { + if (!config.commands.close.enabled) { + return notEnabled("close"); + } + + return enabled; +} + +export function reopenEnabled(config) { + if (!config.commands.reopen.enabled) { + return notEnabled("reopen"); + } + + return enabled; +} + +export function removeLabelEnabled(config, labels) { + const labelConfig = config.commands.removeLabel; + if (!labelConfig.enabled) { + return notEnabled("remove-label"); + } + + if ( + // if length is = 0 then all labels are allowed + labelConfig.allowedLabels.length > 0 && + !labels.every((label) => labelConfig.allowedLabels.includes(label)) + ) { + return { + enabled: false, + error: `${labels} doesn't match the allowed labels \`${labelConfig.allowedLabels.join( + "," + )}\``, + }; + } + + return enabled; +} + +export function reviewerEnabled(config) { + if (!config.commands.reviewer.enabled) { + return notEnabled("reviewer"); + } + + return enabled; +} diff --git a/app/command-enabled.test.js b/app/command-enabled.test.js new file mode 100644 index 0000000..93bdcad --- /dev/null +++ b/app/command-enabled.test.js @@ -0,0 +1,297 @@ +import { + closeEnabled, + labelEnabled, + removeLabelEnabled, + reopenEnabled, + reviewerEnabled, + transferEnabled, +} from "./command-enabled.js"; + +describe("command-enabled", () => { + describe("transferEnabled", () => { + test("is enabled when config is enabled", () => { + const sut = transferEnabled({ + commands: { + transfer: { + enabled: true, + }, + }, + }); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when config is disabled", () => { + const sut = transferEnabled({ + commands: { + transfer: { + enabled: false, + }, + }, + }); + + expect(sut.enabled).toEqual(false); + }); + }); + + describe("labelEnabled", () => { + test("is enabled when config is enabled", () => { + const sut = labelEnabled( + { + commands: { + label: { + enabled: true, + allowedLabels: [], + }, + }, + }, + ["label1"] + ); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when config is disabled", () => { + const sut = labelEnabled( + { + commands: { + label: { + enabled: false, + allowedLabels: [], + }, + }, + }, + ["label1"] + ); + + expect(sut.enabled).toEqual(false); + }); + + test("is enabled when label is in allowedLabels", () => { + const sut = labelEnabled( + { + commands: { + label: { + enabled: true, + allowedLabels: ["label2", "label1"], + }, + }, + }, + ["label1"] + ); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when label is not in allowedLabels", () => { + const sut = labelEnabled( + { + commands: { + label: { + enabled: true, + allowedLabels: ["label2", "label1"], + }, + }, + }, + ["label4"] + ); + + expect(sut.enabled).toEqual(false); + }); + test("is enabled when all labels are in allowedLabels", () => { + const sut = labelEnabled( + { + commands: { + label: { + enabled: true, + allowedLabels: ["label2", "label1", "label3", "label4"], + }, + }, + }, + ["label3", "label1", "label2"] + ); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when not all labels are in allowedLabels", () => { + const sut = labelEnabled( + { + commands: { + label: { + enabled: true, + allowedLabels: ["label2", "label1", "label3", "label4"], + }, + }, + }, + ["label2", "label1", "label5"] + ); + + expect(sut.enabled).toEqual(false); + }); + }); + + describe("closeEnabled", () => { + test("is enabled when config is enabled", () => { + const sut = closeEnabled({ + commands: { + close: { + enabled: true, + }, + }, + }); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when config is disabled", () => { + const sut = closeEnabled({ + commands: { + close: { + enabled: false, + }, + }, + }); + + expect(sut.enabled).toEqual(false); + }); + }); + describe("reopenEnabled", () => { + test("is enabled when config is enabled", () => { + const sut = reopenEnabled({ + commands: { + reopen: { + enabled: true, + }, + }, + }); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when config is disabled", () => { + const sut = reopenEnabled({ + commands: { + reopen: { + enabled: false, + }, + }, + }); + + expect(sut.enabled).toEqual(false); + }); + }); + + describe("removeLabelEnabled", () => { + test("is enabled when config is enabled", () => { + const sut = removeLabelEnabled( + { + commands: { + removeLabel: { + enabled: true, + allowedLabels: [], + }, + }, + }, + ["label1"] + ); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when config is disabled", () => { + const sut = removeLabelEnabled( + { + commands: { + removeLabel: { + enabled: false, + allowedLabels: [], + }, + }, + }, + ["label1"] + ); + + expect(sut.enabled).toEqual(false); + }); + + test("is enabled when label is in allowedLabels", () => { + const sut = removeLabelEnabled( + { + commands: { + removeLabel: { + enabled: true, + allowedLabels: ["label2", "label1"], + }, + }, + }, + ["label1"] + ); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when label is not in allowedLabels", () => { + const sut = removeLabelEnabled( + { + commands: { + removeLabel: { + enabled: true, + allowedLabels: ["label2", "label1"], + }, + }, + }, + ["label4"] + ); + + expect(sut.enabled).toEqual(false); + }); + test("is enabled when all labels are in allowedLabels", () => { + const sut = removeLabelEnabled( + { + commands: { + removeLabel: { + enabled: true, + allowedLabels: ["label2", "label1", "label3", "label4"], + }, + }, + }, + ["label3", "label1", "label2"] + ); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when not all labels are in allowedLabels", () => { + const sut = removeLabelEnabled( + { + commands: { + removeLabel: { + enabled: true, + allowedLabels: ["label2", "label1", "label3", "label4"], + }, + }, + }, + ["label2", "label1", "label5"] + ); + + expect(sut.enabled).toEqual(false); + }); + }); + + describe("reviewerEnabled", () => { + test("is enabled when config is enabled", () => { + const sut = reviewerEnabled({ + commands: { + reviewer: { + enabled: true, + }, + }, + }); + + expect(sut.enabled).toEqual(true); + }); + test("is disabled when config is disabled", () => { + const sut = reviewerEnabled({ + commands: { + reviewer: { + enabled: false, + }, + }, + }); + + expect(sut.enabled).toEqual(false); + }); + }); +}); diff --git a/app/commands.js b/app/commands.js new file mode 100644 index 0000000..74acde3 --- /dev/null +++ b/app/commands.js @@ -0,0 +1,23 @@ +import { TransferCommand } from "./commands/transfer-command.js"; +import { CloseCommand } from "./commands/close-command.js"; +import { ReopenCommand } from "./commands/reopen-command.js"; +import { LabelCommand } from "./commands/label-command.js"; +import { RemoveLabelCommand } from "./commands/remove-label-command.js"; +import { ReviewerCommand } from "./commands/reviewer-command.js"; + +export function getCommands(id, payload) { + return [ + new TransferCommand(id, payload), + new CloseCommand(id, payload), + new ReopenCommand(id, payload), + new LabelCommand(id, payload), + new RemoveLabelCommand(id, payload), + new ReviewerCommand(id, payload), + ]; +} + +export function noneMatch(commands) { + return !commands.some((command) => { + return command.matches() !== null; + }); +} diff --git a/app/commands.test.js b/app/commands.test.js new file mode 100644 index 0000000..af8ed5d --- /dev/null +++ b/app/commands.test.js @@ -0,0 +1,32 @@ +import { getCommands, noneMatch } from "./commands.js"; + +describe("commands", () => { + test("noneMatch is true when nothing matches", () => { + const commands = getCommands("any", { + comment: { + body: "nothing that would every match", + }, + }); + + expect(noneMatch(commands)).toEqual(true); + }); + + test("noneMatch is false when something matches", () => { + const commands = getCommands("any", { + comment: { + body: "/label one", + }, + }); + + expect(noneMatch(commands)).toEqual(false); + }); + test("noneMatch is false when multiple matches", () => { + const commands = getCommands("any", { + comment: { + body: "/label one \n/reviewer reviewer", + }, + }); + + expect(noneMatch(commands)).toEqual(false); + }); +}); diff --git a/app/commands/actor-request.js b/app/commands/actor-request.js new file mode 100644 index 0000000..d9206ce --- /dev/null +++ b/app/commands/actor-request.js @@ -0,0 +1,3 @@ +export function actorRequest(payload) { + return `as requested by ${payload.sender.login}`; +} diff --git a/app/commands/close-command.js b/app/commands/close-command.js new file mode 100644 index 0000000..382bb30 --- /dev/null +++ b/app/commands/close-command.js @@ -0,0 +1,34 @@ +import { Command } from "./command.js"; +import { closeMatcher } from "../matchers.js"; +import { closeEnabled } from "../command-enabled.js"; +import { closeIssue } from "../github.js"; +import { actorRequest } from "./actor-request.js"; + +export class CloseCommand extends Command { + constructor(id, payload) { + super(id, payload); + } + + matches() { + return closeMatcher(this.payload.comment.body); + } + + enabled(config) { + return closeEnabled(config); + } + + async run(authToken) { + const sourceRepo = this.payload.repository.name; + const closeMatches = this.matches(); + const reason = + closeMatches.length > 1 && closeMatches[1] === "not-planned" + ? "NOT_PLANNED" + : "COMPLETED"; + console.log( + `${this.id} Closing issue ${ + this.payload.issue.html_url + }, reason: ${reason} ${actorRequest(this.payload)}` + ); + await closeIssue(authToken, sourceRepo, this.payload.issue.node_id, reason); + } +} diff --git a/app/commands/command.js b/app/commands/command.js new file mode 100644 index 0000000..988b480 --- /dev/null +++ b/app/commands/command.js @@ -0,0 +1,22 @@ +export class Command { + constructor(id, payload) { + this.id = id; + this.payload = payload; + } + + matches() { + throw new Error("matches() must be implemented"); + } + + // eslint-disable-next-line no-unused-vars + enabled(config) { + return { + enabled: false, + }; + } + + // eslint-disable-next-line no-unused-vars + run(authToken) { + throw new Error("run(authToken) must be implemented"); + } +} diff --git a/app/commands/label-command.js b/app/commands/label-command.js new file mode 100644 index 0000000..24977b5 --- /dev/null +++ b/app/commands/label-command.js @@ -0,0 +1,39 @@ +import { labelMatcher } from "../matchers.js"; +import { labelEnabled } from "../command-enabled.js"; +import { addLabel } from "../github.js"; +import { Command } from "./command.js"; +import { actorRequest } from "./actor-request.js"; +import { extractCommaSeparated } from "../converters.js"; + +export class LabelCommand extends Command { + constructor(id, payload) { + super(id, payload); + } + + matches() { + return labelMatcher(this.payload.comment.body); + } + + enabled(config) { + const labels = extractCommaSeparated(this.matches()[1]); + return labelEnabled(config, labels); + } + + async run(authToken) { + const labels = extractCommaSeparated(this.matches()[1]); + const sourceRepo = this.payload.repository.name; + + console.log( + `${this.id} Labeling issue ${ + this.payload.issue.html_url + } with labels ${labels} ${actorRequest(this.payload)}` + ); + await addLabel( + authToken, + this.payload.repository.owner.login, + sourceRepo, + this.payload.issue.node_id, + labels + ); + } +} diff --git a/app/commands/remove-label-command.js b/app/commands/remove-label-command.js new file mode 100644 index 0000000..f358df9 --- /dev/null +++ b/app/commands/remove-label-command.js @@ -0,0 +1,38 @@ +import { removeLabelMatcher } from "../matchers.js"; +import { removeLabelEnabled } from "../command-enabled.js"; +import { removeLabel } from "../github.js"; +import { Command } from "./command.js"; +import { actorRequest } from "./actor-request.js"; +import { extractCommaSeparated } from "../converters.js"; + +export class RemoveLabelCommand extends Command { + constructor(id, payload) { + super(id, payload); + } + + matches() { + return removeLabelMatcher(this.payload.comment.body); + } + + enabled(config) { + const removeLabels = extractCommaSeparated(this.matches()[1]); + return removeLabelEnabled(config, removeLabels); + } + + async run(authToken) { + const sourceRepo = this.payload.repository.name; + const labels = extractCommaSeparated(this.matches()[1]); + console.log( + `${this.id} Removing label(s) from issue ${ + this.payload.issue.html_url + }, labels ${labels} ${actorRequest(this.payload)}` + ); + await removeLabel( + authToken, + this.payload.repository.owner.login, + sourceRepo, + this.payload.issue.node_id, + labels + ); + } +} diff --git a/app/commands/reopen-command.js b/app/commands/reopen-command.js new file mode 100644 index 0000000..703f1e1 --- /dev/null +++ b/app/commands/reopen-command.js @@ -0,0 +1,29 @@ +import { reopenMatcher } from "../matchers.js"; +import { reopenEnabled } from "../command-enabled.js"; +import { reopenIssue } from "../github.js"; +import { Command } from "./command.js"; +import { actorRequest } from "./actor-request.js"; + +export class ReopenCommand extends Command { + constructor(id, payload) { + super(id, payload); + } + + matches() { + return reopenMatcher(this.payload.comment.body); + } + + enabled(config) { + return reopenEnabled(config); + } + + async run(authToken) { + const sourceRepo = this.payload.repository.name; + console.log( + `${this.id} Re-opening issue ${ + this.payload.issue.html_url + } ${actorRequest(this.payload)}` + ); + await reopenIssue(authToken, sourceRepo, this.payload.issue.node_id); + } +} diff --git a/app/commands/reviewer-command.js b/app/commands/reviewer-command.js new file mode 100644 index 0000000..e197678 --- /dev/null +++ b/app/commands/reviewer-command.js @@ -0,0 +1,42 @@ +import { reviewerMatcher } from "../matchers.js"; +import { reviewerEnabled } from "../command-enabled.js"; +import { requestReviewers } from "../github.js"; +import { Command } from "./command.js"; +import { actorRequest } from "./actor-request.js"; +import { extractUsersAndTeams } from "../converters.js"; + +export class ReviewerCommand extends Command { + constructor(id, payload) { + super(id, payload); + } + + matches() { + return reviewerMatcher(this.payload.comment.body); + } + + enabled(config) { + return reviewerEnabled(config); + } + + async run(authToken) { + const reviewerMatches = this.matches()[1]; + const sourceRepo = this.payload.repository.name; + console.log( + `${this.id} Requesting review for ${reviewerMatches} at ${ + this.payload.issue.html_url + } ${actorRequest(this.payload)}` + ); + const reviewers = extractUsersAndTeams( + this.payload.repository.owner.login, + reviewerMatches + ); + await requestReviewers( + authToken, + this.payload.repository.owner.login, + sourceRepo, + this.payload.issue.node_id, + reviewers.users, + reviewers.teams + ); + } +} diff --git a/app/commands/transfer-command.js b/app/commands/transfer-command.js new file mode 100644 index 0000000..9dca986 --- /dev/null +++ b/app/commands/transfer-command.js @@ -0,0 +1,36 @@ +import { transferMatcher } from "../matchers.js"; +import { transferEnabled } from "../command-enabled.js"; +import { transferIssue } from "../github.js"; +import { Command } from "./command.js"; +import { actorRequest } from "./actor-request.js"; + +export class TransferCommand extends Command { + constructor(id, payload) { + super(id, payload); + } + + matches() { + return transferMatcher(this.payload.comment.body); + } + + enabled(config) { + return transferEnabled(config); + } + + async run(authToken) { + const targetRepo = this.matches()[1]; + const sourceRepo = this.payload.repository.name; + console.log( + `${this.id} Transferring issue ${ + this.payload.issue.html_url + } to repo ${targetRepo} ${actorRequest(this.payload)}` + ); + await transferIssue( + authToken, + this.payload.repository.owner.login, + sourceRepo, + targetRepo, + this.payload.issue.node_id + ); + } +} diff --git a/app/default-config.js b/app/default-config.js new file mode 100644 index 0000000..b7c9f7b --- /dev/null +++ b/app/default-config.js @@ -0,0 +1,21 @@ +export const defaultConfig = { + commands: { + label: { + allowedLabels: [], + enabled: true, + }, + removeLabel: { + allowedLabels: [], + enabled: true, + }, + reopen: { + enabled: true, + }, + reviewer: { + enabled: true, + }, + transfer: { + enabled: true, + }, + }, +}; diff --git a/app/github.js b/app/github.js index e134df4..7322b71 100644 --- a/app/github.js +++ b/app/github.js @@ -211,7 +211,7 @@ async function lookupTeam(token, organization, teamName, originalTeamName) { }; } -async function reportError(token, subjectId, comment) { +export async function reportError(token, subjectId, comment) { await graphql( ` mutation ($comment: String!, $subjectId: ID!) { diff --git a/app/matchers.js b/app/matchers.js index 76dbab2..137e98b 100644 --- a/app/matchers.js +++ b/app/matchers.js @@ -11,11 +11,12 @@ export function reopenMatcher(text) { } export function labelMatcher(text) { - return text.match(/\/label ([\sA-Za-z\d-,]+)/); + return text.match(/\/label ([\s/A-Za-z\d-,:]+)/); } export function removeLabelMatcher(text) { - return text.match(/\/remove-label ([\sA-Za-z\d-,]+)/); + // TODO prevent matching across lines + return text.match(/\/remove-label ([\s/A-Za-z\d-,:]+)/); } export function reviewerMatcher(text) { diff --git a/app/router.js b/app/router.js index 6287b22..d1b418a 100644 --- a/app/router.js +++ b/app/router.js @@ -1,127 +1,44 @@ -import { - closeMatcher, - labelMatcher, - removeLabelMatcher, - reopenMatcher, - reviewerMatcher, - transferMatcher, -} from "./matchers.js"; -import { - addLabel, - closeIssue, - removeLabel, - reopenIssue, - requestReviewers, - transferIssue, -} from "./github.js"; +import { reportError } from "./github.js"; import { getAuthToken } from "./auth.js"; -import { extractCommaSeparated, extractUsersAndTeams } from "./converters.js"; +import { defaultConfig } from "./default-config.js"; -export async function router(auth, id, payload, verbose) { - const sourceRepo = payload.repository.name; - const transferMatches = transferMatcher(payload.comment.body); - const actorRequest = `as requested by ${payload.sender.login}`; - if (transferMatches) { - const targetRepo = transferMatches[1]; - console.log( - `${id} Transferring issue ${payload.issue.html_url} to repo ${targetRepo} ${actorRequest}` - ); - await transferIssue( - await getAuthToken(auth, payload.installation.id), - payload.repository.owner.login, - sourceRepo, - targetRepo, - payload.issue.node_id - ); - return; - } +import { Octokit } from "@octokit/core"; +import { config as octoKitConfig } from "@probot/octokit-plugin-config"; - const closeMatches = closeMatcher(payload.comment.body); - if (closeMatches) { - const reason = - closeMatches.length > 1 && closeMatches[1] === "not-planned" - ? "NOT_PLANNED" - : "COMPLETED"; - console.log( - `${id} Closing issue ${payload.issue.html_url}, reason: ${reason} ${actorRequest}` - ); - await closeIssue( - await getAuthToken(auth, payload.installation.id), - sourceRepo, - payload.issue.node_id, - reason - ); - return; - } - - const reopenMatches = reopenMatcher(payload.comment.body); - if (reopenMatches) { - console.log( - `${id} Re-opening issue ${payload.issue.html_url} ${actorRequest}` - ); - await reopenIssue( - await getAuthToken(auth, payload.installation.id), - sourceRepo, - payload.issue.node_id - ); - return; - } +import deepmerge from "deepmerge"; +import { getCommands, noneMatch } from "./commands.js"; - const labelMatches = labelMatcher(payload.comment.body); - if (labelMatches) { - const labels = extractCommaSeparated(labelMatches[1]); - - console.log( - `${id} Labeling issue ${payload.issue.html_url} with labels ${labels} ${actorRequest}` - ); - await addLabel( - await getAuthToken(auth, payload.installation.id), - payload.repository.owner.login, - sourceRepo, - payload.issue.node_id, - labels - ); - return; - } - - const removeLabelMatches = removeLabelMatcher(payload.comment.body); - if (removeLabelMatches) { - const labels = extractCommaSeparated(removeLabelMatches[1]); +export async function router(auth, id, payload, verbose) { + const sourceRepo = payload.repository.name; - console.log( - `${id} Removing label(s) from issue ${payload.issue.html_url}, labels ${labels} ${actorRequest}` - ); - await removeLabel( - await getAuthToken(auth, payload.installation.id), - payload.repository.owner.login, - sourceRepo, - payload.issue.node_id, - labels - ); - return; - } + const commands = getCommands(id, payload); - const reviewerMatches = reviewerMatcher(payload.comment.body); - if (reviewerMatches) { - console.log( - `${id} Requesting review for ${reviewerMatches[1]} at ${payload.issue.html_url} ${actorRequest}` - ); - const reviewers = extractUsersAndTeams( - payload.repository.owner.login, - reviewerMatches[1] - ); - await requestReviewers( - await getAuthToken(auth, payload.installation.id), - payload.repository.owner.login, - sourceRepo, - payload.issue.node_id, - reviewers.users, - reviewers.teams - ); + if (noneMatch(commands)) { + console.log("none match"); + if (verbose) { + console.log("No match for", payload.comment.body); + } return; } - if (verbose) { - console.log("No match for", payload.comment.body); + const authToken = await getAuthToken(auth, payload.installation.id); + const OctokitConfig = Octokit.plugin(octoKitConfig); + const octokit = new OctokitConfig({ auth: authToken }); + + // TODO validate against schema + // noinspection JSUnusedGlobalSymbols + const { config } = await octokit.config.get({ + owner: payload.repository.owner.login, + repo: sourceRepo, + path: ".github/comment-ops.yml", + defaults: (configs) => deepmerge.all([defaultConfig, ...configs]), + }); + + const runCommands = commands.filter((command) => command.matches()); + for (const command of runCommands) { + const result = command.enabled(config); + result.enabled + ? await command.run(authToken) + : await reportError(authToken, payload.issue.node_id, result.error); } } diff --git a/docker-compose.yml b/docker-compose.yml index 2d0392d..942255c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,4 @@ services: environment: - GITHUB_APP_ID - GITHUB_APP_PRIVATE_KEY - - GITHUB_APP_INSTALLATION_ID - - SOURCE_OWNER - - TARGET_OWNER - WEBHOOK_SECRET diff --git a/package-lock.json b/package-lock.json index e82b266..91966f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,11 @@ "license": "MIT", "dependencies": { "@octokit/auth-app": "^4.0.4", + "@octokit/core": "^4.0.4", "@octokit/graphql": "^5.0.0", - "@octokit/webhooks": "^10.0.9" + "@octokit/webhooks": "^10.0.9", + "@probot/octokit-plugin-config": "^1.1.5", + "deepmerge": "^4.2.2" }, "devDependencies": { "@types/jest": "^28.1.6", @@ -1260,6 +1263,76 @@ "node": ">= 14" } }, + "node_modules/@octokit/auth-token": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.0.tgz", + "integrity": "sha512-MDNFUBcJIptB9At7HiV7VCvU3NcL4GnfCQaP8C5lrxWrRPMJBnemYtehaKSOlaM7AYxeRyj9etenu8LVpSpVaQ==", + "dependencies": { + "@octokit/types": "^6.0.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.4.tgz", + "integrity": "sha512-sUpR/hc4Gc7K34o60bWC7WUH6Q7T6ftZ2dUmepSyJr9PRF76/qqkWjE2SOEzCqLA5W83SaISymwKtxks+96hPQ==", + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.0.tgz", + "integrity": "sha512-Kz/mIkOTjs9rV50hf/JK9pIDl4aGwAtT8pry6Rpy+hVXkAPhXanNQRxMoq6AeRgDCZR6t/A1zKniY2V1YhrzlQ==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.0.tgz", + "integrity": "sha512-7IAmHnaezZrgUqtRShMlByJK33MT9ZDnMRgZjnRrRV9a/jzzFwKGz0vxhFU6i7VMLraYcQ1qmcAOin37Kryq+Q==", + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.0.tgz", + "integrity": "sha512-WBtpzm9lR8z4IHIMtOqr6XwfkGvMOOILNLxsWvDwtzm/n7f5AWuqJTXQXdDtOvPfTDrH4TPhEvW2qMlR4JFA2w==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@octokit/endpoint": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", @@ -1466,6 +1539,34 @@ "node": ">= 14" } }, + "node_modules/@probot/octokit-plugin-config": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-1.1.5.tgz", + "integrity": "sha512-dPrccDkb5QVZYZ3Gq3aDEdfsuqid687iu+z3jBKFI1LwgQuRaUsmihR0ZLHdXKX6HK6rUw/5Jxg5ZUo0OWWUSA==", + "dependencies": { + "@types/js-yaml": "^4.0.5", + "js-yaml": "^4.1.0" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@probot/octokit-plugin-config/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@probot/octokit-plugin-config/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@sinclair/typebox": { "version": "0.24.20", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.20.tgz", @@ -1579,6 +1680,11 @@ "pretty-format": "^28.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -1987,6 +2093,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2293,7 +2404,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6375,6 +6485,63 @@ } } }, + "@octokit/auth-token": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.0.tgz", + "integrity": "sha512-MDNFUBcJIptB9At7HiV7VCvU3NcL4GnfCQaP8C5lrxWrRPMJBnemYtehaKSOlaM7AYxeRyj9etenu8LVpSpVaQ==", + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.4.tgz", + "integrity": "sha512-sUpR/hc4Gc7K34o60bWC7WUH6Q7T6ftZ2dUmepSyJr9PRF76/qqkWjE2SOEzCqLA5W83SaISymwKtxks+96hPQ==", + "requires": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/endpoint": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.0.tgz", + "integrity": "sha512-Kz/mIkOTjs9rV50hf/JK9pIDl4aGwAtT8pry6Rpy+hVXkAPhXanNQRxMoq6AeRgDCZR6t/A1zKniY2V1YhrzlQ==", + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.0.tgz", + "integrity": "sha512-7IAmHnaezZrgUqtRShMlByJK33MT9ZDnMRgZjnRrRV9a/jzzFwKGz0vxhFU6i7VMLraYcQ1qmcAOin37Kryq+Q==", + "requires": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.0.tgz", + "integrity": "sha512-WBtpzm9lR8z4IHIMtOqr6XwfkGvMOOILNLxsWvDwtzm/n7f5AWuqJTXQXdDtOvPfTDrH4TPhEvW2qMlR4JFA2w==", + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + } + } + }, "@octokit/endpoint": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", @@ -6551,6 +6718,30 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-6.2.4.tgz", "integrity": "sha512-MlumL1ClswnrebjNWmUFiHjEqpRl2T3Eh6j6R9mP2SNK7SjYw4tXGeCKHOyZiNpLjc5/1+P39BxwLN28zNn8iQ==" }, + "@probot/octokit-plugin-config": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-1.1.5.tgz", + "integrity": "sha512-dPrccDkb5QVZYZ3Gq3aDEdfsuqid687iu+z3jBKFI1LwgQuRaUsmihR0ZLHdXKX6HK6rUw/5Jxg5ZUo0OWWUSA==", + "requires": { + "@types/js-yaml": "^4.0.5", + "js-yaml": "^4.1.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + } + } + }, "@sinclair/typebox": { "version": "0.24.20", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.20.tgz", @@ -6664,6 +6855,11 @@ "pretty-format": "^28.0.0" } }, + "@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==" + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -6964,6 +7160,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7198,8 +7399,7 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "deprecation": { "version": "2.3.1", diff --git a/package.json b/package.json index ff99959..4a283d5 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,19 @@ "scripts": { "start": "node index.js", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "lint": "eslint .", - "lint:fix": "eslint --fix ." + "lint": "eslint . && prettier --check .", + "lint:fix": "prettier --write . && eslint --fix ." }, "author": "", "type": "module", "license": "MIT", "dependencies": { "@octokit/auth-app": "^4.0.4", + "@octokit/core": "^4.0.4", "@octokit/graphql": "^5.0.0", - "@octokit/webhooks": "^10.0.9" + "@octokit/webhooks": "^10.0.9", + "@probot/octokit-plugin-config": "^1.1.5", + "deepmerge": "^4.2.2" }, "devDependencies": { "@types/jest": "^28.1.6",