Skip to content

Commit

Permalink
Merge pull request #80 from ubq-testing/feat/requiredStartLabels
Browse files Browse the repository at this point in the history
Feat/required start labels
  • Loading branch information
rndquu authored Nov 8, 2024
2 parents d9642c0 + 56232c8 commit 844cac7
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 9 deletions.
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

0 comments on commit 844cac7

Please sign in to comment.