github-issue-tower-defence-management 1.162.5 → 1.164.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 +19 -0
- package/README.md +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +3 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/RestIssueRepository.js +24 -0
- package/bin/adapter/repositories/issue/RestIssueRepository.js.map +1 -1
- package/bin/domain/entities/RequiredProjectField.js +1 -0
- package/bin/domain/entities/RequiredProjectField.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +46 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +31 -0
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -1
- package/lib/typescript-compat/bin/tsc +2 -0
- package/lib/typescript-compat/bin/tsserver +2 -0
- package/lib/typescript-compat/lib/typescript.js +1 -0
- package/lib/typescript-compat/package.json +13 -0
- package/package.json +4 -3
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +8 -0
- package/src/adapter/repositories/issue/RestIssueRepository.ts +30 -0
- package/src/domain/entities/RequiredProjectField.ts +1 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +235 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +70 -0
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +464 -28
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +56 -3
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +3 -2
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/RestIssueRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/RestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +2 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +3 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectRequiredFieldCreateUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts"],"names":[],"mappings":";;;AACA,mEAAyE;AACzE,
|
|
1
|
+
{"version":3,"file":"ProjectRequiredFieldCreateUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('@typescript/typescript6');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typescript",
|
|
3
|
+
"version": "6.0.3",
|
|
4
|
+
"description": "TypeScript 6 compatibility shim for projects with typescript@7 tooling dependencies",
|
|
5
|
+
"main": "./lib/typescript.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tsc": "./bin/tsc",
|
|
8
|
+
"tsserver": "./bin/tsserver"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@typescript/typescript6": "6.0.2"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-issue-tower-defence-management",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.164.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"@types/react-dom": "^19.2.3",
|
|
83
83
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
84
84
|
"@typescript-eslint/parser": "^8.65.0",
|
|
85
|
+
"@typescript/typescript6": "6.0.2",
|
|
85
86
|
"@vitejs/plugin-react": "^6.0.4",
|
|
86
87
|
"check-node-version": "^4.2.1",
|
|
87
88
|
"class-variance-authority": "^0.7.1",
|
|
@@ -104,12 +105,12 @@
|
|
|
104
105
|
"tailwind-merge": "^3.6.0",
|
|
105
106
|
"tailwindcss": "^4.3.3",
|
|
106
107
|
"ts-jest": "^29.4.12",
|
|
107
|
-
"typescript": "
|
|
108
|
+
"typescript": "file:./lib/typescript-compat",
|
|
108
109
|
"typescript-eslint": "^8.65.0",
|
|
109
110
|
"vite": "^8.1.5"
|
|
110
111
|
},
|
|
111
112
|
"dependencies": {
|
|
112
|
-
"better-sqlite3": "13.0.
|
|
113
|
+
"better-sqlite3": "13.0.3",
|
|
113
114
|
"commander": "^15.0.0",
|
|
114
115
|
"dompurify": "3.4.13",
|
|
115
116
|
"dotenv": "^17.4.2",
|
|
@@ -425,6 +425,7 @@ const createStubIssueRepository = (
|
|
|
425
425
|
createComment: () => notImplemented('createComment'),
|
|
426
426
|
updateLabels: () => notImplemented('updateLabels'),
|
|
427
427
|
removeLabel: () => notImplemented('removeLabel'),
|
|
428
|
+
getOrCreateLabel: () => notImplemented('getOrCreateLabel'),
|
|
428
429
|
updateAssigneeList: () => notImplemented('updateAssigneeList'),
|
|
429
430
|
searchIssues: () => notImplemented('searchIssues'),
|
|
430
431
|
get: async (issueUrl: string): Promise<Issue | null> =>
|
|
@@ -527,6 +527,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
527
527
|
| 'removeLabel'
|
|
528
528
|
| 'updateAssigneeList'
|
|
529
529
|
| 'searchIssues'
|
|
530
|
+
| 'getOrCreateLabel'
|
|
530
531
|
>,
|
|
531
532
|
readonly graphqlProjectItemRepository: Pick<
|
|
532
533
|
GraphqlProjectItemRepository,
|
|
@@ -1006,6 +1007,13 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1006
1007
|
removeLabel = (issue: Issue, label: string): Promise<void> => {
|
|
1007
1008
|
return this.restIssueRepository.removeLabel(issue, label);
|
|
1008
1009
|
};
|
|
1010
|
+
getOrCreateLabel = (
|
|
1011
|
+
org: string,
|
|
1012
|
+
repo: string,
|
|
1013
|
+
labelName: string,
|
|
1014
|
+
): Promise<void> => {
|
|
1015
|
+
return this.restIssueRepository.getOrCreateLabel(org, repo, labelName);
|
|
1016
|
+
};
|
|
1009
1017
|
updateAssigneeList = (
|
|
1010
1018
|
issue: Pick<Issue, 'org' | 'repo' | 'number'>,
|
|
1011
1019
|
assigneeList: Member['name'][],
|
|
@@ -154,6 +154,36 @@ export class RestIssueRepository
|
|
|
154
154
|
}
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
getOrCreateLabel = async (
|
|
158
|
+
org: string,
|
|
159
|
+
repo: string,
|
|
160
|
+
labelName: string,
|
|
161
|
+
): Promise<void> => {
|
|
162
|
+
try {
|
|
163
|
+
await ky.get(
|
|
164
|
+
`https://api.github.com/repos/${org}/${repo}/labels/${encodeURIComponent(labelName)}`,
|
|
165
|
+
{
|
|
166
|
+
headers: {
|
|
167
|
+
Authorization: `token ${this.ghToken}`,
|
|
168
|
+
Accept: 'application/vnd.github.v3+json',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (e instanceof HTTPError && e.response.status === 404) {
|
|
174
|
+
await ky.post(`https://api.github.com/repos/${org}/${repo}/labels`, {
|
|
175
|
+
json: { name: labelName, color: 'ededed' },
|
|
176
|
+
headers: {
|
|
177
|
+
Authorization: `token ${this.ghToken}`,
|
|
178
|
+
Accept: 'application/vnd.github.v3+json',
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
157
187
|
updateAssigneeList = async (
|
|
158
188
|
issue: Pick<Issue, 'org' | 'repo' | 'number'>,
|
|
159
189
|
assigneeList: Member['name'][],
|
|
@@ -16,6 +16,7 @@ const storyOption = (
|
|
|
16
16
|
const REQUIRED_STORY_OPTIONS: Omit<FieldOption, 'id'>[] = [
|
|
17
17
|
storyOption('regular / NO STORY', 'RED'),
|
|
18
18
|
storyOption('regular / WORKFLOW BLOCKER', 'RED'),
|
|
19
|
+
storyOption('regular / inquiry', 'RED'),
|
|
19
20
|
storyOption('regular / high priority', 'RED'),
|
|
20
21
|
storyOption('regular / workflow management', 'YELLOW'),
|
|
21
22
|
storyOption('regular / routine management', 'YELLOW'),
|
|
@@ -92,6 +92,8 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
92
92
|
get: jest.Mock;
|
|
93
93
|
update: jest.Mock;
|
|
94
94
|
updateStatus: jest.Mock;
|
|
95
|
+
updateLabels: jest.Mock;
|
|
96
|
+
getOrCreateLabel: jest.Mock;
|
|
95
97
|
findRelatedOpenPRs: jest.Mock;
|
|
96
98
|
getStoryObjectMap: jest.Mock;
|
|
97
99
|
getOpenPullRequest: jest.Mock;
|
|
@@ -128,6 +130,8 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
128
130
|
get: jest.fn(),
|
|
129
131
|
update: jest.fn(),
|
|
130
132
|
updateStatus: jest.fn(),
|
|
133
|
+
updateLabels: jest.fn().mockResolvedValue(undefined),
|
|
134
|
+
getOrCreateLabel: jest.fn().mockResolvedValue(undefined),
|
|
131
135
|
findRelatedOpenPRs: jest.fn(),
|
|
132
136
|
getOpenPullRequest: jest.fn(),
|
|
133
137
|
getPullRequestChangedFilePaths: jest.fn().mockResolvedValue([]),
|
|
@@ -799,6 +803,237 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
799
803
|
);
|
|
800
804
|
});
|
|
801
805
|
|
|
806
|
+
it('should add nextStepAgent label and return to Awaiting Workspace when report has nextStepAgent', async () => {
|
|
807
|
+
const issue = createMockIssue({
|
|
808
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
809
|
+
status: 'Preparation',
|
|
810
|
+
org: 'user',
|
|
811
|
+
repo: 'repo',
|
|
812
|
+
labels: [],
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
816
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
817
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
818
|
+
createMockComment({
|
|
819
|
+
content:
|
|
820
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:chore", "nextStep": null}\n```',
|
|
821
|
+
}),
|
|
822
|
+
]);
|
|
823
|
+
|
|
824
|
+
await useCase.run({
|
|
825
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
826
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
827
|
+
thresholdForAutoReject: 3,
|
|
828
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
829
|
+
allowedIssueAuthors: null,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
expect(mockIssueRepository.getOrCreateLabel).toHaveBeenCalledWith(
|
|
833
|
+
'user',
|
|
834
|
+
'repo',
|
|
835
|
+
'llm-agent:chore',
|
|
836
|
+
);
|
|
837
|
+
expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
|
|
838
|
+
expect.objectContaining({ labels: ['llm-agent:chore'] }),
|
|
839
|
+
['llm-agent:chore'],
|
|
840
|
+
);
|
|
841
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
842
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
843
|
+
mockProject,
|
|
844
|
+
);
|
|
845
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
846
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
847
|
+
expect.stringContaining('llm-agent:chore'),
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should preserve existing labels when adding nextStepAgent label', async () => {
|
|
852
|
+
const issue = createMockIssue({
|
|
853
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
854
|
+
status: 'Preparation',
|
|
855
|
+
org: 'user',
|
|
856
|
+
repo: 'repo',
|
|
857
|
+
labels: ['existing-label'],
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
861
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
862
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
863
|
+
createMockComment({
|
|
864
|
+
content:
|
|
865
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:impl", "nextStep": null}\n```',
|
|
866
|
+
}),
|
|
867
|
+
]);
|
|
868
|
+
|
|
869
|
+
await useCase.run({
|
|
870
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
871
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
872
|
+
thresholdForAutoReject: 3,
|
|
873
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
874
|
+
allowedIssueAuthors: null,
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
|
|
878
|
+
expect.objectContaining({
|
|
879
|
+
labels: ['existing-label', 'llm-agent:impl'],
|
|
880
|
+
}),
|
|
881
|
+
['existing-label', 'llm-agent:impl'],
|
|
882
|
+
);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('should not add nextStepAgent label when nextStepAgent is null', async () => {
|
|
886
|
+
const issue = createMockIssue({
|
|
887
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
888
|
+
status: 'Preparation',
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
892
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
893
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
894
|
+
createMockComment({
|
|
895
|
+
content:
|
|
896
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": null, "nextStep": null}\n```',
|
|
897
|
+
}),
|
|
898
|
+
]);
|
|
899
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
900
|
+
{
|
|
901
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
902
|
+
isConflicted: false,
|
|
903
|
+
isPassedAllCiJob: true,
|
|
904
|
+
isCiStateSuccess: true,
|
|
905
|
+
isResolvedAllReviewComments: true,
|
|
906
|
+
isBranchOutOfDate: false,
|
|
907
|
+
missingRequiredCheckNames: [],
|
|
908
|
+
},
|
|
909
|
+
]);
|
|
910
|
+
|
|
911
|
+
await useCase.run({
|
|
912
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
913
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
914
|
+
thresholdForAutoReject: 3,
|
|
915
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
916
|
+
allowedIssueAuthors: null,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
expect(mockIssueRepository.getOrCreateLabel).not.toHaveBeenCalled();
|
|
920
|
+
expect(mockIssueRepository.updateLabels).not.toHaveBeenCalled();
|
|
921
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
922
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
923
|
+
mockProject,
|
|
924
|
+
);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('should create label when nextStepAgent label does not exist in repo', async () => {
|
|
928
|
+
const issue = createMockIssue({
|
|
929
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
930
|
+
status: 'Preparation',
|
|
931
|
+
org: 'user',
|
|
932
|
+
repo: 'repo',
|
|
933
|
+
labels: [],
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
937
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
938
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
939
|
+
createMockComment({
|
|
940
|
+
content:
|
|
941
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:new-agent", "nextStep": null}\n```',
|
|
942
|
+
}),
|
|
943
|
+
]);
|
|
944
|
+
|
|
945
|
+
await useCase.run({
|
|
946
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
947
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
948
|
+
thresholdForAutoReject: 3,
|
|
949
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
950
|
+
allowedIssueAuthors: null,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
expect(mockIssueRepository.getOrCreateLabel).toHaveBeenCalledWith(
|
|
954
|
+
'user',
|
|
955
|
+
'repo',
|
|
956
|
+
'llm-agent:new-agent',
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it('should remove existing llm-agent labels and labelsAsLlmAgentName labels when adding nextStepAgent label', async () => {
|
|
961
|
+
const issue = createMockIssue({
|
|
962
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
963
|
+
status: 'Preparation',
|
|
964
|
+
org: 'user',
|
|
965
|
+
repo: 'repo',
|
|
966
|
+
labels: ['llm-agent:chore', 'chore', 'feature-flag'],
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
970
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
971
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
972
|
+
createMockComment({
|
|
973
|
+
content:
|
|
974
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:impl", "nextStep": null}\n```',
|
|
975
|
+
}),
|
|
976
|
+
]);
|
|
977
|
+
|
|
978
|
+
await useCase.run({
|
|
979
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
980
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
981
|
+
thresholdForAutoReject: 3,
|
|
982
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
983
|
+
allowedIssueAuthors: null,
|
|
984
|
+
labelsAsLlmAgentName: ['chore', 'accounting'],
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
|
|
988
|
+
expect.objectContaining({
|
|
989
|
+
labels: ['feature-flag', 'llm-agent:impl'],
|
|
990
|
+
}),
|
|
991
|
+
['feature-flag', 'llm-agent:impl'],
|
|
992
|
+
);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it('should not add nextStepAgent label when nextStepAgent is an empty string', async () => {
|
|
996
|
+
const issue = createMockIssue({
|
|
997
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
998
|
+
status: 'Preparation',
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1002
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1003
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1004
|
+
createMockComment({
|
|
1005
|
+
content:
|
|
1006
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "", "nextStep": null}\n```',
|
|
1007
|
+
}),
|
|
1008
|
+
]);
|
|
1009
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1010
|
+
{
|
|
1011
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1012
|
+
isConflicted: false,
|
|
1013
|
+
isPassedAllCiJob: true,
|
|
1014
|
+
isCiStateSuccess: true,
|
|
1015
|
+
isResolvedAllReviewComments: true,
|
|
1016
|
+
isBranchOutOfDate: false,
|
|
1017
|
+
missingRequiredCheckNames: [],
|
|
1018
|
+
},
|
|
1019
|
+
]);
|
|
1020
|
+
|
|
1021
|
+
await useCase.run({
|
|
1022
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1023
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1024
|
+
thresholdForAutoReject: 3,
|
|
1025
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1026
|
+
allowedIssueAuthors: null,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
expect(mockIssueRepository.getOrCreateLabel).not.toHaveBeenCalled();
|
|
1030
|
+
expect(mockIssueRepository.updateLabels).not.toHaveBeenCalled();
|
|
1031
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1032
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
1033
|
+
mockProject,
|
|
1034
|
+
);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
802
1037
|
it('should auto-escalate to Failed Preparation after threshold rejections', async () => {
|
|
803
1038
|
const issue = createMockIssue({
|
|
804
1039
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -58,6 +58,8 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
58
58
|
| 'get'
|
|
59
59
|
| 'update'
|
|
60
60
|
| 'updateStatus'
|
|
61
|
+
| 'updateLabels'
|
|
62
|
+
| 'getOrCreateLabel'
|
|
61
63
|
| 'findRelatedOpenPRs'
|
|
62
64
|
| 'getStoryObjectMap'
|
|
63
65
|
| 'getOpenPullRequest'
|
|
@@ -193,6 +195,43 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
193
195
|
const isTrustedAuthor = (author: string): boolean =>
|
|
194
196
|
this.isAuthorTrusted(author, params.allowedIssueAuthors ?? null);
|
|
195
197
|
|
|
198
|
+
const lastComment = comments[comments.length - 1];
|
|
199
|
+
if (
|
|
200
|
+
lastComment &&
|
|
201
|
+
isTrustedAuthor(lastComment.author) &&
|
|
202
|
+
lastComment.content.startsWith('From: :robot:')
|
|
203
|
+
) {
|
|
204
|
+
const nextStepAgent = this.extractNextStepAgent(lastComment.content);
|
|
205
|
+
if (nextStepAgent !== null) {
|
|
206
|
+
await this.issueRepository.getOrCreateLabel(
|
|
207
|
+
issue.org,
|
|
208
|
+
issue.repo,
|
|
209
|
+
nextStepAgent,
|
|
210
|
+
);
|
|
211
|
+
const agentLabels = params.labelsAsLlmAgentName ?? [];
|
|
212
|
+
const filteredLabels = issue.labels.filter(
|
|
213
|
+
(label) =>
|
|
214
|
+
!label.startsWith('llm-agent:') && !agentLabels.includes(label),
|
|
215
|
+
);
|
|
216
|
+
const updatedLabels = [...new Set([...filteredLabels, nextStepAgent])];
|
|
217
|
+
issue.labels = updatedLabels;
|
|
218
|
+
issue.status = AWAITING_WORKSPACE_STATUS_NAME;
|
|
219
|
+
await this.issueRepository.update(issue, project);
|
|
220
|
+
await this.issueRepository.updateStatus(
|
|
221
|
+
project,
|
|
222
|
+
issue,
|
|
223
|
+
awaitingWorkspaceStatusOption.id,
|
|
224
|
+
);
|
|
225
|
+
await this.issueRepository.updateLabels(issue, updatedLabels);
|
|
226
|
+
await this.patchConsoleTab(issue);
|
|
227
|
+
await this.issueCommentRepository.createComment(
|
|
228
|
+
issue,
|
|
229
|
+
`Next step agent: ${nextStepAgent}`,
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
196
235
|
const { rejections, approvedPrUrl } = await this.collectRejections(
|
|
197
236
|
issue,
|
|
198
237
|
comments,
|
|
@@ -405,6 +444,37 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
405
444
|
return nextStepValue !== null && nextStepValue !== undefined;
|
|
406
445
|
};
|
|
407
446
|
|
|
447
|
+
private extractNextStepAgent = (body: string): string | null => {
|
|
448
|
+
const reportMatch = body.match(/```json\n([\s\S]*?)\n```/);
|
|
449
|
+
if (!reportMatch || reportMatch.length < 2) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
let reportJson: unknown;
|
|
453
|
+
try {
|
|
454
|
+
reportJson = JSON.parse(reportMatch[1]);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.warn(
|
|
457
|
+
'Invalid JSON in report body while checking nextStepAgent:',
|
|
458
|
+
error,
|
|
459
|
+
);
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (typeof reportJson !== 'object' || reportJson === null) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
if (!('nextStepAgent' in reportJson)) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const nextStepAgentValue = Reflect.get(reportJson, 'nextStepAgent');
|
|
469
|
+
if (
|
|
470
|
+
typeof nextStepAgentValue !== 'string' ||
|
|
471
|
+
nextStepAgentValue.trim() === ''
|
|
472
|
+
) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
return nextStepAgentValue;
|
|
476
|
+
};
|
|
477
|
+
|
|
408
478
|
private setDependedIssueUrlForAllOpenPRs = async (
|
|
409
479
|
issue: { url: string; labels: string[]; isPr: boolean },
|
|
410
480
|
issueUrl: string,
|