Skip to content

Commit f395f3d

Browse files
committed
feat(github-adapter): issue-comment wrappers (create/update/delete/list)
1 parent 2207954 commit f395f3d

3 files changed

Lines changed: 266 additions & 0 deletions

File tree

packages/integrations/github/src/client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export {
2121
updateIssueState,
2222
} from "./issue-writes"
2323
export type { IssueStateUpdate } from "./issue-writes"
24+
export {
25+
createIssueComment,
26+
updateIssueComment,
27+
deleteIssueComment,
28+
listIssueComments,
29+
} from "./comments"
30+
export type { GithubComment } from "./comments"
2431

2532
export function createInstallationClient(
2633
opts: InstallationClientOptions,
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// packages/integrations/github/src/comments.test.ts
2+
import { describe, test, expect, mock } from "bun:test"
3+
import {
4+
createIssueComment,
5+
updateIssueComment,
6+
deleteIssueComment,
7+
listIssueComments,
8+
} from "./comments"
9+
import type { Octokit } from "@octokit/rest"
10+
11+
function makeComment(id: number, body: string) {
12+
return {
13+
id,
14+
body,
15+
user: { id: 42, login: "test-user", avatar_url: "https://example.com/avatar.png" },
16+
created_at: "2026-01-01T00:00:00Z",
17+
updated_at: "2026-01-02T00:00:00Z",
18+
}
19+
}
20+
21+
describe("createIssueComment", () => {
22+
test("returns normalized shape from API response", async () => {
23+
const createComment = mock(async () => ({ data: makeComment(100, "Hello!") }))
24+
const client = {
25+
rest: { issues: { createComment } },
26+
} as unknown as Octokit
27+
28+
const result = await createIssueComment(client, "owner", "repo", 5, "Hello!")
29+
30+
expect(createComment).toHaveBeenCalledWith({
31+
owner: "owner",
32+
repo: "repo",
33+
issue_number: 5,
34+
body: "Hello!",
35+
})
36+
expect(result).toEqual({
37+
id: 100,
38+
body: "Hello!",
39+
user: { id: 42, login: "test-user", avatar_url: "https://example.com/avatar.png" },
40+
createdAt: "2026-01-01T00:00:00Z",
41+
updatedAt: "2026-01-02T00:00:00Z",
42+
})
43+
})
44+
45+
test("handles null user gracefully", async () => {
46+
const createComment = mock(async () => ({
47+
data: {
48+
id: 200,
49+
body: "test",
50+
user: null,
51+
created_at: "2026-01-01T00:00:00Z",
52+
updated_at: "2026-01-01T00:00:00Z",
53+
},
54+
}))
55+
const client = {
56+
rest: { issues: { createComment } },
57+
} as unknown as Octokit
58+
59+
const result = await createIssueComment(client, "owner", "repo", 1, "test")
60+
expect(result.user).toEqual({ id: 0, login: "", avatar_url: null })
61+
})
62+
})
63+
64+
describe("updateIssueComment", () => {
65+
test("calls updateComment with comment_id + body", async () => {
66+
const updateComment = mock(async () => ({ data: {} }))
67+
const client = {
68+
rest: { issues: { updateComment } },
69+
} as unknown as Octokit
70+
71+
await updateIssueComment(client, "owner", "repo", 999, "New body")
72+
73+
expect(updateComment).toHaveBeenCalledWith({
74+
owner: "owner",
75+
repo: "repo",
76+
comment_id: 999,
77+
body: "New body",
78+
})
79+
})
80+
})
81+
82+
describe("deleteIssueComment", () => {
83+
test("calls deleteComment with comment_id", async () => {
84+
const deleteComment = mock(async () => ({ data: {} }))
85+
const client = {
86+
rest: { issues: { deleteComment } },
87+
} as unknown as Octokit
88+
89+
await deleteIssueComment(client, "owner", "repo", 888)
90+
91+
expect(deleteComment).toHaveBeenCalledWith({
92+
owner: "owner",
93+
repo: "repo",
94+
comment_id: 888,
95+
})
96+
})
97+
})
98+
99+
describe("listIssueComments", () => {
100+
test("flattens paginated results", async () => {
101+
const page1 = [makeComment(1, "First"), makeComment(2, "Second")]
102+
const page2 = [makeComment(3, "Third")]
103+
104+
// Build an async iterator that yields two pages
105+
const pages = [{ data: page1 }, { data: page2 }]
106+
let pageIdx = 0
107+
const asyncIterator = {
108+
[Symbol.asyncIterator]() {
109+
return {
110+
async next() {
111+
if (pageIdx < pages.length) {
112+
return { value: pages[pageIdx++], done: false }
113+
}
114+
return { value: undefined, done: true }
115+
},
116+
}
117+
},
118+
}
119+
120+
const paginateIterator = mock(() => asyncIterator)
121+
const listComments = mock(async () => ({ data: [] }))
122+
const client = {
123+
rest: { issues: { listComments } },
124+
paginate: { iterator: paginateIterator },
125+
} as unknown as Octokit
126+
127+
const results = await listIssueComments(client, "owner", "repo", 7)
128+
129+
expect(results).toHaveLength(3)
130+
expect(results[0].id).toBe(1)
131+
expect(results[1].id).toBe(2)
132+
expect(results[2].id).toBe(3)
133+
expect(paginateIterator).toHaveBeenCalledWith(listComments, {
134+
owner: "owner",
135+
repo: "repo",
136+
issue_number: 7,
137+
per_page: 100,
138+
})
139+
})
140+
141+
test("returns empty array when no comments", async () => {
142+
const asyncIterator = {
143+
[Symbol.asyncIterator]() {
144+
return {
145+
async next() {
146+
return { value: undefined, done: true }
147+
},
148+
}
149+
},
150+
}
151+
152+
const paginateIterator = mock(() => asyncIterator)
153+
const listComments = mock(async () => ({ data: [] }))
154+
const client = {
155+
rest: { issues: { listComments } },
156+
paginate: { iterator: paginateIterator },
157+
} as unknown as Octokit
158+
159+
const results = await listIssueComments(client, "owner", "repo", 1)
160+
expect(results).toHaveLength(0)
161+
})
162+
})
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// packages/integrations/github/src/comments.ts
2+
// Octokit wrappers for GitHub issue comments.
3+
import type { Octokit } from "@octokit/rest"
4+
5+
export type GithubComment = {
6+
id: number
7+
body: string
8+
user: { id: number; login: string; avatar_url: string | null }
9+
createdAt: string
10+
updatedAt: string
11+
}
12+
13+
export async function createIssueComment(
14+
client: Octokit,
15+
owner: string,
16+
repo: string,
17+
issueNumber: number,
18+
body: string,
19+
): Promise<GithubComment> {
20+
const res = await client.rest.issues.createComment({
21+
owner,
22+
repo,
23+
issue_number: issueNumber,
24+
body,
25+
})
26+
const c = res.data
27+
return {
28+
id: c.id,
29+
body: c.body ?? "",
30+
user: {
31+
id: c.user?.id ?? 0,
32+
login: c.user?.login ?? "",
33+
avatar_url: c.user?.avatar_url ?? null,
34+
},
35+
createdAt: c.created_at,
36+
updatedAt: c.updated_at,
37+
}
38+
}
39+
40+
export async function updateIssueComment(
41+
client: Octokit,
42+
owner: string,
43+
repo: string,
44+
commentId: number,
45+
body: string,
46+
): Promise<void> {
47+
await client.rest.issues.updateComment({
48+
owner,
49+
repo,
50+
comment_id: commentId,
51+
body,
52+
})
53+
}
54+
55+
export async function deleteIssueComment(
56+
client: Octokit,
57+
owner: string,
58+
repo: string,
59+
commentId: number,
60+
): Promise<void> {
61+
await client.rest.issues.deleteComment({
62+
owner,
63+
repo,
64+
comment_id: commentId,
65+
})
66+
}
67+
68+
export async function listIssueComments(
69+
client: Octokit,
70+
owner: string,
71+
repo: string,
72+
issueNumber: number,
73+
): Promise<GithubComment[]> {
74+
const items: GithubComment[] = []
75+
const iterator = client.paginate.iterator(client.rest.issues.listComments, {
76+
owner,
77+
repo,
78+
issue_number: issueNumber,
79+
per_page: 100,
80+
})
81+
for await (const { data } of iterator) {
82+
for (const c of data) {
83+
items.push({
84+
id: c.id,
85+
body: c.body ?? "",
86+
user: {
87+
id: c.user?.id ?? 0,
88+
login: c.user?.login ?? "",
89+
avatar_url: c.user?.avatar_url ?? null,
90+
},
91+
createdAt: c.created_at,
92+
updatedAt: c.updated_at,
93+
})
94+
}
95+
}
96+
return items
97+
}

0 commit comments

Comments
 (0)