Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config based approach to commands #60

Merged
merged 17 commits into from
Jul 29, 2022
61 changes: 61 additions & 0 deletions app/command-enabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export function transferEnabled(octokit, config) {
const transferConfig = config.commands.transfer;

// TODO check permissions
return {
enabled: transferConfig.enabled,
};
}

export function labelEnabled(octokit, config, labels) {
const labelConfig = config.commands.label;

if (!labelConfig.enabled) {
return {
enabled: false,
error: "The label command is not enabled for this repository",
};
}

// TODO set intersection
if (
labelConfig.allowed_labels.length > 0 &&
!labels.includes(labelConfig.allowed_labels[0])
) {
return {
enabled: false,
error: `${labels} doesn't match the allowed labels \`${labelConfig.allowed_labels.join(
","
)}\``,
};
}

// TODO check permissions
return {
enabled: true,
};
}

export function closeEnabled(config, octokit) {
return {
enabled: true,
};
}

export function reopenEnabled(config, octokit) {
return {
enabled: true,
};
}

export function removeLabelEnabled(config, octokit, labels) {
return {
enabled: true,
};
}

export function reviewerEnabled(config, octokit) {
return {
enabled: true,
};
}
53 changes: 53 additions & 0 deletions app/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
closeMatcher,
labelMatcher,
removeLabelMatcher,
reopenMatcher,
reviewerMatcher,
transferMatcher,
} from "./matchers.js";
import {
closeEnabled,
labelEnabled,
removeLabelEnabled,
reopenEnabled,
reviewerEnabled,
transferEnabled,
} from "./command-enabled.js";

export function getCommands(commentBody) {
return {
transfer: {
matches: transferMatcher(commentBody),
enabled: (config, octokit) => transferEnabled(octokit, config),
},
close: {
matches: closeMatcher(commentBody),
enabled: (config, octokit) => closeEnabled(config, octokit),
},
reopen: {
matches: reopenMatcher(commentBody),
enabled: (config, octokit) => reopenEnabled(config, octokit),
},
label: {
matches: labelMatcher(commentBody),
enabled: (octokit, config, labels) =>
labelEnabled(octokit, config, labels),
},
"remove-label": {
timja marked this conversation as resolved.
Show resolved Hide resolved
matches: removeLabelMatcher(commentBody),
enabled: (config, octokit, labels) =>
removeLabelEnabled(config, octokit, labels),
},
reviewer: {
matches: reviewerMatcher(commentBody),
enabled: (config, octokit) => reviewerEnabled(config, octokit),
},
};
}

export function noneMatch(commands) {
return Object.keys(commands)
.map((key) => !!commands[key].matches)
.every((element) => element === false);
timja marked this conversation as resolved.
Show resolved Hide resolved
}
20 changes: 20 additions & 0 deletions app/commands.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getCommands, noneMatch } from "./commands.js";

describe("commands", () => {
test("noneMatch is true when nothing matches", () => {
const commands = getCommands("nothing that would ever match");

expect(noneMatch(commands)).toEqual(true);
});

test("noneMatch is false when something matches", () => {
const commands = getCommands("/label one");

expect(noneMatch(commands)).toEqual(false);
});
test("noneMatch is false when multiple matches", () => {
const commands = getCommands("/label one /reviewer reviewer");

expect(noneMatch(commands)).toEqual(false);
});
});
26 changes: 26 additions & 0 deletions app/default-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const defaultConfig = {
commands: {
label: {
permission: "author-or-member",
allowed_labels: [],
timja marked this conversation as resolved.
Show resolved Hide resolved
enabled: true,
},
"remove-label": {
timja marked this conversation as resolved.
Show resolved Hide resolved
permission: "none",
allowed_labels: [],
enabled: true,
},
reopen: {
permission: "member",
enabled: true,
},
reviewer: {
permission: "none",
enabled: true,
},
transfer: {
permission: "write-single-or-author",
enabled: true,
},
},
};
2 changes: 1 addition & 1 deletion app/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!) {
Expand Down
158 changes: 94 additions & 64 deletions app/router.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,74 @@
import {
closeMatcher,
labelMatcher,
removeLabelMatcher,
reopenMatcher,
reviewerMatcher,
transferMatcher,
} from "./matchers.js";
import {
addLabel,
closeIssue,
removeLabel,
reopenIssue,
reportError,
requestReviewers,
transferIssue,
} from "./github.js";
import { getAuthToken } from "./auth.js";
import { extractCommaSeparated, extractUsersAndTeams } from "./converters.js";
import { defaultConfig } from "./default-config.js";

import { Octokit } from "@octokit/core";
import { config as octoKitConfig } from "@probot/octokit-plugin-config";

import deepmerge from "deepmerge";
import { getCommands, noneMatch } from "./commands.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
);

