npm-cli-gh-issue-preparator 1.21.0 → 1.22.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 +7 -0
- package/README.md +8 -0
- package/bin/adapter/entry-points/cli/index.js +15 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/repositories/FetchWebhookRepository.js +15 -0
- package/bin/adapter/repositories/FetchWebhookRepository.js.map +1 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +25 -1
- 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/config.example.yml +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +96 -0
- package/src/adapter/entry-points/cli/index.ts +33 -1
- package/src/adapter/repositories/FetchWebhookRepository.test.ts +50 -0
- package/src/adapter/repositories/FetchWebhookRepository.ts +12 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +336 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +51 -1
- package/src/domain/usecases/adapter-interfaces/WebhookRepository.ts +3 -0
- package/types/adapter/entry-points/cli/index.d.ts +1 -0
- package/types/adapter/entry-points/cli/index.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/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.22.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.21.0...v1.22.0) (2026-03-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **core:** send webhook notification when workflow blocker issue status changes to awaiting quality check ([1269936](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/1269936add37747b524cc151524c81aadd6c17e8))
|
|
7
|
+
|
|
1
8
|
# [1.21.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.20.0...v1.21.0) (2026-03-09)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -50,6 +50,14 @@ npx npm-cli-gh-issue-preparator startDaemon --configFilePath ./config.yml --proj
|
|
|
50
50
|
npx npm-cli-gh-issue-preparator notifyFinishedIssuePreparation --configFilePath ./config.yml --issueUrl <issueUrl>
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Notify finished issue preparation with webhook notification
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
npx npm-cli-gh-issue-preparator notifyFinishedIssuePreparation --configFilePath ./config.yml --issueUrl <issueUrl> --workflowBlockerResolvedWebhookUrl 'https://example.com/webhook?url={URL}&message={MESSAGE}'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When a workflow blocker issue's status changes to awaiting quality check, a GET request is sent to the specified webhook URL. The `{URL}` and `{MESSAGE}` placeholders are replaced with URL-encoded values.
|
|
60
|
+
|
|
53
61
|
## Contributing
|
|
54
62
|
|
|
55
63
|
See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
@@ -51,6 +51,7 @@ const TowerDefenceProjectRepository_1 = require("../../repositories/TowerDefence
|
|
|
51
51
|
const GitHubIssueCommentRepository_1 = require("../../repositories/GitHubIssueCommentRepository");
|
|
52
52
|
const NodeLocalCommandRunner_1 = require("../../repositories/NodeLocalCommandRunner");
|
|
53
53
|
const OauthAPIClaudeRepository_1 = require("../../repositories/OauthAPIClaudeRepository");
|
|
54
|
+
const FetchWebhookRepository_1 = require("../../repositories/FetchWebhookRepository");
|
|
54
55
|
const getStringValue = (obj, key) => {
|
|
55
56
|
const value = obj[key];
|
|
56
57
|
return typeof value === 'string' ? value : undefined;
|
|
@@ -78,6 +79,7 @@ const loadConfigFile = (configFilePath) => {
|
|
|
78
79
|
allowedIssueAuthors: getStringValue(parsed, 'allowedIssueAuthors'),
|
|
79
80
|
awaitingQualityCheckStatus: getStringValue(parsed, 'awaitingQualityCheckStatus'),
|
|
80
81
|
thresholdForAutoReject: getNumberValue(parsed, 'thresholdForAutoReject'),
|
|
82
|
+
workflowBlockerResolvedWebhookUrl: getStringValue(parsed, 'workflowBlockerResolvedWebhookUrl'),
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
85
|
catch (error) {
|
|
@@ -198,6 +200,7 @@ program
|
|
|
198
200
|
.option('--awaitingWorkspaceStatus <status>', 'Status for issues awaiting workspace')
|
|
199
201
|
.option('--awaitingQualityCheckStatus <status>', 'Status for issues awaiting quality check')
|
|
200
202
|
.option('--thresholdForAutoReject <count>', 'Threshold for auto-escalation after consecutive rejections (default: 3)')
|
|
203
|
+
.option('--workflowBlockerResolvedWebhookUrl <url>', 'Webhook URL to notify when a workflow blocker issue status changes to awaiting quality check. Supports {URL} and {MESSAGE} placeholders.')
|
|
201
204
|
.action(async (options) => {
|
|
202
205
|
const token = process.env.GH_TOKEN;
|
|
203
206
|
if (!token) {
|
|
@@ -225,10 +228,20 @@ program
|
|
|
225
228
|
console.error('awaitingQualityCheckStatus is required. Provide via --awaitingQualityCheckStatus or config file.');
|
|
226
229
|
process.exit(1);
|
|
227
230
|
}
|
|
231
|
+
const workflowBlockerResolvedWebhookUrl = options.workflowBlockerResolvedWebhookUrl ??
|
|
232
|
+
config.workflowBlockerResolvedWebhookUrl ??
|
|
233
|
+
null;
|
|
228
234
|
const projectRepository = new TowerDefenceProjectRepository_1.TowerDefenceProjectRepository(options.configFilePath, token);
|
|
235
|
+
const towerDefenceIssueRepository = new TowerDefenceIssueRepository_1.TowerDefenceIssueRepository(options.configFilePath, token);
|
|
229
236
|
const graphqlIssueRepository = new GraphqlIssueRepository_1.GraphqlIssueRepository(token);
|
|
230
237
|
const issueCommentRepository = new GitHubIssueCommentRepository_1.GitHubIssueCommentRepository(token);
|
|
231
|
-
const
|
|
238
|
+
const webhookRepository = new FetchWebhookRepository_1.FetchWebhookRepository();
|
|
239
|
+
const useCase = new NotifyFinishedIssuePreparationUseCase_1.NotifyFinishedIssuePreparationUseCase(projectRepository, {
|
|
240
|
+
get: graphqlIssueRepository.get.bind(graphqlIssueRepository),
|
|
241
|
+
update: graphqlIssueRepository.update.bind(graphqlIssueRepository),
|
|
242
|
+
findRelatedOpenPRs: graphqlIssueRepository.findRelatedOpenPRs.bind(graphqlIssueRepository),
|
|
243
|
+
getStoryObjectMap: towerDefenceIssueRepository.getStoryObjectMap.bind(towerDefenceIssueRepository),
|
|
244
|
+
}, issueCommentRepository, webhookRepository);
|
|
232
245
|
let thresholdForAutoReject = 3;
|
|
233
246
|
const rawThreshold = options.thresholdForAutoReject ?? config.thresholdForAutoReject;
|
|
234
247
|
if (rawThreshold !== undefined) {
|
|
@@ -248,6 +261,7 @@ program
|
|
|
248
261
|
awaitingWorkspaceStatus,
|
|
249
262
|
awaitingQualityCheckStatus,
|
|
250
263
|
thresholdForAutoReject,
|
|
264
|
+
workflowBlockerResolvedWebhookUrl,
|
|
251
265
|
});
|
|
252
266
|
});
|
|
253
267
|
/* istanbul ignore next */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,oDAA4B;AAC5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,uCAAyB;AACzB,8CAAgC;AAChC,yCAAoC;AACpC,8FAA2F;AAC3F,0HAAuH;AACvH,gGAA6F;AAC7F,sFAAmF;AACnF,oGAAiG;AACjG,kGAA+F;AAC/F,sFAAmF;AACnF,0FAAuF;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FetchWebhookRepository = void 0;
|
|
4
|
+
class FetchWebhookRepository {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.sendGetRequest = async (url) => {
|
|
7
|
+
const response = await fetch(url);
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
throw new Error(`Webhook request failed with status ${response.status}: ${response.statusText}`);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.FetchWebhookRepository = FetchWebhookRepository;
|
|
15
|
+
//# sourceMappingURL=FetchWebhookRepository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FetchWebhookRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/FetchWebhookRepository.ts"],"names":[],"mappings":";;;AAEA,MAAa,sBAAsB;IAAnC;QACE,mBAAc,GAAG,KAAK,EAAE,GAAW,EAAiB,EAAE;YACpD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CACb,sCAAsC,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,UAAU,EAAE,CAChF,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;IACJ,CAAC;CAAA;AATD,wDASC"}
|
|
@@ -16,10 +16,11 @@ class IllegalIssueStatusError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
exports.IllegalIssueStatusError = IllegalIssueStatusError;
|
|
18
18
|
class NotifyFinishedIssuePreparationUseCase {
|
|
19
|
-
constructor(projectRepository, issueRepository, issueCommentRepository) {
|
|
19
|
+
constructor(projectRepository, issueRepository, issueCommentRepository, webhookRepository) {
|
|
20
20
|
this.projectRepository = projectRepository;
|
|
21
21
|
this.issueRepository = issueRepository;
|
|
22
22
|
this.issueCommentRepository = issueCommentRepository;
|
|
23
|
+
this.webhookRepository = webhookRepository;
|
|
23
24
|
this.run = async (params) => {
|
|
24
25
|
let project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
25
26
|
project = await this.projectRepository.prepareStatus(params.preparationStatus, project);
|
|
@@ -39,6 +40,7 @@ class NotifyFinishedIssuePreparationUseCase {
|
|
|
39
40
|
issue.status = params.awaitingQualityCheckStatus;
|
|
40
41
|
await this.issueRepository.update(issue, project);
|
|
41
42
|
await this.issueCommentRepository.createComment(issue, `Failed to pass the check autimatically for ${params.thresholdForAutoReject} times`);
|
|
43
|
+
await this.sendWorkflowBlockerNotification(params.issueUrl, params.workflowBlockerResolvedWebhookUrl, project);
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
const rejections = [];
|
|
@@ -101,12 +103,34 @@ class NotifyFinishedIssuePreparationUseCase {
|
|
|
101
103
|
if (rejections.length <= 0) {
|
|
102
104
|
issue.status = params.awaitingQualityCheckStatus;
|
|
103
105
|
await this.issueRepository.update(issue, project);
|
|
106
|
+
await this.sendWorkflowBlockerNotification(params.issueUrl, params.workflowBlockerResolvedWebhookUrl, project);
|
|
104
107
|
return;
|
|
105
108
|
}
|
|
106
109
|
issue.status = params.awaitingWorkspaceStatus;
|
|
107
110
|
await this.issueRepository.update(issue, project);
|
|
108
111
|
await this.issueCommentRepository.createComment(issue, `Auto Status Check: REJECTED\n${rejections.map((r) => `- ${r.detail}`).join('\n')}`);
|
|
109
112
|
};
|
|
113
|
+
this.sendWorkflowBlockerNotification = async (issueUrl, webhookUrlTemplate, project) => {
|
|
114
|
+
if (webhookUrlTemplate === null) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const storyObjectMap = await this.issueRepository.getStoryObjectMap(project);
|
|
119
|
+
const isWorkflowBlocker = Array.from(storyObjectMap.entries()).some(([storyName, storyObject]) => storyName.toLowerCase().includes('workflow blocker') &&
|
|
120
|
+
storyObject.issues.some((issue) => issue.url === issueUrl));
|
|
121
|
+
if (!isWorkflowBlocker) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const message = `Workflow blocker resolved: ${issueUrl}`;
|
|
125
|
+
const webhookUrl = webhookUrlTemplate
|
|
126
|
+
.replace('{URL}', encodeURIComponent(issueUrl))
|
|
127
|
+
.replace('{MESSAGE}', encodeURIComponent(message));
|
|
128
|
+
await this.webhookRepository.sendGetRequest(webhookUrl);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.warn('Failed to send workflow blocker notification:', error);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
exports.NotifyFinishedIssuePreparationUseCase = NotifyFinishedIssuePreparationUseCase;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotifyFinishedIssuePreparationUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"NotifyFinishedIssuePreparationUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebhookRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/WebhookRepository.ts"],"names":[],"mappings":""}
|
package/config.example.yml
CHANGED
package/package.json
CHANGED
|
@@ -29,6 +29,11 @@ jest.mock('../../repositories/OauthAPIClaudeRepository', () => ({
|
|
|
29
29
|
isClaudeAvailable: jest.fn(),
|
|
30
30
|
})),
|
|
31
31
|
}));
|
|
32
|
+
jest.mock('../../repositories/FetchWebhookRepository', () => ({
|
|
33
|
+
FetchWebhookRepository: jest.fn().mockImplementation(() => ({
|
|
34
|
+
sendGetRequest: jest.fn(),
|
|
35
|
+
})),
|
|
36
|
+
}));
|
|
32
37
|
|
|
33
38
|
describe('CLI', () => {
|
|
34
39
|
const originalEnv = process.env;
|
|
@@ -90,6 +95,7 @@ describe('CLI', () => {
|
|
|
90
95
|
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
91
96
|
thresholdForAutoReject: 5,
|
|
92
97
|
allowedIssueAuthors: 'user1,user2',
|
|
98
|
+
workflowBlockerResolvedWebhookUrl: 'https://example.com/webhook',
|
|
93
99
|
};
|
|
94
100
|
writeConfig(config);
|
|
95
101
|
|
|
@@ -1073,6 +1079,7 @@ describe('CLI', () => {
|
|
|
1073
1079
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
1074
1080
|
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
1075
1081
|
thresholdForAutoReject: 3,
|
|
1082
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1076
1083
|
});
|
|
1077
1084
|
});
|
|
1078
1085
|
|
|
@@ -1111,6 +1118,7 @@ describe('CLI', () => {
|
|
|
1111
1118
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
1112
1119
|
awaitingQualityCheckStatus: 'Override QC',
|
|
1113
1120
|
thresholdForAutoReject: 3,
|
|
1121
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1114
1122
|
});
|
|
1115
1123
|
});
|
|
1116
1124
|
|
|
@@ -1151,6 +1159,7 @@ describe('CLI', () => {
|
|
|
1151
1159
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
1152
1160
|
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
1153
1161
|
thresholdForAutoReject: 5,
|
|
1162
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1154
1163
|
});
|
|
1155
1164
|
});
|
|
1156
1165
|
|
|
@@ -1193,6 +1202,7 @@ describe('CLI', () => {
|
|
|
1193
1202
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
1194
1203
|
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
1195
1204
|
thresholdForAutoReject: 7,
|
|
1205
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1196
1206
|
});
|
|
1197
1207
|
});
|
|
1198
1208
|
|
|
@@ -1400,5 +1410,91 @@ describe('CLI', () => {
|
|
|
1400
1410
|
consoleErrorSpy.mockRestore();
|
|
1401
1411
|
processExitSpy.mockRestore();
|
|
1402
1412
|
});
|
|
1413
|
+
|
|
1414
|
+
it('should pass workflowBlockerResolvedWebhookUrl from config file', async () => {
|
|
1415
|
+
const configWithWebhook = {
|
|
1416
|
+
...defaultConfig,
|
|
1417
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1418
|
+
'https://example.com/webhook?url={URL}',
|
|
1419
|
+
};
|
|
1420
|
+
writeConfig(configWithWebhook);
|
|
1421
|
+
|
|
1422
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
1423
|
+
const MockedNotifyFinishedUseCase = jest.mocked(
|
|
1424
|
+
NotifyFinishedIssuePreparationUseCase,
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
MockedNotifyFinishedUseCase.mockImplementation(function (
|
|
1428
|
+
this: NotifyFinishedIssuePreparationUseCase,
|
|
1429
|
+
) {
|
|
1430
|
+
this.run = mockRun;
|
|
1431
|
+
return this;
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
await program.parseAsync([
|
|
1435
|
+
'node',
|
|
1436
|
+
'test',
|
|
1437
|
+
'notifyFinishedIssuePreparation',
|
|
1438
|
+
'--configFilePath',
|
|
1439
|
+
configFilePath,
|
|
1440
|
+
'--issueUrl',
|
|
1441
|
+
'https://github.com/test/issue/1',
|
|
1442
|
+
]);
|
|
1443
|
+
|
|
1444
|
+
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
1445
|
+
expect(mockRun).toHaveBeenCalledWith({
|
|
1446
|
+
projectUrl: 'https://github.com/test/project',
|
|
1447
|
+
issueUrl: 'https://github.com/test/issue/1',
|
|
1448
|
+
preparationStatus: 'Preparing',
|
|
1449
|
+
awaitingWorkspaceStatus: 'Awaiting',
|
|
1450
|
+
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
1451
|
+
thresholdForAutoReject: 3,
|
|
1452
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1453
|
+
'https://example.com/webhook?url={URL}',
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
it('should pass workflowBlockerResolvedWebhookUrl from CLI overriding config', async () => {
|
|
1458
|
+
const configWithWebhook = {
|
|
1459
|
+
...defaultConfig,
|
|
1460
|
+
workflowBlockerResolvedWebhookUrl: 'https://example.com/config-webhook',
|
|
1461
|
+
};
|
|
1462
|
+
writeConfig(configWithWebhook);
|
|
1463
|
+
|
|
1464
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
1465
|
+
const MockedNotifyFinishedUseCase = jest.mocked(
|
|
1466
|
+
NotifyFinishedIssuePreparationUseCase,
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
MockedNotifyFinishedUseCase.mockImplementation(function (
|
|
1470
|
+
this: NotifyFinishedIssuePreparationUseCase,
|
|
1471
|
+
) {
|
|
1472
|
+
this.run = mockRun;
|
|
1473
|
+
return this;
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
await program.parseAsync([
|
|
1477
|
+
'node',
|
|
1478
|
+
'test',
|
|
1479
|
+
'notifyFinishedIssuePreparation',
|
|
1480
|
+
'--configFilePath',
|
|
1481
|
+
configFilePath,
|
|
1482
|
+
'--issueUrl',
|
|
1483
|
+
'https://github.com/test/issue/1',
|
|
1484
|
+
'--workflowBlockerResolvedWebhookUrl',
|
|
1485
|
+
'https://example.com/cli-webhook',
|
|
1486
|
+
]);
|
|
1487
|
+
|
|
1488
|
+
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
1489
|
+
expect(mockRun).toHaveBeenCalledWith({
|
|
1490
|
+
projectUrl: 'https://github.com/test/project',
|
|
1491
|
+
issueUrl: 'https://github.com/test/issue/1',
|
|
1492
|
+
preparationStatus: 'Preparing',
|
|
1493
|
+
awaitingWorkspaceStatus: 'Awaiting',
|
|
1494
|
+
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
1495
|
+
thresholdForAutoReject: 3,
|
|
1496
|
+
workflowBlockerResolvedWebhookUrl: 'https://example.com/cli-webhook',
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1403
1499
|
});
|
|
1404
1500
|
});
|
|
@@ -13,6 +13,7 @@ import { TowerDefenceProjectRepository } from '../../repositories/TowerDefencePr
|
|
|
13
13
|
import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
|
|
14
14
|
import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
|
|
15
15
|
import { OauthAPIClaudeRepository } from '../../repositories/OauthAPIClaudeRepository';
|
|
16
|
+
import { FetchWebhookRepository } from '../../repositories/FetchWebhookRepository';
|
|
16
17
|
|
|
17
18
|
type ConfigFile = {
|
|
18
19
|
projectUrl?: string;
|
|
@@ -25,6 +26,7 @@ type ConfigFile = {
|
|
|
25
26
|
allowedIssueAuthors?: string;
|
|
26
27
|
awaitingQualityCheckStatus?: string;
|
|
27
28
|
thresholdForAutoReject?: number;
|
|
29
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
type StartDaemonOptions = {
|
|
@@ -46,6 +48,7 @@ type NotifyFinishedOptions = {
|
|
|
46
48
|
awaitingWorkspaceStatus?: string;
|
|
47
49
|
awaitingQualityCheckStatus?: string;
|
|
48
50
|
thresholdForAutoReject?: string;
|
|
51
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
49
52
|
configFilePath: string;
|
|
50
53
|
};
|
|
51
54
|
|
|
@@ -98,6 +101,10 @@ const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
|
98
101
|
'awaitingQualityCheckStatus',
|
|
99
102
|
),
|
|
100
103
|
thresholdForAutoReject: getNumberValue(parsed, 'thresholdForAutoReject'),
|
|
104
|
+
workflowBlockerResolvedWebhookUrl: getStringValue(
|
|
105
|
+
parsed,
|
|
106
|
+
'workflowBlockerResolvedWebhookUrl',
|
|
107
|
+
),
|
|
101
108
|
};
|
|
102
109
|
} catch (error) {
|
|
103
110
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -294,6 +301,10 @@ program
|
|
|
294
301
|
'--thresholdForAutoReject <count>',
|
|
295
302
|
'Threshold for auto-escalation after consecutive rejections (default: 3)',
|
|
296
303
|
)
|
|
304
|
+
.option(
|
|
305
|
+
'--workflowBlockerResolvedWebhookUrl <url>',
|
|
306
|
+
'Webhook URL to notify when a workflow blocker issue status changes to awaiting quality check. Supports {URL} and {MESSAGE} placeholders.',
|
|
307
|
+
)
|
|
297
308
|
.action(async (options: NotifyFinishedOptions) => {
|
|
298
309
|
const token = process.env.GH_TOKEN;
|
|
299
310
|
if (!token) {
|
|
@@ -336,17 +347,37 @@ program
|
|
|
336
347
|
process.exit(1);
|
|
337
348
|
}
|
|
338
349
|
|
|
350
|
+
const workflowBlockerResolvedWebhookUrl: string | null =
|
|
351
|
+
options.workflowBlockerResolvedWebhookUrl ??
|
|
352
|
+
config.workflowBlockerResolvedWebhookUrl ??
|
|
353
|
+
null;
|
|
354
|
+
|
|
339
355
|
const projectRepository = new TowerDefenceProjectRepository(
|
|
340
356
|
options.configFilePath,
|
|
341
357
|
token,
|
|
342
358
|
);
|
|
359
|
+
const towerDefenceIssueRepository = new TowerDefenceIssueRepository(
|
|
360
|
+
options.configFilePath,
|
|
361
|
+
token,
|
|
362
|
+
);
|
|
343
363
|
const graphqlIssueRepository = new GraphqlIssueRepository(token);
|
|
344
364
|
const issueCommentRepository = new GitHubIssueCommentRepository(token);
|
|
365
|
+
const webhookRepository = new FetchWebhookRepository();
|
|
345
366
|
|
|
346
367
|
const useCase = new NotifyFinishedIssuePreparationUseCase(
|
|
347
368
|
projectRepository,
|
|
348
|
-
|
|
369
|
+
{
|
|
370
|
+
get: graphqlIssueRepository.get.bind(graphqlIssueRepository),
|
|
371
|
+
update: graphqlIssueRepository.update.bind(graphqlIssueRepository),
|
|
372
|
+
findRelatedOpenPRs: graphqlIssueRepository.findRelatedOpenPRs.bind(
|
|
373
|
+
graphqlIssueRepository,
|
|
374
|
+
),
|
|
375
|
+
getStoryObjectMap: towerDefenceIssueRepository.getStoryObjectMap.bind(
|
|
376
|
+
towerDefenceIssueRepository,
|
|
377
|
+
),
|
|
378
|
+
},
|
|
349
379
|
issueCommentRepository,
|
|
380
|
+
webhookRepository,
|
|
350
381
|
);
|
|
351
382
|
|
|
352
383
|
let thresholdForAutoReject = 3;
|
|
@@ -374,6 +405,7 @@ program
|
|
|
374
405
|
awaitingWorkspaceStatus,
|
|
375
406
|
awaitingQualityCheckStatus,
|
|
376
407
|
thresholdForAutoReject,
|
|
408
|
+
workflowBlockerResolvedWebhookUrl,
|
|
377
409
|
});
|
|
378
410
|
});
|
|
379
411
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { FetchWebhookRepository } from './FetchWebhookRepository';
|
|
2
|
+
|
|
3
|
+
describe('FetchWebhookRepository', () => {
|
|
4
|
+
let repository: FetchWebhookRepository;
|
|
5
|
+
const originalFetch = global.fetch;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
repository = new FetchWebhookRepository();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
global.fetch = originalFetch;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should send GET request to the provided URL', async () => {
|
|
16
|
+
const mockFetch = jest.fn().mockResolvedValue({
|
|
17
|
+
ok: true,
|
|
18
|
+
status: 200,
|
|
19
|
+
});
|
|
20
|
+
global.fetch = mockFetch;
|
|
21
|
+
|
|
22
|
+
await repository.sendGetRequest('https://example.com/webhook');
|
|
23
|
+
|
|
24
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/webhook');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should throw error when response is not ok', async () => {
|
|
28
|
+
const mockFetch = jest.fn().mockResolvedValue({
|
|
29
|
+
ok: false,
|
|
30
|
+
status: 500,
|
|
31
|
+
statusText: 'Internal Server Error',
|
|
32
|
+
});
|
|
33
|
+
global.fetch = mockFetch;
|
|
34
|
+
|
|
35
|
+
await expect(
|
|
36
|
+
repository.sendGetRequest('https://example.com/webhook'),
|
|
37
|
+
).rejects.toThrow(
|
|
38
|
+
'Webhook request failed with status 500: Internal Server Error',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should propagate network errors', async () => {
|
|
43
|
+
const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'));
|
|
44
|
+
global.fetch = mockFetch;
|
|
45
|
+
|
|
46
|
+
await expect(
|
|
47
|
+
repository.sendGetRequest('https://example.com/webhook'),
|
|
48
|
+
).rejects.toThrow('Network error');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { WebhookRepository } from '../../domain/usecases/adapter-interfaces/WebhookRepository';
|
|
2
|
+
|
|
3
|
+
export class FetchWebhookRepository implements WebhookRepository {
|
|
4
|
+
sendGetRequest = async (url: string): Promise<void> => {
|
|
5
|
+
const response = await fetch(url);
|
|
6
|
+
if (!response.ok) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`Webhook request failed with status ${response.status}: ${response.statusText}`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -2,9 +2,11 @@ import { NotifyFinishedIssuePreparationUseCase } from './NotifyFinishedIssuePrep
|
|
|
2
2
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
3
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
4
|
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
5
|
+
import { WebhookRepository } from './adapter-interfaces/WebhookRepository';
|
|
5
6
|
import { Issue } from '../entities/Issue';
|
|
6
7
|
import { Project } from '../entities/Project';
|
|
7
8
|
import { Comment } from '../entities/Comment';
|
|
9
|
+
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
8
10
|
|
|
9
11
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
10
12
|
|
|
@@ -66,6 +68,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
66
68
|
let mockProjectRepository: Mocked<ProjectRepository>;
|
|
67
69
|
let mockIssueRepository: Mocked<IssueRepository>;
|
|
68
70
|
let mockIssueCommentRepository: Mocked<IssueCommentRepository>;
|
|
71
|
+
let mockWebhookRepository: Mocked<WebhookRepository>;
|
|
69
72
|
let mockProject: Project;
|
|
70
73
|
|
|
71
74
|
beforeEach(() => {
|
|
@@ -96,10 +99,15 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
96
99
|
createComment: jest.fn(),
|
|
97
100
|
};
|
|
98
101
|
|
|
102
|
+
mockWebhookRepository = {
|
|
103
|
+
sendGetRequest: jest.fn(),
|
|
104
|
+
};
|
|
105
|
+
|
|
99
106
|
useCase = new NotifyFinishedIssuePreparationUseCase(
|
|
100
107
|
mockProjectRepository,
|
|
101
108
|
mockIssueRepository,
|
|
102
109
|
mockIssueCommentRepository,
|
|
110
|
+
mockWebhookRepository,
|
|
103
111
|
);
|
|
104
112
|
});
|
|
105
113
|
|
|
@@ -140,6 +148,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
140
148
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
141
149
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
142
150
|
thresholdForAutoReject: 3,
|
|
151
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
143
152
|
});
|
|
144
153
|
|
|
145
154
|
expect(mockProjectRepository.prepareStatus).toHaveBeenCalledTimes(3);
|
|
@@ -190,6 +199,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
190
199
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
191
200
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
192
201
|
thresholdForAutoReject: 3,
|
|
202
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
193
203
|
});
|
|
194
204
|
|
|
195
205
|
expect(mockIssueRepository.update).toHaveBeenCalledTimes(1);
|
|
@@ -214,6 +224,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
214
224
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
215
225
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
216
226
|
thresholdForAutoReject: 3,
|
|
227
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
217
228
|
}),
|
|
218
229
|
).rejects.toThrow(
|
|
219
230
|
'Issue not found: https://github.com/user/repo/issues/999',
|
|
@@ -237,6 +248,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
237
248
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
238
249
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
239
250
|
thresholdForAutoReject: 3,
|
|
251
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
240
252
|
}),
|
|
241
253
|
).rejects.toThrow(
|
|
242
254
|
'Illegal issue status for https://github.com/user/repo/issues/1: expected Preparation, but got Done',
|
|
@@ -275,6 +287,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
275
287
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
276
288
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
277
289
|
thresholdForAutoReject: 3,
|
|
290
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
278
291
|
});
|
|
279
292
|
|
|
280
293
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -321,6 +334,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
321
334
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
322
335
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
323
336
|
thresholdForAutoReject: 3,
|
|
337
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
324
338
|
});
|
|
325
339
|
|
|
326
340
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -365,6 +379,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
365
379
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
366
380
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
367
381
|
thresholdForAutoReject: 3,
|
|
382
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
368
383
|
});
|
|
369
384
|
|
|
370
385
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -397,6 +412,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
397
412
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
398
413
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
399
414
|
thresholdForAutoReject: 3,
|
|
415
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
400
416
|
});
|
|
401
417
|
|
|
402
418
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -446,6 +462,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
446
462
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
447
463
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
448
464
|
thresholdForAutoReject: 3,
|
|
465
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
449
466
|
});
|
|
450
467
|
|
|
451
468
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -489,6 +506,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
489
506
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
490
507
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
491
508
|
thresholdForAutoReject: 3,
|
|
509
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
492
510
|
});
|
|
493
511
|
|
|
494
512
|
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
@@ -532,6 +550,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
532
550
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
533
551
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
534
552
|
thresholdForAutoReject: 3,
|
|
553
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
535
554
|
});
|
|
536
555
|
|
|
537
556
|
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
@@ -562,6 +581,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
562
581
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
563
582
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
564
583
|
thresholdForAutoReject: 3,
|
|
584
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
565
585
|
});
|
|
566
586
|
|
|
567
587
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -617,6 +637,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
617
637
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
618
638
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
619
639
|
thresholdForAutoReject: 3,
|
|
640
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
620
641
|
});
|
|
621
642
|
|
|
622
643
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -663,6 +684,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
663
684
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
664
685
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
665
686
|
thresholdForAutoReject: 3,
|
|
687
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
666
688
|
});
|
|
667
689
|
|
|
668
690
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -709,6 +731,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
709
731
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
710
732
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
711
733
|
thresholdForAutoReject: 3,
|
|
734
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
712
735
|
});
|
|
713
736
|
|
|
714
737
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -755,6 +778,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
755
778
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
756
779
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
757
780
|
thresholdForAutoReject: 3,
|
|
781
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
758
782
|
});
|
|
759
783
|
|
|
760
784
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -807,6 +831,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
807
831
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
808
832
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
809
833
|
thresholdForAutoReject: 3,
|
|
834
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
810
835
|
});
|
|
811
836
|
|
|
812
837
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -859,6 +884,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
859
884
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
860
885
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
861
886
|
thresholdForAutoReject: 3,
|
|
887
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
862
888
|
});
|
|
863
889
|
|
|
864
890
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
@@ -899,6 +925,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
899
925
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
900
926
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
901
927
|
thresholdForAutoReject: 3,
|
|
928
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
902
929
|
});
|
|
903
930
|
|
|
904
931
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -935,6 +962,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
935
962
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
936
963
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
937
964
|
thresholdForAutoReject: 3,
|
|
965
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
938
966
|
});
|
|
939
967
|
|
|
940
968
|
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
@@ -977,6 +1005,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
977
1005
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
978
1006
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
979
1007
|
thresholdForAutoReject: 3,
|
|
1008
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
980
1009
|
});
|
|
981
1010
|
|
|
982
1011
|
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
@@ -1010,6 +1039,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1010
1039
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1011
1040
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1012
1041
|
thresholdForAutoReject: 3,
|
|
1042
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1013
1043
|
});
|
|
1014
1044
|
|
|
1015
1045
|
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
@@ -1026,4 +1056,310 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1026
1056
|
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
1027
1057
|
);
|
|
1028
1058
|
});
|
|
1059
|
+
|
|
1060
|
+
describe('workflow blocker webhook notification', () => {
|
|
1061
|
+
const createWorkflowBlockerStoryObjectMap = (
|
|
1062
|
+
issueUrl: string,
|
|
1063
|
+
): StoryObjectMap => {
|
|
1064
|
+
const map: StoryObjectMap = new Map();
|
|
1065
|
+
map.set('Workflow Blocker Story', {
|
|
1066
|
+
story: {
|
|
1067
|
+
id: 'story-1',
|
|
1068
|
+
name: 'Workflow Blocker Story',
|
|
1069
|
+
color: 'GRAY',
|
|
1070
|
+
description: '',
|
|
1071
|
+
},
|
|
1072
|
+
storyIssue: null,
|
|
1073
|
+
issues: [createMockIssue({ url: issueUrl })],
|
|
1074
|
+
});
|
|
1075
|
+
return map;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const createNonBlockerStoryObjectMap = (): StoryObjectMap => {
|
|
1079
|
+
const map: StoryObjectMap = new Map();
|
|
1080
|
+
map.set('Regular Story', {
|
|
1081
|
+
story: {
|
|
1082
|
+
id: 'story-2',
|
|
1083
|
+
name: 'Regular Story',
|
|
1084
|
+
color: 'GRAY',
|
|
1085
|
+
description: '',
|
|
1086
|
+
},
|
|
1087
|
+
storyIssue: null,
|
|
1088
|
+
issues: [
|
|
1089
|
+
createMockIssue({
|
|
1090
|
+
url: 'https://github.com/user/repo/issues/99',
|
|
1091
|
+
}),
|
|
1092
|
+
],
|
|
1093
|
+
});
|
|
1094
|
+
return map;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
it('should send webhook when workflow blocker issue status changes to awaitingQualityCheckStatus on checks pass', async () => {
|
|
1098
|
+
const issue = createMockIssue({
|
|
1099
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1100
|
+
status: 'Preparation',
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1104
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1105
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1106
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1107
|
+
]);
|
|
1108
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1109
|
+
{
|
|
1110
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1111
|
+
isConflicted: false,
|
|
1112
|
+
isPassedAllCiJob: true,
|
|
1113
|
+
isCiStateSuccess: true,
|
|
1114
|
+
isResolvedAllReviewComments: true,
|
|
1115
|
+
isBranchOutOfDate: false,
|
|
1116
|
+
missingRequiredCheckNames: [],
|
|
1117
|
+
},
|
|
1118
|
+
]);
|
|
1119
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1120
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1121
|
+
'https://github.com/user/repo/issues/1',
|
|
1122
|
+
),
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
await useCase.run({
|
|
1126
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1127
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1128
|
+
preparationStatus: 'Preparation',
|
|
1129
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1130
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1131
|
+
thresholdForAutoReject: 3,
|
|
1132
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1133
|
+
'https://example.com/webhook?url={URL}&msg={MESSAGE}',
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledWith(
|
|
1137
|
+
`https://example.com/webhook?url=${encodeURIComponent('https://github.com/user/repo/issues/1')}&msg=${encodeURIComponent('Workflow blocker resolved: https://github.com/user/repo/issues/1')}`,
|
|
1138
|
+
);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it('should send webhook when workflow blocker issue auto-escalates', async () => {
|
|
1142
|
+
const issue = createMockIssue({
|
|
1143
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1144
|
+
status: 'Preparation',
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1148
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1149
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1150
|
+
createMockComment({
|
|
1151
|
+
content: 'Auto Status Check: REJECTED - first',
|
|
1152
|
+
}),
|
|
1153
|
+
createMockComment({
|
|
1154
|
+
content: 'Auto Status Check: REJECTED - second',
|
|
1155
|
+
}),
|
|
1156
|
+
createMockComment({
|
|
1157
|
+
content: 'Auto Status Check: REJECTED - third',
|
|
1158
|
+
}),
|
|
1159
|
+
]);
|
|
1160
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1161
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1162
|
+
'https://github.com/user/repo/issues/1',
|
|
1163
|
+
),
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
await useCase.run({
|
|
1167
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1168
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1169
|
+
preparationStatus: 'Preparation',
|
|
1170
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1171
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1172
|
+
thresholdForAutoReject: 3,
|
|
1173
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1174
|
+
'https://example.com/notify={MESSAGE}',
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledTimes(1);
|
|
1178
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledWith(
|
|
1179
|
+
expect.stringContaining('https://example.com/notify='),
|
|
1180
|
+
);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('should not send webhook for non-blocker issues', async () => {
|
|
1184
|
+
const issue = createMockIssue({
|
|
1185
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1186
|
+
status: 'Preparation',
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1190
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1191
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1192
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1193
|
+
]);
|
|
1194
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1195
|
+
{
|
|
1196
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1197
|
+
isConflicted: false,
|
|
1198
|
+
isPassedAllCiJob: true,
|
|
1199
|
+
isCiStateSuccess: true,
|
|
1200
|
+
isResolvedAllReviewComments: true,
|
|
1201
|
+
isBranchOutOfDate: false,
|
|
1202
|
+
missingRequiredCheckNames: [],
|
|
1203
|
+
},
|
|
1204
|
+
]);
|
|
1205
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1206
|
+
createNonBlockerStoryObjectMap(),
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
await useCase.run({
|
|
1210
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1211
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1212
|
+
preparationStatus: 'Preparation',
|
|
1213
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1214
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1215
|
+
thresholdForAutoReject: 3,
|
|
1216
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1217
|
+
'https://example.com/webhook?msg={MESSAGE}',
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalled();
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
it('should not send webhook when URL is null', async () => {
|
|
1224
|
+
const issue = createMockIssue({
|
|
1225
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1226
|
+
status: 'Preparation',
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1230
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1231
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1232
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1233
|
+
]);
|
|
1234
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1235
|
+
{
|
|
1236
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1237
|
+
isConflicted: false,
|
|
1238
|
+
isPassedAllCiJob: true,
|
|
1239
|
+
isCiStateSuccess: true,
|
|
1240
|
+
isResolvedAllReviewComments: true,
|
|
1241
|
+
isBranchOutOfDate: false,
|
|
1242
|
+
missingRequiredCheckNames: [],
|
|
1243
|
+
},
|
|
1244
|
+
]);
|
|
1245
|
+
|
|
1246
|
+
await useCase.run({
|
|
1247
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1248
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1249
|
+
preparationStatus: 'Preparation',
|
|
1250
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1251
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1252
|
+
thresholdForAutoReject: 3,
|
|
1253
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
expect(mockIssueRepository.getStoryObjectMap).not.toHaveBeenCalled();
|
|
1257
|
+
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalled();
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
it('should log warning and not block workflow when webhook fails', async () => {
|
|
1261
|
+
const issue = createMockIssue({
|
|
1262
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1263
|
+
status: 'Preparation',
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1267
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1268
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1269
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1270
|
+
]);
|
|
1271
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1272
|
+
{
|
|
1273
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1274
|
+
isConflicted: false,
|
|
1275
|
+
isPassedAllCiJob: true,
|
|
1276
|
+
isCiStateSuccess: true,
|
|
1277
|
+
isResolvedAllReviewComments: true,
|
|
1278
|
+
isBranchOutOfDate: false,
|
|
1279
|
+
missingRequiredCheckNames: [],
|
|
1280
|
+
},
|
|
1281
|
+
]);
|
|
1282
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1283
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1284
|
+
'https://github.com/user/repo/issues/1',
|
|
1285
|
+
),
|
|
1286
|
+
);
|
|
1287
|
+
mockWebhookRepository.sendGetRequest.mockRejectedValue(
|
|
1288
|
+
new Error('Network error'),
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
1292
|
+
|
|
1293
|
+
await useCase.run({
|
|
1294
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1295
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1296
|
+
preparationStatus: 'Preparation',
|
|
1297
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1298
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1299
|
+
thresholdForAutoReject: 3,
|
|
1300
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1301
|
+
'https://example.com/webhook?msg={MESSAGE}',
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
1305
|
+
'Failed to send workflow blocker notification:',
|
|
1306
|
+
expect.any(Error),
|
|
1307
|
+
);
|
|
1308
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1309
|
+
expect.objectContaining({
|
|
1310
|
+
status: 'Awaiting Quality Check',
|
|
1311
|
+
}),
|
|
1312
|
+
mockProject,
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
consoleWarnSpy.mockRestore();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('should URL-encode placeholders in webhook URL', async () => {
|
|
1319
|
+
const issue = createMockIssue({
|
|
1320
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1321
|
+
status: 'Preparation',
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1325
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1326
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1327
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1328
|
+
]);
|
|
1329
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1330
|
+
{
|
|
1331
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1332
|
+
isConflicted: false,
|
|
1333
|
+
isPassedAllCiJob: true,
|
|
1334
|
+
isCiStateSuccess: true,
|
|
1335
|
+
isResolvedAllReviewComments: true,
|
|
1336
|
+
isBranchOutOfDate: false,
|
|
1337
|
+
missingRequiredCheckNames: [],
|
|
1338
|
+
},
|
|
1339
|
+
]);
|
|
1340
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1341
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1342
|
+
'https://github.com/user/repo/issues/1',
|
|
1343
|
+
),
|
|
1344
|
+
);
|
|
1345
|
+
|
|
1346
|
+
await useCase.run({
|
|
1347
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1348
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1349
|
+
preparationStatus: 'Preparation',
|
|
1350
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1351
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1352
|
+
thresholdForAutoReject: 3,
|
|
1353
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1354
|
+
'https://example.com/runTasker/notify=:={MESSAGE}',
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
const calledUrl = mockWebhookRepository.sendGetRequest.mock.calls[0][0];
|
|
1358
|
+
expect(calledUrl).not.toContain('{MESSAGE}');
|
|
1359
|
+
expect(calledUrl).not.toContain('{URL}');
|
|
1360
|
+
expect(calledUrl).toContain(
|
|
1361
|
+
encodeURIComponent('Workflow blocker resolved:'),
|
|
1362
|
+
);
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1029
1365
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
2
2
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
3
|
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
4
|
+
import { WebhookRepository } from './adapter-interfaces/WebhookRepository';
|
|
4
5
|
|
|
5
6
|
export class IssueNotFoundError extends Error {
|
|
6
7
|
constructor(issueUrl: string) {
|
|
@@ -37,12 +38,16 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
37
38
|
>,
|
|
38
39
|
private readonly issueRepository: Pick<
|
|
39
40
|
IssueRepository,
|
|
40
|
-
'get' | 'update' | 'findRelatedOpenPRs'
|
|
41
|
+
'get' | 'update' | 'findRelatedOpenPRs' | 'getStoryObjectMap'
|
|
41
42
|
>,
|
|
42
43
|
private readonly issueCommentRepository: Pick<
|
|
43
44
|
IssueCommentRepository,
|
|
44
45
|
'getCommentsFromIssue' | 'createComment'
|
|
45
46
|
>,
|
|
47
|
+
private readonly webhookRepository: Pick<
|
|
48
|
+
WebhookRepository,
|
|
49
|
+
'sendGetRequest'
|
|
50
|
+
>,
|
|
46
51
|
) {}
|
|
47
52
|
|
|
48
53
|
run = async (params: {
|
|
@@ -52,6 +57,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
52
57
|
awaitingWorkspaceStatus: string;
|
|
53
58
|
awaitingQualityCheckStatus: string;
|
|
54
59
|
thresholdForAutoReject: number;
|
|
60
|
+
workflowBlockerResolvedWebhookUrl: string | null;
|
|
55
61
|
}): Promise<void> => {
|
|
56
62
|
let project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
57
63
|
project = await this.projectRepository.prepareStatus(
|
|
@@ -98,6 +104,11 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
98
104
|
issue,
|
|
99
105
|
`Failed to pass the check autimatically for ${params.thresholdForAutoReject} times`,
|
|
100
106
|
);
|
|
107
|
+
await this.sendWorkflowBlockerNotification(
|
|
108
|
+
params.issueUrl,
|
|
109
|
+
params.workflowBlockerResolvedWebhookUrl,
|
|
110
|
+
project,
|
|
111
|
+
);
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -165,6 +176,11 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
165
176
|
if (rejections.length <= 0) {
|
|
166
177
|
issue.status = params.awaitingQualityCheckStatus;
|
|
167
178
|
await this.issueRepository.update(issue, project);
|
|
179
|
+
await this.sendWorkflowBlockerNotification(
|
|
180
|
+
params.issueUrl,
|
|
181
|
+
params.workflowBlockerResolvedWebhookUrl,
|
|
182
|
+
project,
|
|
183
|
+
);
|
|
168
184
|
return;
|
|
169
185
|
}
|
|
170
186
|
|
|
@@ -176,4 +192,38 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
176
192
|
`Auto Status Check: REJECTED\n${rejections.map((r) => `- ${r.detail}`).join('\n')}`,
|
|
177
193
|
);
|
|
178
194
|
};
|
|
195
|
+
|
|
196
|
+
private sendWorkflowBlockerNotification = async (
|
|
197
|
+
issueUrl: string,
|
|
198
|
+
webhookUrlTemplate: string | null,
|
|
199
|
+
project: Parameters<IssueRepository['getStoryObjectMap']>[0],
|
|
200
|
+
): Promise<void> => {
|
|
201
|
+
if (webhookUrlTemplate === null) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const storyObjectMap =
|
|
207
|
+
await this.issueRepository.getStoryObjectMap(project);
|
|
208
|
+
|
|
209
|
+
const isWorkflowBlocker = Array.from(storyObjectMap.entries()).some(
|
|
210
|
+
([storyName, storyObject]) =>
|
|
211
|
+
storyName.toLowerCase().includes('workflow blocker') &&
|
|
212
|
+
storyObject.issues.some((issue) => issue.url === issueUrl),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (!isWorkflowBlocker) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const message = `Workflow blocker resolved: ${issueUrl}`;
|
|
220
|
+
const webhookUrl = webhookUrlTemplate
|
|
221
|
+
.replace('{URL}', encodeURIComponent(issueUrl))
|
|
222
|
+
.replace('{MESSAGE}', encodeURIComponent(message));
|
|
223
|
+
|
|
224
|
+
await this.webhookRepository.sendGetRequest(webhookUrl);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.warn('Failed to send workflow blocker notification:', error);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
179
229
|
}
|
|
@@ -11,6 +11,7 @@ type ConfigFile = {
|
|
|
11
11
|
allowedIssueAuthors?: string;
|
|
12
12
|
awaitingQualityCheckStatus?: string;
|
|
13
13
|
thresholdForAutoReject?: number;
|
|
14
|
+
workflowBlockerResolvedWebhookUrl?: string;
|
|
14
15
|
};
|
|
15
16
|
declare const loadConfigFile: (configFilePath: string) => ConfigFile;
|
|
16
17
|
declare const program: Command;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAMA,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":";AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,KAAK,UAAU,GAAG;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,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;CAC5C,CAAC;AA4CF,QAAA,MAAM,cAAc,GAAI,gBAAgB,MAAM,KAAG,UA0ChD,CAAC;AAEF,QAAA,MAAM,OAAO,SAAgB,CAAC;AA2S9B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { WebhookRepository } from '../../domain/usecases/adapter-interfaces/WebhookRepository';
|
|
2
|
+
export declare class FetchWebhookRepository implements WebhookRepository {
|
|
3
|
+
sendGetRequest: (url: string) => Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=FetchWebhookRepository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FetchWebhookRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FetchWebhookRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAE/F,qBAAa,sBAAuB,YAAW,iBAAiB;IAC9D,cAAc,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC,CAOjD;CACH"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
2
2
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
3
|
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
4
|
+
import { WebhookRepository } from './adapter-interfaces/WebhookRepository';
|
|
4
5
|
export declare class IssueNotFoundError extends Error {
|
|
5
6
|
constructor(issueUrl: string);
|
|
6
7
|
}
|
|
@@ -11,7 +12,8 @@ export declare class NotifyFinishedIssuePreparationUseCase {
|
|
|
11
12
|
private readonly projectRepository;
|
|
12
13
|
private readonly issueRepository;
|
|
13
14
|
private readonly issueCommentRepository;
|
|
14
|
-
|
|
15
|
+
private readonly webhookRepository;
|
|
16
|
+
constructor(projectRepository: Pick<ProjectRepository, 'getByUrl' | 'prepareStatus'>, issueRepository: Pick<IssueRepository, 'get' | 'update' | 'findRelatedOpenPRs' | 'getStoryObjectMap'>, issueCommentRepository: Pick<IssueCommentRepository, 'getCommentsFromIssue' | 'createComment'>, webhookRepository: Pick<WebhookRepository, 'sendGetRequest'>);
|
|
15
17
|
run: (params: {
|
|
16
18
|
projectUrl: string;
|
|
17
19
|
issueUrl: string;
|
|
@@ -19,6 +21,8 @@ export declare class NotifyFinishedIssuePreparationUseCase {
|
|
|
19
21
|
awaitingWorkspaceStatus: string;
|
|
20
22
|
awaitingQualityCheckStatus: string;
|
|
21
23
|
thresholdForAutoReject: number;
|
|
24
|
+
workflowBlockerResolvedWebhookUrl: string | null;
|
|
22
25
|
}) => Promise<void>;
|
|
26
|
+
private sendWorkflowBlockerNotification;
|
|
23
27
|
}
|
|
24
28
|
//# sourceMappingURL=NotifyFinishedIssuePreparationUseCase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotifyFinishedIssuePreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"NotifyFinishedIssuePreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAE3E,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,QAAQ,EAAE,MAAM;CAI7B;AACD,qBAAa,uBAAwB,SAAQ,KAAK;gBAE9C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,cAAc,EAAE,MAAM,GAAG,IAAI;CAOhC;AAUD,qBAAa,qCAAqC;IAE9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAIlC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAIhC,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IAIvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAZjB,iBAAiB,EAAE,IAAI,CACtC,iBAAiB,EACjB,UAAU,GAAG,eAAe,CAC7B,EACgB,eAAe,EAAE,IAAI,CACpC,eAAe,EACf,KAAK,GAAG,QAAQ,GAAG,oBAAoB,GAAG,mBAAmB,CAC9D,EACgB,sBAAsB,EAAE,IAAI,CAC3C,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC,EACgB,iBAAiB,EAAE,IAAI,CACtC,iBAAiB,EACjB,gBAAgB,CACjB;IAGH,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,uBAAuB,EAAE,MAAM,CAAC;QAChC,0BAA0B,EAAE,MAAM,CAAC;QACnC,sBAAsB,EAAE,MAAM,CAAC;QAC/B,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;KAClD,KAAG,OAAO,CAAC,IAAI,CAAC,CAqIf;IAEF,OAAO,CAAC,+BAA+B,CAgCrC;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebhookRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/WebhookRepository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C"}
|