Skip to content

Commit 3f469c6

Browse files
feat: delete bot comment if target comment is deleted (#55)
<!-- 👋 Hi, thanks for sending a PR to octoguide! 🗺️ Please fill out all fields below and make sure each item is true and [x] checked. Otherwise we may not be able to review your PR. --> ## PR Checklist - [x] Addresses an existing open issue: fixes #42 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/octoguide/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/octoguide/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Adds a `runCommentCleanup` function that's called in `runOctoGuideAction` if the payload type is deleted. If the payload has a comment, then the corresponding existing comment is retrieved and deleted. Bot comment deletion notably does not go through the `EntityActor` interface added in #43. Because the "entity" is deleted and not available anymore, many of the methods there don't make sense. Switches a few ancillary functions to take url strings instead of entities, since `runCommentCleanup` doesn't run with a full entity. Also renames the `octoguide` function to `runOctoGuideRules`. That's more precisely what it does, and this disambiguates it from `runCommentCleanup`. 🗺️
1 parent 64f4b13 commit 3f469c6

15 files changed

+10320
-10026
lines changed

dist/index.js

Lines changed: 9913 additions & 9848 deletions
Large diffs are not rendered by default.

src/action/comments/createCommentBody.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export function createCommentBody(entity: Entity, message: string): string {
66
return [
77
message,
88
`> 🗺️ _This message was posted automatically by [OctoGuide](https://github.com/JoshuaKGoldberg/OctoGuide): a bot for GitHub repository best practices._`,
9-
createCommentIdentifier(entity),
9+
createCommentIdentifier(entity.data.html_url),
1010
].join("\n\n");
1111
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { Entity } from "../../types/entities.js";
2-
3-
export function createCommentIdentifier(entity: Entity) {
4-
return `<!-- OctoGuide response for: ${entity.data.html_url} -->`;
1+
export function createCommentIdentifier(url: string) {
2+
return `<!-- OctoGuide response for: ${url} -->`;
53
}

src/action/comments/getExistingComment.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import type { EntityActor } from "../../actors/types.js";
2-
import type { Entity } from "../../types/entities.js";
32

43
import { createCommentIdentifier } from "./createCommentIdentifier.js";
54

6-
export async function getExistingComment(actor: EntityActor, entity: Entity) {
7-
const commentIdentifier = createCommentIdentifier(entity);
5+
export async function getExistingComment(actor: EntityActor, url: string) {
6+
const commentIdentifier = createCommentIdentifier(url);
87
const comments = await actor.listComments();
98

109
return comments.find((comment) => comment.body?.endsWith(commentIdentifier));

src/action/comments/setCommentForReports.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { RuleReport } from "../../types/rules.js";
66
import { EntityActor } from "../../actors/types.js";
77
import { createNewCommentForReports } from "./createNewCommentForReports.js";
88
import { getExistingComment } from "./getExistingComment.js";
9-
import { updateExistingCommentAsPassed } from "./updateExistingCommentAsPassed.js";
109
import { updateExistingCommentForReports } from "./updateExistingCommentForReports.js";
1110

1211
export interface ReportComment {
@@ -19,7 +18,7 @@ export async function getCommentForReports(
1918
entity: Entity,
2019
reports: RuleReport[],
2120
): Promise<ReportComment | undefined> {
22-
const existingComment = await getExistingComment(actor, entity);
21+
const existingComment = await getExistingComment(actor, entity.data.html_url);
2322

2423
core.info(
2524
existingComment
@@ -30,7 +29,12 @@ export async function getCommentForReports(
3029
if (!reports.length) {
3130
if (existingComment) {
3231
core.info("Updating existing comment as passed.");
33-
await updateExistingCommentAsPassed(actor, entity, existingComment);
32+
await updateExistingCommentForReports(
33+
actor,
34+
entity,
35+
existingComment,
36+
reports,
37+
);
3438
}
3539
return (
3640
existingComment && { status: "existing", url: existingComment.html_url }

src/action/comments/updateExistingCommentAsPassed.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/action/runCommentCleanup.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type * as github from "@actions/github";
2+
3+
import { describe, expect, it, vi } from "vitest";
4+
5+
import { runCommentCleanup } from "./runCommentCleanup";
6+
7+
const mockCore = {
8+
info: vi.fn(),
9+
};
10+
11+
vi.mock("@actions/core", () => ({
12+
get info() {
13+
return mockCore.info;
14+
},
15+
}));
16+
17+
const mockOctokitFromAuth = vi.fn();
18+
19+
vi.mock("octokit-from-auth", () => ({
20+
get octokitFromAuth() {
21+
return mockOctokitFromAuth;
22+
},
23+
}));
24+
25+
const mockCreateActor = vi.fn();
26+
27+
vi.mock("../actors/createActor.js", () => ({
28+
get createActor() {
29+
return mockCreateActor;
30+
},
31+
}));
32+
33+
const mockGetExistingComment = vi.fn();
34+
35+
vi.mock("./comments/getExistingComment.js", () => ({
36+
get getExistingComment() {
37+
return mockGetExistingComment;
38+
},
39+
}));
40+
41+
const auth = "gho_...";
42+
const locator = { owner: "owner", repository: "repo" };
43+
const url = "/repos/owner/repo/issues/1";
44+
45+
describe(runCommentCleanup, () => {
46+
it("does nothing when the payload does not contain a comment", async () => {
47+
const payload = {
48+
issue: {},
49+
} as typeof github.context.payload;
50+
51+
await runCommentCleanup({ auth, payload, url });
52+
53+
expect(mockOctokitFromAuth).not.toHaveBeenCalled();
54+
});
55+
56+
it("throws an error when the actor cannot be resolved", async () => {
57+
const payload = {
58+
comment: {},
59+
} as typeof github.context.payload;
60+
61+
mockCreateActor.mockReturnValueOnce({});
62+
63+
await expect(runCommentCleanup({ auth, payload, url })).rejects.toThrow(
64+
"Could not resolve GitHub entity actor.",
65+
);
66+
});
67+
68+
it("logs info without throwing when an existing comment cannot be found", async () => {
69+
const payload = {
70+
comment: {},
71+
} as typeof github.context.payload;
72+
73+
mockCreateActor.mockReturnValueOnce({ actor: {}, locator });
74+
mockGetExistingComment.mockResolvedValueOnce(undefined);
75+
76+
await runCommentCleanup({ auth, payload, url });
77+
expect(mockCore.info).toHaveBeenCalledWith(
78+
"No existing comment found. Nothing to clean up.",
79+
);
80+
});
81+
82+
it("deletes the comment when it exists as a discussion comment", async () => {
83+
const payload = {
84+
comment: { id: 123 },
85+
discussion: {},
86+
} as typeof github.context.payload;
87+
const nodeId = "abc123";
88+
89+
mockOctokitFromAuth.mockResolvedValueOnce({
90+
graphql: vi.fn(),
91+
});
92+
mockCreateActor.mockReturnValueOnce({ actor: {}, locator });
93+
mockGetExistingComment.mockResolvedValueOnce({
94+
node_id: nodeId,
95+
});
96+
97+
await runCommentCleanup({ auth, payload, url });
98+
expect(mockCore.info).toHaveBeenCalledWith(
99+
`Deleting discussion comment with node id: ${nodeId}`,
100+
);
101+
});
102+
103+
it("deletes the comment when it exists as an issue-like comment", async () => {
104+
const id = 123;
105+
const payload = {
106+
comment: { id },
107+
issue: {},
108+
} as typeof github.context.payload;
109+
110+
mockOctokitFromAuth.mockResolvedValueOnce({
111+
rest: {
112+
issues: {
113+
deleteComment: vi.fn(),
114+
},
115+
},
116+
});
117+
mockCreateActor.mockReturnValueOnce({ actor: {}, locator });
118+
mockGetExistingComment.mockResolvedValueOnce({ id });
119+
120+
await runCommentCleanup({ auth, payload, url });
121+
expect(mockCore.info).toHaveBeenCalledWith(
122+
`Deleting issue-like comment with id: ${id.toString()}`,
123+
);
124+
});
125+
});

src/action/runCommentCleanup.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type * as github from "@actions/github";
2+
3+
import * as core from "@actions/core";
4+
import { octokitFromAuth } from "octokit-from-auth";
5+
6+
import { createActor } from "../actors/createActor.js";
7+
import { getExistingComment } from "./comments/getExistingComment.js";
8+
9+
export interface RunCommentCleanupSettings {
10+
auth: string;
11+
payload: typeof github.context.payload;
12+
url: string;
13+
}
14+
15+
export async function runCommentCleanup({
16+
auth,
17+
payload,
18+
url,
19+
}: RunCommentCleanupSettings) {
20+
if (!payload.comment) {
21+
return;
22+
}
23+
24+
const octokit = await octokitFromAuth({ auth });
25+
const { actor, locator } = createActor(octokit, url);
26+
if (!actor) {
27+
throw new Error("Could not resolve GitHub entity actor.");
28+
}
29+
30+
const existingComment = await getExistingComment(actor, url);
31+
if (!existingComment) {
32+
core.info("No existing comment found. Nothing to clean up.");
33+
return;
34+
}
35+
36+
if (payload.discussion) {
37+
core.info(
38+
`Deleting discussion comment with node id: ${existingComment.node_id}`,
39+
);
40+
await octokit.graphql(
41+
`
42+
mutation($body: String!, $commentId: ID!) {
43+
deleteDiscussionComment(input: {
44+
body: $body,
45+
commentId: $commentId
46+
}) {
47+
comment {
48+
id
49+
}
50+
}
51+
}
52+
`,
53+
{
54+
commentId: existingComment.node_id,
55+
},
56+
);
57+
} else {
58+
core.info(
59+
`Deleting issue-like comment with id: ${existingComment.id.toString()}`,
60+
);
61+
await octokit.rest.issues.deleteComment({
62+
comment_id: existingComment.id,
63+
owner: locator.owner,
64+
repo: locator.repository,
65+
});
66+
}
67+
}

src/action/runOctoGuideAction.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import type * as github from "@actions/github";
22

33
import * as core from "@actions/core";
44

5-
import { runOctoGuide } from "../index.js";
5+
import { runOctoGuideRules } from "../index.js";
66
import { cliReporter } from "../reporters/cliReporter.js";
77
import { isKnownConfig } from "../rules/configs.js";
88
import { EntityData } from "../types/entities.js";
99
import { getCommentForReports } from "./comments/setCommentForReports.js";
10+
import { runCommentCleanup } from "./runCommentCleanup.js";
1011

1112
export async function runOctoGuideAction(context: typeof github.context) {
1213
const { payload } = context;
14+
if (!payload.action) {
15+
core.info("Unknown payload action. Exiting.");
16+
return;
17+
}
1318

1419
core.debug(`Full target payload: ${JSON.stringify(payload, null, 2)}`);
1520

@@ -21,21 +26,32 @@ export async function runOctoGuideAction(context: typeof github.context) {
2126
throw new Error("Could not determine an entity to run OctoGuide on.");
2227
}
2328

24-
if (typeof target.html_url !== "string") {
29+
const url = target.html_url;
30+
if (typeof url !== "string") {
2531
throw new Error("Target entity's html_url is not a string.");
2632
}
2733

28-
core.info(`Targeting entity at html_url: ${target.html_url}`);
34+
const auth = core.getInput("github-token");
35+
if (!auth) {
36+
throw new Error("Please provide a with.github-token to octoguide.");
37+
}
38+
39+
core.info(`Targeting ${payload.action} entity at html_url: ${url}`);
40+
41+
if (payload.action === "deleted") {
42+
await runCommentCleanup({ auth, payload, url });
43+
return;
44+
}
2945

3046
const config = core.getInput("config") || "recommended";
3147
if (!isKnownConfig(config)) {
3248
throw new Error(`Unknown config provided: ${config}`);
3349
}
3450

35-
const { actor, entity, reports } = await runOctoGuide({
51+
const { actor, entity, reports } = await runOctoGuideRules({
52+
auth,
3653
config,
37-
githubToken: core.getInput("github-token"),
38-
url: target.html_url,
54+
entity: url,
3955
});
4056

4157
core.debug(`Full entity: ${JSON.stringify(entity, null, 2)}`);

0 commit comments

Comments
 (0)