Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ commands:
enabled: false
label:
allowedLabels: [] # any label is allowed
protectedLabels: [] # only collaborators, members and owners can modify these
enabled: false
removeLabel:
allowedLabels: [] # any label is allowed
protectedLabels: [] # only collaborators, members and owners can modify these
enabled: false
reopen:
enabled: false
Expand Down Expand Up @@ -89,6 +91,8 @@ Closes the current issue. `not-planned` can be passed as the closure reason, e.g

Adds a label to the current issue or pull request

`protectedLabels` can be used to restrict specific labels so they can only be modified by repository collaborators, members and owners.

#### Permissions required

- Issues
Expand All @@ -100,6 +104,8 @@ Adds a label to the current issue or pull request

Removes a label from the current issue or pull request

`protectedLabels` can be used to restrict specific labels so they can only be modified by repository collaborators, members and owners.

#### Permissions required

- Issues
Expand Down
35 changes: 30 additions & 5 deletions app/command-enabled.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,35 @@ function notEnabled(command) {
};
}

function trimLabels(labels) {
function trimLabels(labels = []) {
return labels.map((it) => it.trim());
}

const privilegedAuthorAssociations = new Set([
"COLLABORATOR",
"MEMBER",
"OWNER",
]);

function protectedLabelEnabled(labelConfig, labels, authorAssociation) {
const protectedLabels = trimLabels(labelConfig.protectedLabels);
const protectedRequestedLabels = labels.filter((label) =>
protectedLabels.includes(label),
);
if (protectedRequestedLabels.length === 0) {
return enabled;
}

if (privilegedAuthorAssociations.has(authorAssociation)) {
return enabled;
}

return {
enabled: false,
error: `${protectedRequestedLabels.join(",")} are protected labels and can only be modified by repository collaborators`,
};
}

export function transferEnabled(config) {
if (!config.commands.transfer.enabled) {
return notEnabled("transfer");
Expand All @@ -21,7 +46,7 @@ export function transferEnabled(config) {
return enabled;
}

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

if (!labelConfig.enabled) {
Expand All @@ -42,7 +67,7 @@ export function labelEnabled(config, labels) {
};
}

return enabled;
return protectedLabelEnabled(labelConfig, labels, authorAssociation);
}

export function closeEnabled(config) {
Expand All @@ -61,7 +86,7 @@ export function reopenEnabled(config) {
return enabled;
}

export function removeLabelEnabled(config, labels) {
export function removeLabelEnabled(config, labels, authorAssociation) {
const labelConfig = config.commands.removeLabel;
if (!labelConfig.enabled) {
return notEnabled("remove-label");
Expand All @@ -81,7 +106,7 @@ export function removeLabelEnabled(config, labels) {
};
}

return enabled;
return protectedLabelEnabled(labelConfig, labels, authorAssociation);
}

export function reviewerEnabled(config) {
Expand Down
72 changes: 72 additions & 0 deletions app/command-enabled.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,42 @@ describe("command-enabled", () => {

expect(sut.enabled).toEqual(false);
});

test("is disabled when protected label is modified by non-collaborator", () => {
const sut = labelEnabled(
{
commands: {
label: {
enabled: true,
allowedLabels: [],
protectedLabels: ["status:active-incident"],
},
},
},
["status:active-incident"],
"CONTRIBUTOR",
);

expect(sut.enabled).toEqual(false);
});

test("is enabled when protected label is modified by collaborator", () => {
const sut = labelEnabled(
{
commands: {
label: {
enabled: true,
allowedLabels: [],
protectedLabels: ["status:active-incident"],
},
},
},
["status:active-incident"],
"COLLABORATOR",
);

expect(sut.enabled).toEqual(true);
});
});

test("is enabled when label is in allowedLabels with blanks", () => {
Expand Down Expand Up @@ -284,6 +320,42 @@ describe("command-enabled", () => {

expect(sut.enabled).toEqual(false);
});

test("is disabled when protected label is removed by non-collaborator", () => {
const sut = removeLabelEnabled(
{
commands: {
removeLabel: {
enabled: true,
allowedLabels: [],
protectedLabels: ["status:active-incident"],
},
},
},
["status:active-incident"],
"FIRST_TIME_CONTRIBUTOR",
);

expect(sut.enabled).toEqual(false);
});

test("is enabled when protected label is removed by member", () => {
const sut = removeLabelEnabled(
{
commands: {
removeLabel: {
enabled: true,
allowedLabels: [],
protectedLabels: ["status:active-incident"],
},
},
},
["status:active-incident"],
"MEMBER",
);

expect(sut.enabled).toEqual(true);
});
});

describe("reviewerEnabled", () => {
Expand Down
3 changes: 2 additions & 1 deletion app/commands/label-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { extractCommaSeparated } from "../converters.js";

import { getLogger } from "../logger.js";
import {
extractAuthorAssociation,
extractBody,
extractHtmlUrl,
extractLabelableId,
Expand All @@ -24,7 +25,7 @@ export class LabelCommand extends Command {

enabled(config) {
const labels = extractCommaSeparated(this.matches()[1]);
return labelEnabled(config, labels);
return labelEnabled(config, labels, extractAuthorAssociation(this.payload));
}

async run(authToken) {
Expand Down
7 changes: 6 additions & 1 deletion app/commands/remove-label-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Command } from "./command.js";
import { extractCommaSeparated } from "../converters.js";
import { getLogger } from "../logger.js";
import {
extractAuthorAssociation,
extractBody,
extractHtmlUrl,
extractLabelableId,
Expand All @@ -23,7 +24,11 @@ export class RemoveLabelCommand extends Command {

enabled(config) {
const removeLabels = extractCommaSeparated(this.matches()[1]);
return removeLabelEnabled(config, removeLabels);
return removeLabelEnabled(
config,
removeLabels,
extractAuthorAssociation(this.payload),
);
}

async run(authToken) {
Expand Down
12 changes: 12 additions & 0 deletions app/comment-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ export function extractLabelableId(payload) {

return payload.issue.node_id;
}

export function extractAuthorAssociation(payload) {
if (payload.review) {
return payload.review.author_association;
}

if (payload.pull_request) {
return payload.pull_request.author_association;
}

return payload.comment.author_association;
}
33 changes: 33 additions & 0 deletions app/comment-extractor.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
extractAuthorAssociation,
extractBody,
extractHtmlUrl,
extractLabelableId,
Expand Down Expand Up @@ -76,4 +77,36 @@ describe("extractors", () => {
).toEqual("PR_abcdefgh");
});
});

describe("extractAuthorAssociation", () => {
test("is a review", () => {
expect(
extractAuthorAssociation({
review: {
author_association: "MEMBER",
},
}),
).toEqual("MEMBER");
});

test("is a pull request body command", () => {
expect(
extractAuthorAssociation({
pull_request: {
author_association: "OWNER",
},
}),
).toEqual("OWNER");
});

test("is an issue comment", () => {
expect(
extractAuthorAssociation({
comment: {
author_association: "CONTRIBUTOR",
},
}),
).toEqual("CONTRIBUTOR");
});
});
});
2 changes: 2 additions & 0 deletions app/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ export const defaultConfig = {
commands: {
label: {
allowedLabels: [],
protectedLabels: [],
enabled: false,
},
removeLabel: {
allowedLabels: [],
protectedLabels: [],
enabled: false,
},
reopen: {
Expand Down