github-issue-tower-defence-management 1.44.2 → 1.44.3
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 +9 -0
- package/README.md +1 -1
- package/bin/adapter/entry-points/cli/index.js +15 -224
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +254 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +57 -5
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +15 -329
- package/src/adapter/entry-points/cli/projectConfig.ts +329 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +144 -5
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +78 -5
- package/types/adapter/entry-points/cli/index.d.ts +1 -25
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +26 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
|
|
4
|
+
export type ConfigFile = {
|
|
5
|
+
projectUrl?: string;
|
|
6
|
+
awaitingWorkspaceStatus?: string;
|
|
7
|
+
preparationStatus?: string;
|
|
8
|
+
defaultAgentName?: string;
|
|
9
|
+
defaultLlmModelName?: string;
|
|
10
|
+
defaultLlmAgentName?: string;
|
|
11
|
+
maximumPreparingIssuesCount?: number;
|
|
12
|
+
allowIssueCacheMinutes?: number;
|
|
13
|
+
utilizationPercentageThreshold?: number;
|
|
14
|
+
allowedIssueAuthors?: string;
|
|
15
|
+
awaitingQualityCheckStatus?: string;
|
|
16
|
+
thresholdForAutoReject?: number;
|
|
17
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
18
|
+
projectName?: string;
|
|
19
|
+
preparationProcessCheckCommand?: string;
|
|
20
|
+
codexHomeCandidates?: string[];
|
|
21
|
+
awLogDirectoryPath?: string;
|
|
22
|
+
awLogStaleThresholdMinutes?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getStringValue = (
|
|
26
|
+
obj: Record<string, unknown>,
|
|
27
|
+
key: string,
|
|
28
|
+
): string | undefined => {
|
|
29
|
+
const value = obj[key];
|
|
30
|
+
return typeof value === 'string' ? value : undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getNumberValue = (
|
|
34
|
+
obj: Record<string, unknown>,
|
|
35
|
+
key: string,
|
|
36
|
+
): number | undefined => {
|
|
37
|
+
const value = obj[key];
|
|
38
|
+
return typeof value === 'number' ? value : undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getStringArrayValue = (
|
|
42
|
+
obj: Record<string, unknown>,
|
|
43
|
+
key: string,
|
|
44
|
+
): string[] | undefined => {
|
|
45
|
+
const value = obj[key];
|
|
46
|
+
if (!Array.isArray(value)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const strings: string[] = [];
|
|
50
|
+
for (const item of value) {
|
|
51
|
+
if (typeof item !== 'string') {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
strings.push(item);
|
|
55
|
+
}
|
|
56
|
+
return strings;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
60
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
61
|
+
|
|
62
|
+
export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(configFilePath, 'utf-8');
|
|
65
|
+
const parsed: unknown = YAML.parse(content);
|
|
66
|
+
if (!isRecord(parsed)) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
projectUrl: getStringValue(parsed, 'projectUrl'),
|
|
71
|
+
awaitingWorkspaceStatus: getStringValue(
|
|
72
|
+
parsed,
|
|
73
|
+
'awaitingWorkspaceStatus',
|
|
74
|
+
),
|
|
75
|
+
preparationStatus: getStringValue(parsed, 'preparationStatus'),
|
|
76
|
+
defaultAgentName: getStringValue(parsed, 'defaultAgentName'),
|
|
77
|
+
defaultLlmModelName: getStringValue(parsed, 'defaultLlmModelName'),
|
|
78
|
+
defaultLlmAgentName: getStringValue(parsed, 'defaultLlmAgentName'),
|
|
79
|
+
maximumPreparingIssuesCount: getNumberValue(
|
|
80
|
+
parsed,
|
|
81
|
+
'maximumPreparingIssuesCount',
|
|
82
|
+
),
|
|
83
|
+
allowIssueCacheMinutes: getNumberValue(parsed, 'allowIssueCacheMinutes'),
|
|
84
|
+
utilizationPercentageThreshold: getNumberValue(
|
|
85
|
+
parsed,
|
|
86
|
+
'utilizationPercentageThreshold',
|
|
87
|
+
),
|
|
88
|
+
allowedIssueAuthors: getStringValue(parsed, 'allowedIssueAuthors'),
|
|
89
|
+
awaitingQualityCheckStatus: getStringValue(
|
|
90
|
+
parsed,
|
|
91
|
+
'awaitingQualityCheckStatus',
|
|
92
|
+
),
|
|
93
|
+
thresholdForAutoReject: getNumberValue(parsed, 'thresholdForAutoReject'),
|
|
94
|
+
workflowBlockerResolvedWebhookUrl: getStringValue(
|
|
95
|
+
parsed,
|
|
96
|
+
'workflowBlockerResolvedWebhookUrl',
|
|
97
|
+
),
|
|
98
|
+
projectName: getStringValue(parsed, 'projectName'),
|
|
99
|
+
preparationProcessCheckCommand: getStringValue(
|
|
100
|
+
parsed,
|
|
101
|
+
'preparationProcessCheckCommand',
|
|
102
|
+
),
|
|
103
|
+
codexHomeCandidates: getStringArrayValue(parsed, 'codexHomeCandidates'),
|
|
104
|
+
awLogDirectoryPath: getStringValue(parsed, 'awLogDirectoryPath'),
|
|
105
|
+
awLogStaleThresholdMinutes: getNumberValue(
|
|
106
|
+
parsed,
|
|
107
|
+
'awLogStaleThresholdMinutes',
|
|
108
|
+
),
|
|
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
|
+
defaultLlmModelName: getStringValue(parsed, 'defaultLlmModelName'),
|
|
143
|
+
defaultLlmAgentName: getStringValue(parsed, 'defaultLlmAgentName'),
|
|
144
|
+
maximumPreparingIssuesCount: getNumberValue(
|
|
145
|
+
parsed,
|
|
146
|
+
'maximumPreparingIssuesCount',
|
|
147
|
+
),
|
|
148
|
+
allowIssueCacheMinutes: getNumberValue(parsed, 'allowIssueCacheMinutes'),
|
|
149
|
+
utilizationPercentageThreshold: getNumberValue(
|
|
150
|
+
parsed,
|
|
151
|
+
'utilizationPercentageThreshold',
|
|
152
|
+
),
|
|
153
|
+
allowedIssueAuthors: getStringValue(parsed, 'allowedIssueAuthors'),
|
|
154
|
+
awaitingQualityCheckStatus: getStringValue(
|
|
155
|
+
parsed,
|
|
156
|
+
'awaitingQualityCheckStatus',
|
|
157
|
+
),
|
|
158
|
+
thresholdForAutoReject: getNumberValue(parsed, 'thresholdForAutoReject'),
|
|
159
|
+
workflowBlockerResolvedWebhookUrl: getStringValue(
|
|
160
|
+
parsed,
|
|
161
|
+
'workflowBlockerResolvedWebhookUrl',
|
|
162
|
+
),
|
|
163
|
+
preparationProcessCheckCommand: getStringValue(
|
|
164
|
+
parsed,
|
|
165
|
+
'preparationProcessCheckCommand',
|
|
166
|
+
),
|
|
167
|
+
codexHomeCandidates: getStringArrayValue(parsed, 'codexHomeCandidates'),
|
|
168
|
+
awLogDirectoryPath: getStringValue(parsed, 'awLogDirectoryPath'),
|
|
169
|
+
awLogStaleThresholdMinutes: getNumberValue(
|
|
170
|
+
parsed,
|
|
171
|
+
'awLogStaleThresholdMinutes',
|
|
172
|
+
),
|
|
173
|
+
};
|
|
174
|
+
} catch {
|
|
175
|
+
console.warn('Failed to parse YAML from project README config section');
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const mergeConfigs = (
|
|
181
|
+
configFile: ConfigFile,
|
|
182
|
+
cliOverrides: ConfigFile,
|
|
183
|
+
readmeOverrides: ConfigFile,
|
|
184
|
+
): ConfigFile => ({
|
|
185
|
+
projectUrl: cliOverrides.projectUrl ?? configFile.projectUrl,
|
|
186
|
+
awaitingWorkspaceStatus:
|
|
187
|
+
readmeOverrides.awaitingWorkspaceStatus ??
|
|
188
|
+
cliOverrides.awaitingWorkspaceStatus ??
|
|
189
|
+
configFile.awaitingWorkspaceStatus,
|
|
190
|
+
preparationStatus:
|
|
191
|
+
readmeOverrides.preparationStatus ??
|
|
192
|
+
cliOverrides.preparationStatus ??
|
|
193
|
+
configFile.preparationStatus,
|
|
194
|
+
defaultAgentName:
|
|
195
|
+
readmeOverrides.defaultAgentName ??
|
|
196
|
+
cliOverrides.defaultAgentName ??
|
|
197
|
+
configFile.defaultAgentName,
|
|
198
|
+
defaultLlmModelName:
|
|
199
|
+
readmeOverrides.defaultLlmModelName ??
|
|
200
|
+
cliOverrides.defaultLlmModelName ??
|
|
201
|
+
configFile.defaultLlmModelName,
|
|
202
|
+
defaultLlmAgentName:
|
|
203
|
+
readmeOverrides.defaultLlmAgentName ??
|
|
204
|
+
cliOverrides.defaultLlmAgentName ??
|
|
205
|
+
configFile.defaultLlmAgentName,
|
|
206
|
+
maximumPreparingIssuesCount:
|
|
207
|
+
readmeOverrides.maximumPreparingIssuesCount ??
|
|
208
|
+
cliOverrides.maximumPreparingIssuesCount ??
|
|
209
|
+
configFile.maximumPreparingIssuesCount,
|
|
210
|
+
allowIssueCacheMinutes:
|
|
211
|
+
readmeOverrides.allowIssueCacheMinutes ??
|
|
212
|
+
cliOverrides.allowIssueCacheMinutes ??
|
|
213
|
+
configFile.allowIssueCacheMinutes,
|
|
214
|
+
utilizationPercentageThreshold:
|
|
215
|
+
readmeOverrides.utilizationPercentageThreshold ??
|
|
216
|
+
cliOverrides.utilizationPercentageThreshold ??
|
|
217
|
+
configFile.utilizationPercentageThreshold,
|
|
218
|
+
allowedIssueAuthors:
|
|
219
|
+
readmeOverrides.allowedIssueAuthors ??
|
|
220
|
+
cliOverrides.allowedIssueAuthors ??
|
|
221
|
+
configFile.allowedIssueAuthors,
|
|
222
|
+
awaitingQualityCheckStatus:
|
|
223
|
+
readmeOverrides.awaitingQualityCheckStatus ??
|
|
224
|
+
cliOverrides.awaitingQualityCheckStatus ??
|
|
225
|
+
configFile.awaitingQualityCheckStatus,
|
|
226
|
+
thresholdForAutoReject:
|
|
227
|
+
readmeOverrides.thresholdForAutoReject ??
|
|
228
|
+
cliOverrides.thresholdForAutoReject ??
|
|
229
|
+
configFile.thresholdForAutoReject,
|
|
230
|
+
workflowBlockerResolvedWebhookUrl:
|
|
231
|
+
readmeOverrides.workflowBlockerResolvedWebhookUrl ??
|
|
232
|
+
cliOverrides.workflowBlockerResolvedWebhookUrl ??
|
|
233
|
+
configFile.workflowBlockerResolvedWebhookUrl,
|
|
234
|
+
projectName: configFile.projectName,
|
|
235
|
+
preparationProcessCheckCommand:
|
|
236
|
+
readmeOverrides.preparationProcessCheckCommand ??
|
|
237
|
+
cliOverrides.preparationProcessCheckCommand ??
|
|
238
|
+
configFile.preparationProcessCheckCommand,
|
|
239
|
+
codexHomeCandidates:
|
|
240
|
+
readmeOverrides.codexHomeCandidates ??
|
|
241
|
+
cliOverrides.codexHomeCandidates ??
|
|
242
|
+
configFile.codexHomeCandidates,
|
|
243
|
+
awLogDirectoryPath:
|
|
244
|
+
readmeOverrides.awLogDirectoryPath ??
|
|
245
|
+
cliOverrides.awLogDirectoryPath ??
|
|
246
|
+
configFile.awLogDirectoryPath,
|
|
247
|
+
awLogStaleThresholdMinutes:
|
|
248
|
+
readmeOverrides.awLogStaleThresholdMinutes ??
|
|
249
|
+
cliOverrides.awLogStaleThresholdMinutes ??
|
|
250
|
+
configFile.awLogStaleThresholdMinutes,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
type GraphqlProjectV2ReadmeResponse = {
|
|
254
|
+
data?: {
|
|
255
|
+
organization?: { projectV2?: { readme?: string | null } };
|
|
256
|
+
user?: { projectV2?: { readme?: string | null } };
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const isGraphqlProjectV2ReadmeResponse = (
|
|
261
|
+
value: unknown,
|
|
262
|
+
): value is GraphqlProjectV2ReadmeResponse => {
|
|
263
|
+
if (!isRecord(value)) return false;
|
|
264
|
+
const data = value['data'];
|
|
265
|
+
if (data !== undefined && !isRecord(data)) return false;
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const fetchProjectReadme = async (
|
|
270
|
+
projectUrl: string,
|
|
271
|
+
token: string,
|
|
272
|
+
): Promise<string | null> => {
|
|
273
|
+
try {
|
|
274
|
+
const urlParts = projectUrl.split('/');
|
|
275
|
+
const projectNumber = parseInt(urlParts[urlParts.length - 1], 10);
|
|
276
|
+
const owner = urlParts[urlParts.length - 3];
|
|
277
|
+
|
|
278
|
+
const query = `
|
|
279
|
+
query($owner: String!, $number: Int!) {
|
|
280
|
+
organization(login: $owner) {
|
|
281
|
+
projectV2(number: $number) {
|
|
282
|
+
readme
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
user(login: $owner) {
|
|
286
|
+
projectV2(number: $number) {
|
|
287
|
+
readme
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: {
|
|
296
|
+
Authorization: `Bearer ${token}`,
|
|
297
|
+
'Content-Type': 'application/json',
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
query,
|
|
301
|
+
variables: { owner, number: projectNumber },
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error(`GraphQL request failed: ${response.status}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const responseData: unknown = await response.json();
|
|
310
|
+
|
|
311
|
+
if (!isGraphqlProjectV2ReadmeResponse(responseData)) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const orgReadme = responseData.data?.organization?.projectV2?.readme;
|
|
316
|
+
const userReadme = responseData.data?.user?.projectV2?.readme;
|
|
317
|
+
const readme =
|
|
318
|
+
typeof orgReadme === 'string'
|
|
319
|
+
? orgReadme
|
|
320
|
+
: typeof userReadme === 'string'
|
|
321
|
+
? userReadme
|
|
322
|
+
: null;
|
|
323
|
+
|
|
324
|
+
return readme;
|
|
325
|
+
} catch {
|
|
326
|
+
console.warn('Failed to fetch project README');
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import YAML from 'yaml';
|
|
3
|
+
import type { HandleScheduledEventUseCase } from '../../../domain/usecases/HandleScheduledEventUseCase';
|
|
3
4
|
|
|
4
5
|
jest.mock('fs');
|
|
5
6
|
jest.mock('gh-cookie', () => ({ getCookieContent: jest.fn() }));
|
|
@@ -15,11 +16,16 @@ jest.mock('../../repositories/issue/ApiV3CheerioRestIssueRepository');
|
|
|
15
16
|
jest.mock('../../repositories/LocalStorageCacheRepository');
|
|
16
17
|
jest.mock('../../repositories/BaseGitHubRepository');
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
type RunFn = HandleScheduledEventUseCase['run'];
|
|
20
|
+
const capturedRunInputs: Parameters<RunFn>[] = [];
|
|
21
|
+
const mockRun = jest.fn().mockImplementation((...args: Parameters<RunFn>) => {
|
|
22
|
+
capturedRunInputs.push(args);
|
|
23
|
+
return Promise.resolve({
|
|
24
|
+
project: { id: 'PVT_kwHOtest123' },
|
|
25
|
+
issues: [],
|
|
26
|
+
cacheUsed: false,
|
|
27
|
+
targetDateTimes: [],
|
|
28
|
+
});
|
|
23
29
|
});
|
|
24
30
|
|
|
25
31
|
jest.mock('../../../domain/usecases/HandleScheduledEventUseCase', () => ({
|
|
@@ -158,10 +164,25 @@ const validConfig = {
|
|
|
158
164
|
},
|
|
159
165
|
};
|
|
160
166
|
|
|
167
|
+
const mockFetchReturningReadme = (readme: string | null): void => {
|
|
168
|
+
const responseBody =
|
|
169
|
+
readme === null
|
|
170
|
+
? { data: {} }
|
|
171
|
+
: { data: { organization: { projectV2: { readme } } } };
|
|
172
|
+
jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
173
|
+
new Response(JSON.stringify(responseBody), {
|
|
174
|
+
status: 200,
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
161
180
|
describe('HandleScheduledEventUseCaseHandler', () => {
|
|
162
181
|
beforeEach(() => {
|
|
163
182
|
jest.clearAllMocks();
|
|
183
|
+
capturedRunInputs.length = 0;
|
|
164
184
|
jest.mocked(fs.readFileSync).mockReturnValue(YAML.stringify(validConfig));
|
|
185
|
+
mockFetchReturningReadme(null);
|
|
165
186
|
});
|
|
166
187
|
|
|
167
188
|
it('should pass bot credentials to repository constructors when provided', async () => {
|
|
@@ -330,4 +351,122 @@ describe('HandleScheduledEventUseCaseHandler', () => {
|
|
|
330
351
|
expect(jest.mocked(fs.writeFileSync)).not.toHaveBeenCalled();
|
|
331
352
|
expect(jest.mocked(fs.renameSync)).not.toHaveBeenCalled();
|
|
332
353
|
});
|
|
354
|
+
|
|
355
|
+
describe('README config overrides', () => {
|
|
356
|
+
const configWithStartPreparation = {
|
|
357
|
+
...validConfig,
|
|
358
|
+
allowIssueCacheMinutes: 5,
|
|
359
|
+
startPreparation: {
|
|
360
|
+
awaitingWorkspaceStatus: 'Awaiting workspace',
|
|
361
|
+
preparationStatus: 'Preparation',
|
|
362
|
+
defaultAgentName: 'yaml-agent',
|
|
363
|
+
configFilePath: '/path/to/config.yml',
|
|
364
|
+
maximumPreparingIssuesCount: 10,
|
|
365
|
+
utilizationPercentageThreshold: 90,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
it('should override startPreparation fields from README config', async () => {
|
|
370
|
+
const readmeContent = `<details>
|
|
371
|
+
<summary>config</summary>
|
|
372
|
+
maximumPreparingIssuesCount: 0
|
|
373
|
+
awaitingWorkspaceStatus: README Awaiting
|
|
374
|
+
preparationStatus: README Preparation
|
|
375
|
+
defaultAgentName: readme-agent
|
|
376
|
+
utilizationPercentageThreshold: 80
|
|
377
|
+
</details>`;
|
|
378
|
+
mockFetchReturningReadme(readmeContent);
|
|
379
|
+
jest
|
|
380
|
+
.mocked(fs.readFileSync)
|
|
381
|
+
.mockReturnValue(YAML.stringify(configWithStartPreparation));
|
|
382
|
+
|
|
383
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
384
|
+
await handler.handle('config.yml', false);
|
|
385
|
+
|
|
386
|
+
expect(capturedRunInputs[0][0]).toMatchObject({
|
|
387
|
+
startPreparation: {
|
|
388
|
+
maximumPreparingIssuesCount: 0,
|
|
389
|
+
awaitingWorkspaceStatus: 'README Awaiting',
|
|
390
|
+
preparationStatus: 'README Preparation',
|
|
391
|
+
defaultAgentName: 'readme-agent',
|
|
392
|
+
utilizationPercentageThreshold: 80,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should override allowIssueCacheMinutes from README config', async () => {
|
|
398
|
+
const readmeContent = `<details>
|
|
399
|
+
<summary>config</summary>
|
|
400
|
+
allowIssueCacheMinutes: 30
|
|
401
|
+
</details>`;
|
|
402
|
+
mockFetchReturningReadme(readmeContent);
|
|
403
|
+
jest
|
|
404
|
+
.mocked(fs.readFileSync)
|
|
405
|
+
.mockReturnValue(YAML.stringify(configWithStartPreparation));
|
|
406
|
+
|
|
407
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
408
|
+
await handler.handle('config.yml', false);
|
|
409
|
+
|
|
410
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
allowIssueCacheMinutes: 30,
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should split comma-separated allowedIssueAuthors from README config', async () => {
|
|
418
|
+
const readmeContent = `<details>
|
|
419
|
+
<summary>config</summary>
|
|
420
|
+
allowedIssueAuthors: 'user1, user2, user3'
|
|
421
|
+
</details>`;
|
|
422
|
+
mockFetchReturningReadme(readmeContent);
|
|
423
|
+
jest
|
|
424
|
+
.mocked(fs.readFileSync)
|
|
425
|
+
.mockReturnValue(YAML.stringify(configWithStartPreparation));
|
|
426
|
+
|
|
427
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
428
|
+
await handler.handle('config.yml', false);
|
|
429
|
+
|
|
430
|
+
expect(capturedRunInputs[0][0]).toMatchObject({
|
|
431
|
+
startPreparation: {
|
|
432
|
+
allowedIssueAuthors: ['user1', 'user2', 'user3'],
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should keep YAML values when README has no config section', async () => {
|
|
438
|
+
mockFetchReturningReadme(null);
|
|
439
|
+
jest
|
|
440
|
+
.mocked(fs.readFileSync)
|
|
441
|
+
.mockReturnValue(YAML.stringify(configWithStartPreparation));
|
|
442
|
+
|
|
443
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
444
|
+
await handler.handle('config.yml', false);
|
|
445
|
+
|
|
446
|
+
expect(capturedRunInputs[0][0]).toMatchObject({
|
|
447
|
+
allowIssueCacheMinutes: 5,
|
|
448
|
+
startPreparation: {
|
|
449
|
+
maximumPreparingIssuesCount: 10,
|
|
450
|
+
awaitingWorkspaceStatus: 'Awaiting workspace',
|
|
451
|
+
defaultAgentName: 'yaml-agent',
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should use README token from manager credentials to fetch README', async () => {
|
|
457
|
+
mockFetchReturningReadme(null);
|
|
458
|
+
jest.mocked(fs.readFileSync).mockReturnValue(YAML.stringify(validConfig));
|
|
459
|
+
|
|
460
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
461
|
+
await handler.handle('config.yml', false);
|
|
462
|
+
|
|
463
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
464
|
+
'https://api.github.com/graphql',
|
|
465
|
+
expect.objectContaining({ method: 'POST' }),
|
|
466
|
+
);
|
|
467
|
+
expect(capturedRunInputs[0][0]).toMatchObject({
|
|
468
|
+
projectUrl: validConfig.projectUrl,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|
|
333
472
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import TYPIA from 'typia';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
+
import {
|
|
5
|
+
fetchProjectReadme,
|
|
6
|
+
parseProjectReadmeConfig,
|
|
7
|
+
} from '../cli/projectConfig';
|
|
4
8
|
import { SystemDateRepository } from '../../repositories/SystemDateRepository';
|
|
5
9
|
import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
|
|
6
10
|
import { GoogleSpreadsheetRepository } from '../../repositories/GoogleSpreadsheetRepository';
|
|
@@ -81,6 +85,75 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
81
85
|
if (input.disabled) {
|
|
82
86
|
return null;
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
const managerToken = input.credentials.manager.github.token;
|
|
90
|
+
const readme = await fetchProjectReadme(input.projectUrl, managerToken);
|
|
91
|
+
const readmeConfig = readme ? parseProjectReadmeConfig(readme) : {};
|
|
92
|
+
|
|
93
|
+
const mergedInput = {
|
|
94
|
+
...input,
|
|
95
|
+
allowIssueCacheMinutes:
|
|
96
|
+
readmeConfig.allowIssueCacheMinutes ?? input.allowIssueCacheMinutes,
|
|
97
|
+
startPreparation: input.startPreparation
|
|
98
|
+
? {
|
|
99
|
+
...input.startPreparation,
|
|
100
|
+
awaitingWorkspaceStatus:
|
|
101
|
+
readmeConfig.awaitingWorkspaceStatus ??
|
|
102
|
+
input.startPreparation.awaitingWorkspaceStatus,
|
|
103
|
+
preparationStatus:
|
|
104
|
+
readmeConfig.preparationStatus ??
|
|
105
|
+
input.startPreparation.preparationStatus,
|
|
106
|
+
defaultAgentName:
|
|
107
|
+
readmeConfig.defaultAgentName ??
|
|
108
|
+
input.startPreparation.defaultAgentName,
|
|
109
|
+
defaultLlmModelName:
|
|
110
|
+
readmeConfig.defaultLlmModelName ??
|
|
111
|
+
input.startPreparation.defaultLlmModelName,
|
|
112
|
+
defaultLlmAgentName:
|
|
113
|
+
readmeConfig.defaultLlmAgentName ??
|
|
114
|
+
input.startPreparation.defaultLlmAgentName,
|
|
115
|
+
maximumPreparingIssuesCount:
|
|
116
|
+
readmeConfig.maximumPreparingIssuesCount ??
|
|
117
|
+
input.startPreparation.maximumPreparingIssuesCount,
|
|
118
|
+
utilizationPercentageThreshold:
|
|
119
|
+
readmeConfig.utilizationPercentageThreshold ??
|
|
120
|
+
input.startPreparation.utilizationPercentageThreshold,
|
|
121
|
+
allowedIssueAuthors: readmeConfig.allowedIssueAuthors
|
|
122
|
+
? readmeConfig.allowedIssueAuthors
|
|
123
|
+
.split(',')
|
|
124
|
+
.map((s) => s.trim())
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
: input.startPreparation.allowedIssueAuthors,
|
|
127
|
+
preparationProcessCheckCommand:
|
|
128
|
+
readmeConfig.preparationProcessCheckCommand ??
|
|
129
|
+
input.startPreparation.preparationProcessCheckCommand,
|
|
130
|
+
codexHomeCandidates:
|
|
131
|
+
readmeConfig.codexHomeCandidates ??
|
|
132
|
+
input.startPreparation.codexHomeCandidates,
|
|
133
|
+
}
|
|
134
|
+
: input.startPreparation,
|
|
135
|
+
notifyFinishedPreparation: input.notifyFinishedPreparation
|
|
136
|
+
? {
|
|
137
|
+
...input.notifyFinishedPreparation,
|
|
138
|
+
awaitingWorkspaceStatus:
|
|
139
|
+
readmeConfig.awaitingWorkspaceStatus ??
|
|
140
|
+
input.notifyFinishedPreparation.awaitingWorkspaceStatus,
|
|
141
|
+
preparationStatus:
|
|
142
|
+
readmeConfig.preparationStatus ??
|
|
143
|
+
input.notifyFinishedPreparation.preparationStatus,
|
|
144
|
+
awaitingQualityCheckStatus:
|
|
145
|
+
readmeConfig.awaitingQualityCheckStatus ??
|
|
146
|
+
input.notifyFinishedPreparation.awaitingQualityCheckStatus,
|
|
147
|
+
thresholdForAutoReject:
|
|
148
|
+
readmeConfig.thresholdForAutoReject ??
|
|
149
|
+
input.notifyFinishedPreparation.thresholdForAutoReject,
|
|
150
|
+
workflowBlockerResolvedWebhookUrl:
|
|
151
|
+
readmeConfig.workflowBlockerResolvedWebhookUrl ??
|
|
152
|
+
input.notifyFinishedPreparation.workflowBlockerResolvedWebhookUrl,
|
|
153
|
+
}
|
|
154
|
+
: input.notifyFinishedPreparation,
|
|
155
|
+
};
|
|
156
|
+
|
|
84
157
|
const systemDateRepository = new SystemDateRepository();
|
|
85
158
|
const localStorageRepository = new LocalStorageRepository();
|
|
86
159
|
const googleSpreadsheetRepository = new GoogleSpreadsheetRepository(
|
|
@@ -217,18 +290,18 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
217
290
|
issueRepository,
|
|
218
291
|
);
|
|
219
292
|
|
|
220
|
-
const result = await handleScheduledEventUseCase.run(
|
|
293
|
+
const result = await handleScheduledEventUseCase.run(mergedInput);
|
|
221
294
|
if (result) {
|
|
222
295
|
const projectId = result.project.id;
|
|
223
296
|
const runtimeConfig = {
|
|
224
297
|
resolvedAt: new Date().toISOString(),
|
|
225
298
|
maximumPreparingIssuesCount:
|
|
226
|
-
|
|
299
|
+
mergedInput.startPreparation?.maximumPreparingIssuesCount ?? null,
|
|
227
300
|
utilizationPercentageThreshold:
|
|
228
|
-
|
|
229
|
-
allowIssueCacheMinutes:
|
|
301
|
+
mergedInput.startPreparation?.utilizationPercentageThreshold ?? 90,
|
|
302
|
+
allowIssueCacheMinutes: mergedInput.allowIssueCacheMinutes,
|
|
230
303
|
thresholdForAutoReject:
|
|
231
|
-
|
|
304
|
+
mergedInput.notifyFinishedPreparation?.thresholdForAutoReject ?? 3,
|
|
232
305
|
};
|
|
233
306
|
const finalPath = `${cachePath}/runtimeConfig-${projectId}.json`;
|
|
234
307
|
const tmpPath = `${finalPath}.tmp`;
|
|
@@ -1,29 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
|
|
4
|
-
projectUrl?: string;
|
|
5
|
-
awaitingWorkspaceStatus?: string;
|
|
6
|
-
preparationStatus?: string;
|
|
7
|
-
defaultAgentName?: string;
|
|
8
|
-
defaultLlmModelName?: string;
|
|
9
|
-
defaultLlmAgentName?: string;
|
|
10
|
-
maximumPreparingIssuesCount?: number;
|
|
11
|
-
allowIssueCacheMinutes?: number;
|
|
12
|
-
utilizationPercentageThreshold?: number;
|
|
13
|
-
allowedIssueAuthors?: string;
|
|
14
|
-
awaitingQualityCheckStatus?: string;
|
|
15
|
-
thresholdForAutoReject?: number;
|
|
16
|
-
workflowBlockerResolvedWebhookUrl?: string;
|
|
17
|
-
projectName?: string;
|
|
18
|
-
preparationProcessCheckCommand?: string;
|
|
19
|
-
codexHomeCandidates?: string[];
|
|
20
|
-
awLogDirectoryPath?: string;
|
|
21
|
-
awLogStaleThresholdMinutes?: number;
|
|
22
|
-
};
|
|
23
|
-
export declare const loadConfigFile: (configFilePath: string) => ConfigFile;
|
|
24
|
-
export declare const parseProjectReadmeConfig: (readme: string) => ConfigFile;
|
|
25
|
-
export declare const mergeConfigs: (configFile: ConfigFile, cliOverrides: ConfigFile, readmeOverrides: ConfigFile) => ConfigFile;
|
|
26
|
-
export declare const fetchProjectReadme: (projectUrl: string, token: string) => Promise<string | null>;
|
|
3
|
+
export { ConfigFile, loadConfigFile, parseProjectReadmeConfig, mergeConfigs, fetchProjectReadme, } from './projectConfig';
|
|
27
4
|
export declare const program: Command;
|
|
28
|
-
export {};
|
|
29
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAwEzB,eAAO,MAAM,OAAO,SAAgB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ConfigFile = {
|
|
2
|
+
projectUrl?: string;
|
|
3
|
+
awaitingWorkspaceStatus?: string;
|
|
4
|
+
preparationStatus?: string;
|
|
5
|
+
defaultAgentName?: string;
|
|
6
|
+
defaultLlmModelName?: string;
|
|
7
|
+
defaultLlmAgentName?: string;
|
|
8
|
+
maximumPreparingIssuesCount?: number;
|
|
9
|
+
allowIssueCacheMinutes?: number;
|
|
10
|
+
utilizationPercentageThreshold?: number;
|
|
11
|
+
allowedIssueAuthors?: string;
|
|
12
|
+
awaitingQualityCheckStatus?: string;
|
|
13
|
+
thresholdForAutoReject?: number;
|
|
14
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
15
|
+
projectName?: string;
|
|
16
|
+
preparationProcessCheckCommand?: string;
|
|
17
|
+
codexHomeCandidates?: string[];
|
|
18
|
+
awLogDirectoryPath?: string;
|
|
19
|
+
awLogStaleThresholdMinutes?: number;
|
|
20
|
+
};
|
|
21
|
+
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
22
|
+
export declare const loadConfigFile: (configFilePath: string) => ConfigFile;
|
|
23
|
+
export declare const parseProjectReadmeConfig: (readme: string) => ConfigFile;
|
|
24
|
+
export declare const mergeConfigs: (configFile: ConfigFile, cliOverrides: ConfigFile, readmeOverrides: ConfigFile) => ConfigFile;
|
|
25
|
+
export declare const fetchProjectReadme: (projectUrl: string, token: string) => Promise<string | null>;
|
|
26
|
+
//# sourceMappingURL=projectConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectConfig.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/projectConfig.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iCAAiC,CAAC,EAAE,MAAM,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAoCF,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACH,CAAC;AAEvE,eAAO,MAAM,cAAc,GAAI,gBAAgB,MAAM,KAAG,UAuDvD,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,QAAQ,MAAM,KAAG,UA2DzD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,YAAY,UAAU,EACtB,cAAc,UAAU,EACxB,iBAAiB,UAAU,KAC1B,UAmED,CAAC;AAkBH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,MAAM,EAClB,OAAO,MAAM,KACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAyDvB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAqB3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAoQP;CACH"}
|