Skip to content

Commit b7ff7b2

Browse files
authored
ci: enhance issue creation by grouping TODOs (#39290)
1 parent d9bceb4 commit b7ff7b2

2 files changed

Lines changed: 60 additions & 17 deletions

File tree

scripts/todo-issue/src/github.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ function buildIssueBody(todo: TodoItem, owner: string, repo: string, sha: string
176176
return lines.join('\n');
177177
}
178178

179+
function buildIssueBodyFromGroup(todos: TodoItem[], owner: string, repo: string, sha: string): string {
180+
const bodies = [...new Set(todos.map((t) => t.body).filter(Boolean))] as string[];
181+
const locationLines = todos.map((todo) => {
182+
const blobUrl = `https://github.com/${owner}/${repo}/blob/${sha}/${encodeURI(todo.filename)}#L${todo.line}-L${todo.line + BLOB_LINES}`;
183+
return `- [\`${todo.filename}#L${todo.line}\`](${blobUrl})`;
184+
});
185+
const lines = [
186+
bodies.join('\n\n') || '',
187+
'',
188+
`<!-- todo-issue -->`,
189+
`📝 Found in ${todos.length} location(s):`,
190+
'',
191+
...locationLines,
192+
'',
193+
`Commit: ${sha}`,
194+
];
195+
return lines.join('\n');
196+
}
197+
179198
async function ensureLabelExists(owner: string, repo: string, label: string, token: string): Promise<void> {
180199
try {
181200
await githubRequest(`/repos/${owner}/${repo}/labels`, token, 'POST', {
@@ -187,14 +206,25 @@ async function ensureLabelExists(owner: string, repo: string, label: string, tok
187206
}
188207
}
189208

190-
export async function createIssue(todo: TodoItem, config: Config, sha: string): Promise<void> {
209+
function mergeTodoGroup(todos: TodoItem[]): TodoItem {
210+
const first = todos[0];
211+
const labels = [...new Set(todos.flatMap((t) => t.labels))];
212+
const assignees = [...new Set(todos.flatMap((t) => t.assignees))];
213+
return { ...first, labels, assignees };
214+
}
215+
216+
export async function createIssue(todoOrGroup: TodoItem | TodoItem[], config: Config, sha: string): Promise<void> {
217+
const group = Array.isArray(todoOrGroup) ? todoOrGroup : [todoOrGroup];
218+
const todo = mergeTodoGroup(group);
219+
191220
for (const label of todo.labels) {
192221
await ensureLabelExists(config.owner, config.repo, label, config.token);
193222
}
194223

195-
const body = buildIssueBody(todo, config.owner, config.repo, sha);
224+
const body = group.length > 1 ? buildIssueBodyFromGroup(group, config.owner, config.repo, sha) : buildIssueBody(todo, config.owner, config.repo, sha);
196225

197-
console.log(`[CREATE] "${todo.title}" (${todo.filename}#L${todo.line})`);
226+
const locations = group.map((t) => `${t.filename}#L${t.line}`).join(', ');
227+
console.log(`[CREATE] "${todo.title}" (${locations})`);
198228

199229
const payload: Record<string, unknown> = {
200230
title: todo.title,

scripts/todo-issue/src/index.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bun
22

3-
import type { Config } from './types';
3+
import type { Config, TodoItem } from './types';
44
import { extractTodos } from './diff';
55
import { matchTodos } from './matcher';
66
import { isSimilar } from './similarity';
@@ -72,10 +72,7 @@ async function getDiff(config: Config): Promise<{ diffText: string; resolvedHead
7272

7373
console.log(`[INFO] Push mode: comparing ${config.baseSha.slice(0, 7)}...${config.headSha.slice(0, 7)}`);
7474
return {
75-
diffText: await getDiffFromApi(
76-
`/repos/${config.owner}/${config.repo}/compare/${config.baseSha}...${config.headSha}`,
77-
config.token,
78-
),
75+
diffText: await getDiffFromApi(`/repos/${config.owner}/${config.repo}/compare/${config.baseSha}...${config.headSha}`, config.token),
7976
resolvedHeadSha: config.headSha,
8077
};
8178
}
@@ -101,24 +98,40 @@ async function run(): Promise<void> {
10198
const existingIssues = await fetchExistingIssues(config);
10299

103100
if (config.importAll || config.pathFilter) {
104-
const toCreate = todos.filter((t) => t.type === 'add').filter((todo) => {
105-
return !existingIssues.find((i) => i.title === todo.title || isSimilar(i.title, todo.title));
106-
});
101+
const added = todos
102+
.filter((t) => t.type === 'add')
103+
.filter((todo) => {
104+
return !existingIssues.find((i) => i.title === todo.title || isSimilar(i.title, todo.title));
105+
});
106+
107+
const byTitle = new Map<string, TodoItem[]>();
108+
for (const todo of added) {
109+
const list = byTitle.get(todo.title) ?? [];
110+
list.push(todo);
111+
byTitle.set(todo.title, list);
112+
}
107113

108-
console.log(`[INFO] Import: ${toCreate.length} new issues to create`);
114+
console.log(`[INFO] Import: ${byTitle.size} new issues to create (${added.length} TODOs grouped by title)`);
109115

110-
for (const todo of toCreate) {
111-
await createIssue(todo, config, resolvedHeadSha);
116+
for (const group of byTitle.values()) {
117+
await createIssue(group, config, resolvedHeadSha);
112118
}
113119
} else {
114120
const { toCreate, toClose, toUpdate, toReference } = matchTodos(todos, existingIssues);
115121

122+
const createByTitle = new Map<string, TodoItem[]>();
123+
for (const todo of toCreate) {
124+
const list = createByTitle.get(todo.title) ?? [];
125+
list.push(todo);
126+
createByTitle.set(todo.title, list);
127+
}
128+
116129
console.log(
117-
`[INFO] Actions: ${toCreate.length} create, ${toClose.length} close, ${toUpdate.length} update, ${toReference.length} reference`,
130+
`[INFO] Actions: ${createByTitle.size} create, ${toClose.length} close, ${toUpdate.length} update, ${toReference.length} reference`,
118131
);
119132

120-
for (const todo of toCreate) {
121-
await createIssue(todo, config, resolvedHeadSha);
133+
for (const group of createByTitle.values()) {
134+
await createIssue(group, config, resolvedHeadSha);
122135
}
123136

124137
for (const todo of toClose) {

0 commit comments

Comments
 (0)