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

Feat/required start labels #80

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu
assignedIssueScope: "org" # or "org" or "network". Default is org
emptyWalletText: "Please set your wallet address with the /wallet command first and try again."
rolesWithReviewAuthority: ["MEMBER", "OWNER"]
requiredLabelsToStart: ["Priority: 5 (Emergency)"]
```

# Testing
Expand Down
10 changes: 9 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
"items": {
"type": "string"
}
},
"requiredLabelsToStart": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
Expand All @@ -87,7 +94,8 @@
"maxConcurrentTasks",
"assignedIssueScope",
"emptyWalletText",
"rolesWithReviewAuthority"
"rolesWithReviewAuthority",
"requiredLabelsToStart"
]
}
}
13 changes: 12 additions & 1 deletion src/handlers/shared/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ export async function start(
teammates: string[]
): Promise<Result> {
const { logger, config } = context;
const { taskStaleTimeoutDuration } = config;
const { taskStaleTimeoutDuration, requiredLabelsToStart } = config;

const issueLabels = issue.labels.map((label) => label.name);

if (requiredLabelsToStart.length && !requiredLabelsToStart.some((label) => issueLabels.includes(label))) {
// The "Priority" label must reflect a business priority, not a development one.
throw logger.error("This task does not reflect a business priority at the moment and cannot be started. This will be reassessed in the coming weeks.", {
requiredLabelsToStart,
issueLabels,
issue: issue.html_url,
});
}

if (!sender) {
throw logger.error(`Skipping '/start' since there is no sender in the context.`);
Expand Down
3 changes: 1 addition & 2 deletions src/handlers/user-start-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export async function userStartStop(context: Context): Promise<Result> {
if (!isIssueCommentEvent(context)) {
return { status: HttpStatusCode.NOT_MODIFIED };
}
const { payload } = context;
const { issue, comment, sender, repository } = payload;
const { issue, comment, sender, repository } = context.payload;
const slashCommand = comment.body.trim().split(" ")[0].replace("/", "");
const teamMates = comment.body
.split("@")
Expand Down
1 change: 1 addition & 0 deletions src/types/plugin-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const pluginSettingsSchema = T.Object(
rolesWithReviewAuthority: T.Transform(rolesWithReviewAuthority)
.Decode((value) => value.map((role) => role.toUpperCase()))
.Encode((value) => value.map((role) => role.toUpperCase())),
requiredLabelsToStart: T.Array(T.String(), { default: [] }),
},
{
default: {},
Expand Down
3 changes: 3 additions & 0 deletions tests/__mocks__/issue-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export default {
{
name: "Time: 1h",
},
{
name: "Priority: 1 (Normal)",
},
],
body: "body",
};
3 changes: 2 additions & 1 deletion tests/__mocks__/valid-configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
},
"assignedIssueScope": "org",
"emptyWalletText": "Please set your wallet address with the /wallet command first and try again.",
"rolesWithReviewAuthority": ["OWNER", "ADMIN", "MEMBER"]
"rolesWithReviewAuthority": ["OWNER", "ADMIN", "MEMBER"],
"requiredLabelsToStart": ["Priority: 1 (Normal)", "Priority: 2 (Medium)", "Priority: 3 (High)", "Priority: 4 (Urgent)", "Priority: 5 (Emergency)"]
}
8 changes: 7 additions & 1 deletion tests/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Value } from "@sinclair/typebox/value";
import { AssignedIssueScope, PluginSettings, pluginSettingsSchema } from "../src/types";
import cfg from "./__mocks__/valid-configuration.json";

const PRIORITY_LABELS = ["Priority: 1 (Normal)", "Priority: 2 (Medium)", "Priority: 3 (High)", "Priority: 4 (Urgent)", "Priority: 5 (Emergency)"];

describe("Configuration tests", () => {
it("Should decode the configuration", () => {
const settings = Value.Default(pluginSettingsSchema, {
Expand All @@ -12,18 +14,22 @@ describe("Configuration tests", () => {
emptyWalletText: "Please set your wallet address with the /wallet command first and try again.",
maxConcurrentTasks: { admin: 20, member: 10, contributor: 2 },
rolesWithReviewAuthority: ["OWNER", "ADMIN", "MEMBER"],
requiredLabelsToStart: PRIORITY_LABELS,
}) as PluginSettings;
expect(settings).toEqual(cfg);
});
it("Should default the admin to infinity if missing from config when decoded", () => {
const settings = Value.Default(pluginSettingsSchema, {}) as PluginSettings;
const settings = Value.Default(pluginSettingsSchema, {
requiredLabelsToStart: PRIORITY_LABELS,
}) as PluginSettings;
const decodedSettings = Value.Decode(pluginSettingsSchema, settings);
expect(decodedSettings.maxConcurrentTasks["admin"]).toEqual(Infinity);
});

it("Should normalize maxConcurrentTasks role keys to lowercase when decoded", () => {
const settings = Value.Default(pluginSettingsSchema, {
maxConcurrentTasks: { ADMIN: 20, memBER: 10, CONTRIBUTOR: 2 },
requiredLabelsToStart: PRIORITY_LABELS,
}) as PluginSettings;
const decodedSettings = Value.Decode(pluginSettingsSchema, settings);
expect(decodedSettings.maxConcurrentTasks).toEqual({ admin: 20, member: 10, contributor: 2 });
Expand Down
53 changes: 50 additions & 3 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type PayloadSender = Context["payload"]["sender"];

const octokit = jest.requireActual("@octokit/rest");
const TEST_REPO = "ubiquity/test-repo";
const PRIORITY_ONE = "Priority: 1 (Normal)";
const PRIORITY_LABELS = [PRIORITY_ONE, "Priority: 2 (Medium)", "Priority: 3 (High)", "Priority: 4 (Urgent)", "Priority: 5 (Emergency)"];

beforeAll(() => {
server.listen();
Expand Down Expand Up @@ -276,6 +278,23 @@ describe("User start/stop", () => {
expect(errorDetails).toContain("Invalid BOT_USER_ID");
}
});

test("Should not allow a user to start if no requiredLabelToStart exists", async () => {
const issue = db.issue.findFirst({ where: { id: { equals: 7 } } }) as unknown as Issue;
const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as PayloadSender;

const context = createContext(issue, sender, "/start", "1", false, [
"Priority: 3 (High)",
"Priority: 4 (Urgent)",
"Priority: 5 (Emergency)",
]) as Context<"issue_comment.created">;

context.adapters = createAdapters(getSupabase(), context);

await expect(userStartStop(context)).rejects.toMatchObject({
logMessage: { raw: "This task does not reflect a business priority at the moment and cannot be started. This will be reassessed in the coming weeks." },
});
});
});

async function setupTests() {
Expand Down Expand Up @@ -324,7 +343,11 @@ async function setupTests() {
node_id: "MDU6SXNzdWUy",
title: "Third issue",
number: 3,
labels: [],
labels: [
{
name: PRIORITY_ONE,
},
],
body: "Third issue body",
owner: "ubiquity",
});
Expand Down Expand Up @@ -361,6 +384,28 @@ async function setupTests() {
assignees: [],
});

db.issue.create({
...issueTemplate,
id: 7,
node_id: "MDU6SXNzdWUg",
title: "Seventh issue",
number: 7,
body: "Seventh issue body",
owner: "ubiquity",
assignees: [],
labels: [
{
name: "Price: 200 USD",
},
{
name: "Time: 1h",
},
{
name: PRIORITY_ONE,
},
],
});

db.pull.create({
id: 1,
html_url: "https://github.com/ubiquity/test-repo/pull/1",
Expand Down Expand Up @@ -595,7 +640,7 @@ function createIssuesForMaxAssignment(n: number, userId: number) {
for (let i = 0; i < n; i++) {
db.issue.create({
...issueTemplate,
id: i + 7,
id: i + 8,
assignee: user,
});
}
Expand All @@ -612,7 +657,8 @@ export function createContext(
sender: Record<string, unknown> | undefined,
body = "/start",
appId: string | null = "1",
startRequiresWallet = false
startRequiresWallet = false,
requiredLabelsToStart: string[] = PRIORITY_LABELS
): Context {
return {
adapters: {} as ReturnType<typeof createAdapters>,
Expand All @@ -634,6 +680,7 @@ export function createContext(
assignedIssueScope: AssignedIssueScope.ORG,
emptyWalletText: "Please set your wallet address with the /wallet command first and try again.",
rolesWithReviewAuthority: ["ADMIN", "OWNER", "MEMBER"],
requiredLabelsToStart,
},
octokit: new octokit.Octokit(),
eventName: "issue_comment.created" as SupportedEventsU,
Expand Down