const commands = getCommands(payload.comment.body);

if (noneMatch(commands)) {
if (verbose) {
console.log("No match for", payload.comment.body);
}
return;
}

const closeMatches = closeMatcher(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
timja marked this conversation as resolved.
Show resolved Hide resolved
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 transferMatches = commands.transfer.matches;
if (commands.transfer.matches) {
const enabled = await commands.transfer.enabled(octokit, config);

if (enabled) {
const targetRepo = transferMatches[1];
console.log(
`${id} Transferring issue ${payload.issue.html_url} to repo ${targetRepo} ${actorRequest}`
);
await transferIssue(
authToken,
payload.repository.owner.login,
sourceRepo,
targetRepo,
payload.issue.node_id
);
return;
} else {
await reportError(
authToken,
payload.issue.node_id,
"/transfer is not enabled for this repository"
);
}
}

const closeMatches = commands.close.matches;
if (closeMatches) {
const reason =
closeMatches.length > 1 && closeMatches[1] === "not-planned"
Expand All @@ -45,63 +77,65 @@ export async function router(auth, id, payload, verbose) {
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
);
await closeIssue(authToken, sourceRepo, payload.issue.node_id, reason);
return;
}

const reopenMatches = reopenMatcher(payload.comment.body);
const reopenMatches = commands.reopen.matches;
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
);
await reopenIssue(authToken, sourceRepo, payload.issue.node_id);
return;
}

const labelMatches = labelMatcher(payload.comment.body);
const labelMatches = commands.label.matches;
if (labelMatches) {
const labels = extractCommaSeparated(labelMatches[1]);
const result = commands.label.enabled(octokit, config, labels);
timja marked this conversation as resolved.
Show resolved Hide resolved

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;
if (result.enabled) {
console.log(
`${id} Labeling issue ${payload.issue.html_url} with labels ${labels} ${actorRequest}`
);
await addLabel(
authToken,
payload.repository.owner.login,
sourceRepo,
payload.issue.node_id,
labels
);
return;
} else {
await reportError(authToken, payload.issue.node_id, result.error);
}
}

const removeLabelMatches = removeLabelMatcher(payload.comment.body);
const removeLabelMatches = commands["remove-label"].matches;
timja marked this conversation as resolved.
Show resolved Hide resolved
if (removeLabelMatches) {
const labels = extractCommaSeparated(removeLabelMatches[1]);

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 result = commands["remove-label"].enabled(octokit, config, labels);

if (result.enabled) {
console.log(
`${id} Removing label(s) from issue ${payload.issue.html_url}, labels ${labels} ${actorRequest}`
);
await removeLabel(
authToken,
payload.repository.owner.login,
sourceRepo,
payload.issue.node_id,
labels
);
return;
} else {
await reportError(authToken, payload.issue.node_id, result.error);
}
}

const reviewerMatches = reviewerMatcher(payload.comment.body);
const reviewerMatches = commands.reviewer.matches;
if (reviewerMatches) {
console.log(
`${id} Requesting review for ${reviewerMatches[1]} at ${payload.issue.html_url} ${actorRequest}`
Expand All @@ -111,7 +145,7 @@ export async function router(auth, id, payload, verbose) {
reviewerMatches[1]
);
await requestReviewers(
await getAuthToken(auth, payload.installation.id),
authToken,
payload.repository.owner.login,
sourceRepo,
payload.issue.node_id,
Expand All @@ -120,8 +154,4 @@ export async function router(auth, id, payload, verbose) {
);
return;
}

if (verbose) {
console.log("No match for", payload.comment.body);
}
}
Loading