github-issue-tower-defence-management 1.32.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +92 -6
- package/bin/adapter/entry-points/cli/index.js +422 -5
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +67 -33
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/FetchWebhookRepository.js +10 -0
- package/bin/adapter/repositories/FetchWebhookRepository.js.map +1 -0
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js +190 -0
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js.map +1 -0
- package/bin/adapter/repositories/OauthAPIClaudeRepository.js +225 -0
- package/bin/adapter/repositories/OauthAPIClaudeRepository.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +17 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +73 -17
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +1315 -15
- package/src/adapter/entry-points/cli/index.ts +648 -5
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +14 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +17 -2
- package/src/adapter/repositories/FetchWebhookRepository.ts +7 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.ts +291 -0
- package/src/adapter/repositories/OauthAPIClaudeRepository.ts +279 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +28 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +30 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +722 -16
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +117 -20
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -0
- package/src/domain/usecases/adapter-interfaces/WebhookRepository.ts +3 -0
- package/types/adapter/entry-points/cli/index.d.ts +19 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/FetchWebhookRepository.d.ts +5 -0
- package/types/adapter/repositories/FetchWebhookRepository.d.ts.map +1 -0
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts +12 -0
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts.map +1 -0
- package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts +13 -0
- package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +10 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts.map +1 -0
|
@@ -1,19 +1,318 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import YAML from 'yaml';
|
|
2
3
|
import { Command } from 'commander';
|
|
4
|
+
import * as fs from 'fs';
|
|
3
5
|
import { HandleScheduledEventUseCaseHandler } from '../handlers/HandleScheduledEventUseCaseHandler';
|
|
6
|
+
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
7
|
+
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
8
|
+
import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
|
|
9
|
+
import { GraphqlProjectRepository } from '../../repositories/GraphqlProjectRepository';
|
|
10
|
+
import { ApiV3IssueRepository } from '../../repositories/issue/ApiV3IssueRepository';
|
|
11
|
+
import { RestIssueRepository } from '../../repositories/issue/RestIssueRepository';
|
|
12
|
+
import { GraphqlProjectItemRepository } from '../../repositories/issue/GraphqlProjectItemRepository';
|
|
13
|
+
import { ApiV3CheerioRestIssueRepository } from '../../repositories/issue/ApiV3CheerioRestIssueRepository';
|
|
14
|
+
import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
|
|
15
|
+
import { CheerioProjectRepository } from '../../repositories/CheerioProjectRepository';
|
|
16
|
+
import { BaseGitHubRepository } from '../../repositories/BaseGitHubRepository';
|
|
17
|
+
import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
|
|
18
|
+
import { OauthAPIClaudeRepository } from '../../repositories/OauthAPIClaudeRepository';
|
|
19
|
+
import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
|
|
20
|
+
import { FetchWebhookRepository } from '../../repositories/FetchWebhookRepository';
|
|
21
|
+
import { Project } from '../../../domain/entities/Project';
|
|
4
22
|
|
|
5
|
-
|
|
23
|
+
type ConfigFile = {
|
|
24
|
+
projectUrl?: string;
|
|
25
|
+
awaitingWorkspaceStatus?: string;
|
|
26
|
+
preparationStatus?: string;
|
|
27
|
+
defaultAgentName?: string;
|
|
28
|
+
logFilePath?: string;
|
|
29
|
+
maximumPreparingIssuesCount?: number;
|
|
30
|
+
allowIssueCacheMinutes?: number;
|
|
31
|
+
awaitingQualityCheckStatus?: string;
|
|
32
|
+
thresholdForAutoReject?: number;
|
|
33
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
34
|
+
projectName?: string;
|
|
35
|
+
};
|
|
6
36
|
|
|
7
|
-
|
|
37
|
+
type StartDaemonOptions = {
|
|
38
|
+
projectUrl?: string;
|
|
39
|
+
awaitingWorkspaceStatus?: string;
|
|
40
|
+
preparationStatus?: string;
|
|
41
|
+
defaultAgentName?: string;
|
|
42
|
+
logFilePath?: string;
|
|
43
|
+
maximumPreparingIssuesCount?: string;
|
|
44
|
+
allowIssueCacheMinutes?: string;
|
|
45
|
+
configFilePath: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type NotifyFinishedOptions = {
|
|
49
|
+
issueUrl: string;
|
|
50
|
+
projectUrl?: string;
|
|
51
|
+
preparationStatus?: string;
|
|
52
|
+
awaitingWorkspaceStatus?: string;
|
|
53
|
+
awaitingQualityCheckStatus?: string;
|
|
54
|
+
thresholdForAutoReject?: string;
|
|
55
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
56
|
+
configFilePath: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getStringValue = (
|
|
60
|
+
obj: Record<string, unknown>,
|
|
61
|
+
key: string,
|
|
62
|
+
): string | undefined => {
|
|
63
|
+
const value = obj[key];
|
|
64
|
+
return typeof value === 'string' ? value : undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getNumberValue = (
|
|
68
|
+
obj: Record<string, unknown>,
|
|
69
|
+
key: string,
|
|
70
|
+
): number | undefined => {
|
|
71
|
+
const value = obj[key];
|
|
72
|
+
return typeof value === 'number' ? value : undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
76
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
77
|
+
|
|
78
|
+
export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(configFilePath, 'utf-8');
|
|
81
|
+
const parsed: unknown = YAML.parse(content);
|
|
82
|
+
if (!isRecord(parsed)) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
projectUrl: getStringValue(parsed, 'projectUrl'),
|
|
87
|
+
awaitingWorkspaceStatus: getStringValue(
|
|
88
|
+
parsed,
|
|
89
|
+
'awaitingWorkspaceStatus',
|
|
90
|
+
),
|
|
91
|
+
preparationStatus: getStringValue(parsed, 'preparationStatus'),
|
|
92
|
+
defaultAgentName: getStringValue(parsed, 'defaultAgentName'),
|
|
93
|
+
logFilePath: getStringValue(parsed, 'logFilePath'),
|
|
94
|
+
maximumPreparingIssuesCount: getNumberValue(
|
|
95
|
+
parsed,
|
|
96
|
+
'maximumPreparingIssuesCount',
|
|
97
|
+
),
|
|
98
|
+
allowIssueCacheMinutes: getNumberValue(parsed, 'allowIssueCacheMinutes'),
|
|
99
|
+
awaitingQualityCheckStatus: getStringValue(
|
|
100
|
+
parsed,
|
|
101
|
+
'awaitingQualityCheckStatus',
|
|
102
|
+
),
|
|
103
|
+
thresholdForAutoReject: getNumberValue(parsed, 'thresholdForAutoReject'),
|
|
104
|
+
workflowBlockerResolvedWebhookUrl: getStringValue(
|
|
105
|
+
parsed,
|
|
106
|
+
'workflowBlockerResolvedWebhookUrl',
|
|
107
|
+
),
|
|
108
|
+
projectName: getStringValue(parsed, 'projectName'),
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
console.error(
|
|
113
|
+
`Failed to load configuration file "${configFilePath}": ${message}`,
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const parseProjectReadmeConfig = (readme: string): ConfigFile => {
|
|
120
|
+
const detailsRegex =
|
|
121
|
+
/<details>\s*<summary>config<\/summary>([\s\S]*?)<\/details>/i;
|
|
122
|
+
const match = detailsRegex.exec(readme);
|
|
123
|
+
if (!match) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
const yamlContent = match[1].trim();
|
|
127
|
+
if (!yamlContent) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const parsed: unknown = YAML.parse(yamlContent);
|
|
132
|
+
if (!isRecord(parsed)) {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
awaitingWorkspaceStatus: getStringValue(
|
|
137
|
+
parsed,
|
|
138
|
+
'awaitingWorkspaceStatus',
|
|
139
|
+
),
|
|
140
|
+
preparationStatus: getStringValue(parsed, 'preparationStatus'),
|
|
141
|
+
defaultAgentName: getStringValue(parsed, 'defaultAgentName'),
|
|
142
|
+
logFilePath: getStringValue(parsed, 'logFilePath'),
|
|
143
|
+
maximumPreparingIssuesCount: getNumberValue(
|
|
144
|
+
parsed,
|
|
145
|
+
'maximumPreparingIssuesCount',
|
|
146
|
+
),
|
|
147
|
+
allowIssueCacheMinutes: getNumberValue(parsed, 'allowIssueCacheMinutes'),
|
|
148
|
+
awaitingQualityCheckStatus: getStringValue(
|
|
149
|
+
parsed,
|
|
150
|
+
'awaitingQualityCheckStatus',
|
|
151
|
+
),
|
|
152
|
+
thresholdForAutoReject: getNumberValue(parsed, 'thresholdForAutoReject'),
|
|
153
|
+
workflowBlockerResolvedWebhookUrl: getStringValue(
|
|
154
|
+
parsed,
|
|
155
|
+
'workflowBlockerResolvedWebhookUrl',
|
|
156
|
+
),
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
console.warn('Failed to parse YAML from project README config section');
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const mergeConfigs = (
|
|
165
|
+
configFile: ConfigFile,
|
|
166
|
+
cliOverrides: ConfigFile,
|
|
167
|
+
readmeOverrides: ConfigFile,
|
|
168
|
+
): ConfigFile => ({
|
|
169
|
+
projectUrl: cliOverrides.projectUrl ?? configFile.projectUrl,
|
|
170
|
+
awaitingWorkspaceStatus:
|
|
171
|
+
readmeOverrides.awaitingWorkspaceStatus ??
|
|
172
|
+
cliOverrides.awaitingWorkspaceStatus ??
|
|
173
|
+
configFile.awaitingWorkspaceStatus,
|
|
174
|
+
preparationStatus:
|
|
175
|
+
readmeOverrides.preparationStatus ??
|
|
176
|
+
cliOverrides.preparationStatus ??
|
|
177
|
+
configFile.preparationStatus,
|
|
178
|
+
defaultAgentName:
|
|
179
|
+
readmeOverrides.defaultAgentName ??
|
|
180
|
+
cliOverrides.defaultAgentName ??
|
|
181
|
+
configFile.defaultAgentName,
|
|
182
|
+
logFilePath:
|
|
183
|
+
readmeOverrides.logFilePath ??
|
|
184
|
+
cliOverrides.logFilePath ??
|
|
185
|
+
configFile.logFilePath,
|
|
186
|
+
maximumPreparingIssuesCount:
|
|
187
|
+
readmeOverrides.maximumPreparingIssuesCount ??
|
|
188
|
+
cliOverrides.maximumPreparingIssuesCount ??
|
|
189
|
+
configFile.maximumPreparingIssuesCount,
|
|
190
|
+
allowIssueCacheMinutes:
|
|
191
|
+
readmeOverrides.allowIssueCacheMinutes ??
|
|
192
|
+
cliOverrides.allowIssueCacheMinutes ??
|
|
193
|
+
configFile.allowIssueCacheMinutes,
|
|
194
|
+
awaitingQualityCheckStatus:
|
|
195
|
+
readmeOverrides.awaitingQualityCheckStatus ??
|
|
196
|
+
cliOverrides.awaitingQualityCheckStatus ??
|
|
197
|
+
configFile.awaitingQualityCheckStatus,
|
|
198
|
+
thresholdForAutoReject:
|
|
199
|
+
readmeOverrides.thresholdForAutoReject ??
|
|
200
|
+
cliOverrides.thresholdForAutoReject ??
|
|
201
|
+
configFile.thresholdForAutoReject,
|
|
202
|
+
workflowBlockerResolvedWebhookUrl:
|
|
203
|
+
readmeOverrides.workflowBlockerResolvedWebhookUrl ??
|
|
204
|
+
cliOverrides.workflowBlockerResolvedWebhookUrl ??
|
|
205
|
+
configFile.workflowBlockerResolvedWebhookUrl,
|
|
206
|
+
projectName: configFile.projectName,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
type GraphqlProjectV2ReadmeResponse = {
|
|
210
|
+
data?: {
|
|
211
|
+
organization?: { projectV2?: { readme?: string | null } };
|
|
212
|
+
user?: { projectV2?: { readme?: string | null } };
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const isGraphqlProjectV2ReadmeResponse = (
|
|
217
|
+
value: unknown,
|
|
218
|
+
): value is GraphqlProjectV2ReadmeResponse => {
|
|
219
|
+
if (!isRecord(value)) return false;
|
|
220
|
+
const data = value['data'];
|
|
221
|
+
if (data !== undefined && !isRecord(data)) return false;
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const fetchProjectReadme = async (
|
|
226
|
+
projectUrl: string,
|
|
227
|
+
token: string,
|
|
228
|
+
): Promise<string | null> => {
|
|
229
|
+
try {
|
|
230
|
+
const urlParts = projectUrl.split('/');
|
|
231
|
+
const projectNumber = parseInt(urlParts[urlParts.length - 1], 10);
|
|
232
|
+
const owner = urlParts[urlParts.length - 3];
|
|
233
|
+
|
|
234
|
+
const query = `
|
|
235
|
+
query($owner: String!, $number: Int!) {
|
|
236
|
+
organization(login: $owner) {
|
|
237
|
+
projectV2(number: $number) {
|
|
238
|
+
readme
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
user(login: $owner) {
|
|
242
|
+
projectV2(number: $number) {
|
|
243
|
+
readme
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: {
|
|
252
|
+
Authorization: `Bearer ${token}`,
|
|
253
|
+
'Content-Type': 'application/json',
|
|
254
|
+
},
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
query,
|
|
257
|
+
variables: { owner, number: projectNumber },
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`GraphQL request failed: ${response.status}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const responseData: unknown = await response.json();
|
|
266
|
+
|
|
267
|
+
if (!isGraphqlProjectV2ReadmeResponse(responseData)) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const orgReadme = responseData.data?.organization?.projectV2?.readme;
|
|
272
|
+
const userReadme = responseData.data?.user?.projectV2?.readme;
|
|
273
|
+
const readme =
|
|
274
|
+
typeof orgReadme === 'string'
|
|
275
|
+
? orgReadme
|
|
276
|
+
: typeof userReadme === 'string'
|
|
277
|
+
? userReadme
|
|
278
|
+
: null;
|
|
279
|
+
|
|
280
|
+
return readme;
|
|
281
|
+
} catch {
|
|
282
|
+
console.warn('Failed to fetch project README');
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const buildGithubRepositoryParams = (
|
|
288
|
+
localStorageRepository: LocalStorageRepository,
|
|
289
|
+
cachePath: string,
|
|
290
|
+
token: string,
|
|
291
|
+
): ConstructorParameters<typeof BaseGitHubRepository> => [
|
|
292
|
+
localStorageRepository,
|
|
293
|
+
`${cachePath}/github.com.cookies.json`,
|
|
294
|
+
token,
|
|
295
|
+
undefined,
|
|
296
|
+
undefined,
|
|
297
|
+
undefined,
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
interface ScheduleOptions {
|
|
8
301
|
trigger: 'issue' | 'schedule';
|
|
9
302
|
config: string;
|
|
10
303
|
issue?: string;
|
|
11
304
|
verbose: boolean;
|
|
12
305
|
}
|
|
13
306
|
|
|
307
|
+
export const program = new Command();
|
|
308
|
+
|
|
14
309
|
program
|
|
15
310
|
.name('github-issue-tower-defence-management')
|
|
16
|
-
.description('CLI tool for GitHub Issue Tower Defence Management')
|
|
311
|
+
.description('CLI tool for GitHub Issue Tower Defence Management');
|
|
312
|
+
|
|
313
|
+
program
|
|
314
|
+
.command('schedule', { isDefault: true })
|
|
315
|
+
.description('Handle scheduled events (trigger: issue or schedule)')
|
|
17
316
|
.requiredOption(
|
|
18
317
|
'-t, --trigger <type>',
|
|
19
318
|
'Trigger type: issue or schedule',
|
|
@@ -22,7 +321,7 @@ program
|
|
|
22
321
|
.requiredOption('-c, --config <path>', 'Path to config YAML file')
|
|
23
322
|
.option('-v, --verbose', 'Verbose output')
|
|
24
323
|
.option('-i, --issue <url>', 'GitHub Issue URL')
|
|
25
|
-
.action(async (options:
|
|
324
|
+
.action(async (options: ScheduleOptions) => {
|
|
26
325
|
if (options.trigger === 'issue' && !options.issue) {
|
|
27
326
|
console.error('Issue URL is required when trigger type is "issue"');
|
|
28
327
|
process.exit(1);
|
|
@@ -33,6 +332,350 @@ program
|
|
|
33
332
|
}
|
|
34
333
|
});
|
|
35
334
|
|
|
36
|
-
|
|
335
|
+
program
|
|
336
|
+
.command('startDaemon')
|
|
337
|
+
.description('Start daemon to prepare GitHub issues')
|
|
338
|
+
.requiredOption(
|
|
339
|
+
'--configFilePath <path>',
|
|
340
|
+
'Path to config file for tower defence management',
|
|
341
|
+
)
|
|
342
|
+
.option('--projectUrl <url>', 'GitHub project URL')
|
|
343
|
+
.option(
|
|
344
|
+
'--awaitingWorkspaceStatus <status>',
|
|
345
|
+
'Status for issues awaiting workspace',
|
|
346
|
+
)
|
|
347
|
+
.option('--preparationStatus <status>', 'Status for issues in preparation')
|
|
348
|
+
.option('--defaultAgentName <name>', 'Default agent name')
|
|
349
|
+
.option('--logFilePath <path>', 'Path to log file')
|
|
350
|
+
.option(
|
|
351
|
+
'--maximumPreparingIssuesCount <count>',
|
|
352
|
+
'Maximum number of issues in preparation status (default: 6)',
|
|
353
|
+
)
|
|
354
|
+
.option(
|
|
355
|
+
'--allowIssueCacheMinutes <minutes>',
|
|
356
|
+
'Allow cache for issues in minutes (default: 0)',
|
|
357
|
+
)
|
|
358
|
+
.action(async (options: StartDaemonOptions) => {
|
|
359
|
+
const token = process.env.GH_TOKEN;
|
|
360
|
+
if (!token) {
|
|
361
|
+
console.error('GH_TOKEN environment variable is required');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const configFileValues = loadConfigFile(options.configFilePath);
|
|
366
|
+
|
|
367
|
+
const cliOverrides: ConfigFile = {
|
|
368
|
+
projectUrl: options.projectUrl,
|
|
369
|
+
awaitingWorkspaceStatus: options.awaitingWorkspaceStatus,
|
|
370
|
+
preparationStatus: options.preparationStatus,
|
|
371
|
+
defaultAgentName: options.defaultAgentName,
|
|
372
|
+
logFilePath: options.logFilePath,
|
|
373
|
+
maximumPreparingIssuesCount: options.maximumPreparingIssuesCount
|
|
374
|
+
? Number(options.maximumPreparingIssuesCount)
|
|
375
|
+
: undefined,
|
|
376
|
+
allowIssueCacheMinutes: options.allowIssueCacheMinutes
|
|
377
|
+
? Number(options.allowIssueCacheMinutes)
|
|
378
|
+
: undefined,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const tempProjectUrl =
|
|
382
|
+
cliOverrides.projectUrl ?? configFileValues.projectUrl;
|
|
383
|
+
|
|
384
|
+
let readmeOverrides: ConfigFile = {};
|
|
385
|
+
if (tempProjectUrl) {
|
|
386
|
+
const readme = await fetchProjectReadme(tempProjectUrl, token);
|
|
387
|
+
if (readme) {
|
|
388
|
+
readmeOverrides = parseProjectReadmeConfig(readme);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const config = mergeConfigs(
|
|
393
|
+
configFileValues,
|
|
394
|
+
cliOverrides,
|
|
395
|
+
readmeOverrides,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const projectUrl = config.projectUrl;
|
|
399
|
+
const awaitingWorkspaceStatus = config.awaitingWorkspaceStatus;
|
|
400
|
+
const preparationStatus = config.preparationStatus;
|
|
401
|
+
const defaultAgentName = config.defaultAgentName;
|
|
402
|
+
const logFilePath = config.logFilePath;
|
|
403
|
+
|
|
404
|
+
if (!projectUrl) {
|
|
405
|
+
console.error(
|
|
406
|
+
'projectUrl is required. Provide via --projectUrl, config file, or project README.',
|
|
407
|
+
);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
if (!awaitingWorkspaceStatus) {
|
|
411
|
+
console.error(
|
|
412
|
+
'awaitingWorkspaceStatus is required. Provide via --awaitingWorkspaceStatus, config file, or project README.',
|
|
413
|
+
);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
if (!preparationStatus) {
|
|
417
|
+
console.error(
|
|
418
|
+
'preparationStatus is required. Provide via --preparationStatus, config file, or project README.',
|
|
419
|
+
);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
if (!defaultAgentName) {
|
|
423
|
+
console.error(
|
|
424
|
+
'defaultAgentName is required. Provide via --defaultAgentName, config file, or project README.',
|
|
425
|
+
);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let maximumPreparingIssuesCount: number | null = null;
|
|
430
|
+
const rawMaxCount = config.maximumPreparingIssuesCount;
|
|
431
|
+
if (rawMaxCount !== undefined) {
|
|
432
|
+
const parsedCount = Number(rawMaxCount);
|
|
433
|
+
if (
|
|
434
|
+
!Number.isFinite(parsedCount) ||
|
|
435
|
+
!Number.isInteger(parsedCount) ||
|
|
436
|
+
parsedCount <= 0
|
|
437
|
+
) {
|
|
438
|
+
console.error(
|
|
439
|
+
'Invalid value for --maximumPreparingIssuesCount. It must be a positive integer.',
|
|
440
|
+
);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
maximumPreparingIssuesCount = parsedCount;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const allowIssueCacheMinutes = config.allowIssueCacheMinutes ?? 0;
|
|
447
|
+
|
|
448
|
+
console.log(
|
|
449
|
+
`maximumPreparingIssuesCount: ${maximumPreparingIssuesCount ?? 'null (default: 6)'}`,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const projectName = config.projectName ?? 'default';
|
|
453
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
454
|
+
const cachePath = `./tmp/cache/${projectName}`;
|
|
455
|
+
const localStorageCacheRepository = new LocalStorageCacheRepository(
|
|
456
|
+
localStorageRepository,
|
|
457
|
+
cachePath,
|
|
458
|
+
);
|
|
459
|
+
const githubRepositoryParams = buildGithubRepositoryParams(
|
|
460
|
+
localStorageRepository,
|
|
461
|
+
cachePath,
|
|
462
|
+
token,
|
|
463
|
+
);
|
|
464
|
+
const projectRepository = {
|
|
465
|
+
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
466
|
+
...new CheerioProjectRepository(...githubRepositoryParams),
|
|
467
|
+
};
|
|
468
|
+
const apiV3IssueRepository = new ApiV3IssueRepository(
|
|
469
|
+
...githubRepositoryParams,
|
|
470
|
+
);
|
|
471
|
+
const restIssueRepository = new RestIssueRepository(
|
|
472
|
+
...githubRepositoryParams,
|
|
473
|
+
);
|
|
474
|
+
const graphqlProjectItemRepository = new GraphqlProjectItemRepository(
|
|
475
|
+
...githubRepositoryParams,
|
|
476
|
+
);
|
|
477
|
+
const issueRepository = new ApiV3CheerioRestIssueRepository(
|
|
478
|
+
apiV3IssueRepository,
|
|
479
|
+
restIssueRepository,
|
|
480
|
+
graphqlProjectItemRepository,
|
|
481
|
+
localStorageCacheRepository,
|
|
482
|
+
...githubRepositoryParams,
|
|
483
|
+
);
|
|
484
|
+
const claudeRepository = new OauthAPIClaudeRepository();
|
|
485
|
+
const localCommandRunner = new NodeLocalCommandRunner();
|
|
486
|
+
|
|
487
|
+
const useCase = new StartPreparationUseCase(
|
|
488
|
+
projectRepository,
|
|
489
|
+
issueRepository,
|
|
490
|
+
claudeRepository,
|
|
491
|
+
localCommandRunner,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
await useCase.run({
|
|
495
|
+
projectUrl,
|
|
496
|
+
awaitingWorkspaceStatus,
|
|
497
|
+
preparationStatus,
|
|
498
|
+
defaultAgentName,
|
|
499
|
+
logFilePath: logFilePath ?? undefined,
|
|
500
|
+
maximumPreparingIssuesCount,
|
|
501
|
+
allowIssueCacheMinutes,
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
program
|
|
506
|
+
.command('notifyFinishedIssuePreparation')
|
|
507
|
+
.description('Notify that issue preparation is finished')
|
|
508
|
+
.requiredOption(
|
|
509
|
+
'--configFilePath <path>',
|
|
510
|
+
'Path to config file for tower defence management',
|
|
511
|
+
)
|
|
512
|
+
.requiredOption('--issueUrl <url>', 'GitHub issue URL')
|
|
513
|
+
.option('--projectUrl <url>', 'GitHub project URL')
|
|
514
|
+
.option('--preparationStatus <status>', 'Status for issues in preparation')
|
|
515
|
+
.option(
|
|
516
|
+
'--awaitingWorkspaceStatus <status>',
|
|
517
|
+
'Status for issues awaiting workspace',
|
|
518
|
+
)
|
|
519
|
+
.option(
|
|
520
|
+
'--awaitingQualityCheckStatus <status>',
|
|
521
|
+
'Status for issues awaiting quality check',
|
|
522
|
+
)
|
|
523
|
+
.option(
|
|
524
|
+
'--thresholdForAutoReject <count>',
|
|
525
|
+
'Threshold for auto-escalation after consecutive rejections (default: 3)',
|
|
526
|
+
)
|
|
527
|
+
.option(
|
|
528
|
+
'--workflowBlockerResolvedWebhookUrl <url>',
|
|
529
|
+
'Webhook URL to notify when a workflow blocker issue status changes to awaiting quality check. Supports {URL} and {MESSAGE} placeholders.',
|
|
530
|
+
)
|
|
531
|
+
.action(async (options: NotifyFinishedOptions) => {
|
|
532
|
+
const token = process.env.GH_TOKEN;
|
|
533
|
+
if (!token) {
|
|
534
|
+
console.error('GH_TOKEN environment variable is required');
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const configFileValues = loadConfigFile(options.configFilePath);
|
|
539
|
+
|
|
540
|
+
const cliOverrides: ConfigFile = {
|
|
541
|
+
projectUrl: options.projectUrl,
|
|
542
|
+
preparationStatus: options.preparationStatus,
|
|
543
|
+
awaitingWorkspaceStatus: options.awaitingWorkspaceStatus,
|
|
544
|
+
awaitingQualityCheckStatus: options.awaitingQualityCheckStatus,
|
|
545
|
+
thresholdForAutoReject: options.thresholdForAutoReject
|
|
546
|
+
? Number(options.thresholdForAutoReject)
|
|
547
|
+
: undefined,
|
|
548
|
+
workflowBlockerResolvedWebhookUrl:
|
|
549
|
+
options.workflowBlockerResolvedWebhookUrl,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const tempProjectUrl =
|
|
553
|
+
cliOverrides.projectUrl ?? configFileValues.projectUrl;
|
|
554
|
+
|
|
555
|
+
let readmeOverrides: ConfigFile = {};
|
|
556
|
+
if (tempProjectUrl) {
|
|
557
|
+
const readme = await fetchProjectReadme(tempProjectUrl, token);
|
|
558
|
+
if (readme) {
|
|
559
|
+
readmeOverrides = parseProjectReadmeConfig(readme);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const config = mergeConfigs(
|
|
564
|
+
configFileValues,
|
|
565
|
+
cliOverrides,
|
|
566
|
+
readmeOverrides,
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
const projectUrl = config.projectUrl;
|
|
570
|
+
const preparationStatus = config.preparationStatus;
|
|
571
|
+
const awaitingWorkspaceStatus = config.awaitingWorkspaceStatus;
|
|
572
|
+
const awaitingQualityCheckStatus = config.awaitingQualityCheckStatus;
|
|
573
|
+
|
|
574
|
+
if (!projectUrl) {
|
|
575
|
+
console.error(
|
|
576
|
+
'projectUrl is required. Provide via --projectUrl, config file, or project README.',
|
|
577
|
+
);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
if (!preparationStatus) {
|
|
581
|
+
console.error(
|
|
582
|
+
'preparationStatus is required. Provide via --preparationStatus, config file, or project README.',
|
|
583
|
+
);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
if (!awaitingWorkspaceStatus) {
|
|
587
|
+
console.error(
|
|
588
|
+
'awaitingWorkspaceStatus is required. Provide via --awaitingWorkspaceStatus, config file, or project README.',
|
|
589
|
+
);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
if (!awaitingQualityCheckStatus) {
|
|
593
|
+
console.error(
|
|
594
|
+
'awaitingQualityCheckStatus is required. Provide via --awaitingQualityCheckStatus, config file, or project README.',
|
|
595
|
+
);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let thresholdForAutoReject = 3;
|
|
600
|
+
const rawThreshold = config.thresholdForAutoReject;
|
|
601
|
+
if (rawThreshold !== undefined) {
|
|
602
|
+
const parsed = Number(rawThreshold);
|
|
603
|
+
if (
|
|
604
|
+
!Number.isFinite(parsed) ||
|
|
605
|
+
!Number.isInteger(parsed) ||
|
|
606
|
+
parsed <= 0
|
|
607
|
+
) {
|
|
608
|
+
console.error(
|
|
609
|
+
'Invalid value for --thresholdForAutoReject. It must be a positive integer.',
|
|
610
|
+
);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
thresholdForAutoReject = parsed;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const workflowBlockerResolvedWebhookUrl: string | null =
|
|
617
|
+
config.workflowBlockerResolvedWebhookUrl ?? null;
|
|
618
|
+
|
|
619
|
+
const projectName = config.projectName ?? 'default';
|
|
620
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
621
|
+
const cachePath = `./tmp/cache/${projectName}`;
|
|
622
|
+
const localStorageCacheRepository = new LocalStorageCacheRepository(
|
|
623
|
+
localStorageRepository,
|
|
624
|
+
cachePath,
|
|
625
|
+
);
|
|
626
|
+
const githubRepositoryParams = buildGithubRepositoryParams(
|
|
627
|
+
localStorageRepository,
|
|
628
|
+
cachePath,
|
|
629
|
+
token,
|
|
630
|
+
);
|
|
631
|
+
const projectRepository = {
|
|
632
|
+
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
633
|
+
...new CheerioProjectRepository(...githubRepositoryParams),
|
|
634
|
+
prepareStatus: async (
|
|
635
|
+
_name: string,
|
|
636
|
+
project: Project,
|
|
637
|
+
): Promise<Project> => {
|
|
638
|
+
return project;
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
const apiV3IssueRepository = new ApiV3IssueRepository(
|
|
642
|
+
...githubRepositoryParams,
|
|
643
|
+
);
|
|
644
|
+
const restIssueRepository = new RestIssueRepository(
|
|
645
|
+
...githubRepositoryParams,
|
|
646
|
+
);
|
|
647
|
+
const graphqlProjectItemRepository = new GraphqlProjectItemRepository(
|
|
648
|
+
...githubRepositoryParams,
|
|
649
|
+
);
|
|
650
|
+
const issueRepository = new ApiV3CheerioRestIssueRepository(
|
|
651
|
+
apiV3IssueRepository,
|
|
652
|
+
restIssueRepository,
|
|
653
|
+
graphqlProjectItemRepository,
|
|
654
|
+
localStorageCacheRepository,
|
|
655
|
+
...githubRepositoryParams,
|
|
656
|
+
);
|
|
657
|
+
const issueCommentRepository = new GitHubIssueCommentRepository(token);
|
|
658
|
+
const webhookRepository = new FetchWebhookRepository();
|
|
659
|
+
|
|
660
|
+
const useCase = new NotifyFinishedIssuePreparationUseCase(
|
|
661
|
+
projectRepository,
|
|
662
|
+
issueRepository,
|
|
663
|
+
issueCommentRepository,
|
|
664
|
+
webhookRepository,
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
await useCase.run({
|
|
668
|
+
projectUrl,
|
|
669
|
+
issueUrl: options.issueUrl,
|
|
670
|
+
preparationStatus,
|
|
671
|
+
awaitingWorkspaceStatus,
|
|
672
|
+
awaitingQualityCheckStatus,
|
|
673
|
+
thresholdForAutoReject,
|
|
674
|
+
workflowBlockerResolvedWebhookUrl,
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
/* istanbul ignore next */
|
|
679
|
+
if (process.argv && require.main === module) {
|
|
37
680
|
program.parse(process.argv);
|
|
38
681
|
}
|
|
@@ -90,6 +90,20 @@ jest.mock('../../repositories/NodeLocalCommandRunner', () => ({
|
|
|
90
90
|
jest.mock('../../repositories/StubClaudeRepository', () => ({
|
|
91
91
|
StubClaudeRepository: jest.fn().mockImplementation(() => ({})),
|
|
92
92
|
}));
|
|
93
|
+
jest.mock(
|
|
94
|
+
'../../../domain/usecases/NotifyFinishedIssuePreparationUseCase',
|
|
95
|
+
() => ({
|
|
96
|
+
NotifyFinishedIssuePreparationUseCase: jest
|
|
97
|
+
.fn()
|
|
98
|
+
.mockImplementation(() => ({})),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
jest.mock('../../repositories/GitHubIssueCommentRepository', () => ({
|
|
102
|
+
GitHubIssueCommentRepository: jest.fn().mockImplementation(() => ({})),
|
|
103
|
+
}));
|
|
104
|
+
jest.mock('../../repositories/FetchWebhookRepository', () => ({
|
|
105
|
+
FetchWebhookRepository: jest.fn().mockImplementation(() => ({})),
|
|
106
|
+
}));
|
|
93
107
|
|
|
94
108
|
import { HandleScheduledEventUseCaseHandler } from './HandleScheduledEventUseCaseHandler';
|
|
95
109
|
import { GraphqlProjectRepository } from '../../repositories/GraphqlProjectRepository';
|