diff --git a/manifest.json b/manifest.json index 4c36892..09b7cc3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Start | Stop", "description": "Assign or un-assign yourself from an issue.", - "ubiquity:listeners": ["issue_comment.created", "issues.assigned", "pull_request.opened"], + "ubiquity:listeners": ["issue_comment.created", "issues.assigned", "issues.unassigned", "pull_request.opened"], "commands": { "start": { "ubiquity:example": "/start", diff --git a/src/handlers/shared/check-assignments.ts b/src/handlers/shared/check-assignments.ts index f105a6b..872e64c 100644 --- a/src/handlers/shared/check-assignments.ts +++ b/src/handlers/shared/check-assignments.ts @@ -1,8 +1,8 @@ -import { Context, isContextCommentCreated } from "../../types"; +import { Context } from "../../types"; import { getOwnerRepoFromHtmlUrl } from "../../utils/issue"; async function getUserStopComments(context: Context, username: string): Promise { - if (!isContextCommentCreated(context)) { + if (!("issue" in context.payload)) { throw new Error("The context does not contain an issue."); } const { payload, octokit, logger } = context; @@ -55,7 +55,7 @@ export async function hasUserBeenUnassigned(context: Context, username: string): } async function getAssignmentEvents(context: Context) { - if (!isContextCommentCreated(context)) { + if (!("issue" in context.payload)) { throw new Error("The context does not contain an issue."); } const { repository, issue } = context.payload; diff --git a/src/handlers/shared/stop.ts b/src/handlers/shared/stop.ts index 8e15eac..af41cf6 100644 --- a/src/handlers/shared/stop.ts +++ b/src/handlers/shared/stop.ts @@ -1,5 +1,5 @@ import { Assignee, Context, Sender } from "../../types"; -import { addCommentToIssue, closePullRequestForAnIssue } from "../../utils/issue"; +import { closePullRequestForAnIssue } from "../../utils/issue"; import { HttpStatusCode, Result } from "../result-types"; export async function stop( diff --git a/src/handlers/user-start-stop.ts b/src/handlers/user-start-stop.ts index e221fa5..23d24b0 100644 --- a/src/handlers/user-start-stop.ts +++ b/src/handlers/user-start-stop.ts @@ -1,14 +1,14 @@ import { Repository } from "@octokit/graphql-schema"; -import { Context, isContextCommentCreated, Label } from "../types"; +import { Context, isIssueCommentEvent, Label } from "../types"; import { QUERY_CLOSING_ISSUE_REFERENCES } from "../utils/get-closing-issue-references"; -import { addCommentToIssue, getOwnerRepoFromHtmlUrl } from "../utils/issue"; +import { addCommentToIssue, closePullRequestForAnIssue, getOwnerRepoFromHtmlUrl } from "../utils/issue"; import { HttpStatusCode, Result } from "./result-types"; import { getDeadline } from "./shared/generate-assignment-comment"; import { start } from "./shared/start"; import { stop } from "./shared/stop"; export async function userStartStop(context: Context): Promise { - if (!isContextCommentCreated(context)) { + if (!isIssueCommentEvent(context)) { return { status: HttpStatusCode.NOT_MODIFIED }; } const { payload } = context; @@ -91,3 +91,14 @@ export async function userPullRequest(context: Context<"pull_request.opened">): } return { status: HttpStatusCode.NOT_MODIFIED }; } + +export async function userUnassigned(context: Context): Promise { + if (!("issue" in context.payload)) { + context.logger.debug("Payload does not contain an issue, skipping issues.unassigned event."); + return { status: HttpStatusCode.NOT_MODIFIED }; + } + const { payload } = context; + const { issue, sender, repository } = payload; + await closePullRequestForAnIssue(context, issue.number, repository, sender.login); + return { status: HttpStatusCode.OK, content: "Linked pull-requests closed." }; +} diff --git a/src/plugin.ts b/src/plugin.ts index 988e70c..f6e9d1b 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -3,7 +3,7 @@ import { Octokit } from "@octokit/rest"; import { createClient } from "@supabase/supabase-js"; import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; import { createAdapters } from "./adapters"; -import { userPullRequest, userSelfAssign, userStartStop } from "./handlers/user-start-stop"; +import { userPullRequest, userSelfAssign, userStartStop, userUnassigned } from "./handlers/user-start-stop"; import { Context, Env, PluginInputs } from "./types"; import { addCommentToIssue } from "./utils/issue"; @@ -32,6 +32,8 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { return await userSelfAssign(context as Context<"issues.assigned">); case "pull_request.opened": return await userPullRequest(context as Context<"pull_request.opened">); + case "issues.unassigned": + return await userUnassigned(context); default: context.logger.error(`Unsupported event: ${context.eventName}`); } diff --git a/src/types/context.ts b/src/types/context.ts index 178918e..829c990 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -6,13 +6,13 @@ import { createAdapters } from "../adapters"; import { Env } from "./env"; import { Logs } from "@ubiquity-dao/ubiquibot-logger"; -export type SupportedEventsU = "issue_comment.created" | "issues.assigned" | "pull_request.opened"; +export type SupportedEventsU = "issue_comment.created" | "issues.assigned" | "pull_request.opened" | "issues.unassigned"; export type SupportedEvents = { [K in SupportedEventsU]: K extends WebhookEventName ? WebhookEvent : never; }; -export function isContextCommentCreated(context: Context): context is Context<"issue_comment.created"> { +export function isIssueCommentEvent(context: Context): context is Context<"issue_comment.created"> { return "issue" in context.payload; } diff --git a/src/utils/issue.ts b/src/utils/issue.ts index fa9dbd9..2dc2529 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,5 +1,5 @@ import ms from "ms"; -import { Context, isContextCommentCreated } from "../types/context"; +import { Context } from "../types/context"; import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; @@ -35,7 +35,7 @@ export async function addCommentToIssue(context: Context, message: string | null return; } - if (!isContextCommentCreated(context)) { + if (!("issue" in context.payload)) { context.logger.error("Cannot post without a referenced issue."); return; } @@ -120,7 +120,6 @@ export async function closePullRequestForAnIssue(context: Context, issueNumber: return logger.info(`No PRs were closed`); } - await addCommentToIssue(context, comment); return logger.info(comment); } diff --git a/tests/main.test.ts b/tests/main.test.ts index 3f349df..4121eea 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -4,7 +4,7 @@ import { db } from "./__mocks__/db"; import { server } from "./__mocks__/node"; import usersGet from "./__mocks__/users-get.json"; import { expect, describe, beforeAll, beforeEach, afterAll, afterEach } from "@jest/globals"; -import { userStartStop } from "../src/handlers/user-start-stop"; +import { userStartStop, userUnassigned } from "../src/handlers/user-start-stop"; import issueTemplate from "./__mocks__/issue-template"; import { createAdapters } from "../src/adapters"; import { createClient } from "@supabase/supabase-js"; @@ -112,6 +112,26 @@ describe("User start/stop", () => { ); }); + test("Author's manual unassign should close linked issue", async () => { + const infoSpy = jest.spyOn(console, "info").mockImplementation(() => {}); + const issue = db.issue.findFirst({ where: { id: { equals: 2 } } }) as unknown as Issue; + const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as PayloadSender; + const context = createContext(issue, sender, "") as Context<"issues.unassigned">; + + context.adapters = createAdapters(getSupabase(), context); + + const { content } = await userUnassigned(context); + + expect(content).toEqual("Linked pull-requests closed."); + const logs = infoSpy.mock.calls.flat(); + expect(logs[0]).toMatch(/Opened prs/); + expect(cleanLogString(logs[3])).toMatch( + cleanLogString( + " › ```diff# These linked pull requests are closed: http://github.com/ubiquity/test-repo/pull/2 http://github.com/ubiquity/test-repo/pull/3" + ) + ); + }); + test("User can't stop an issue they're not assigned to", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 2 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as PayloadSender;