github-issue-tower-defence-management 1.185.0 → 1.186.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 +17 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/index.js +2 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/console/webServer.js +3 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js +40 -3
- package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/entities/Project.js +11 -1
- package/bin/domain/entities/Project.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js +9 -1
- package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +14 -10
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
- package/src/adapter/entry-points/console/ui/src/index.css +35 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/console/webServer.ts +7 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
- package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.test.ts +48 -0
- package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.ts +55 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/domain/entities/ClaudeHandoverSession.ts +1 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/entities/Project.ts +11 -2
- package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/TokenExhaustionHandoverUseCase.test.ts +59 -1
- package/src/domain/usecases/TokenExhaustionHandoverUseCase.ts +14 -1
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts +1 -0
- package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/entities/ClaudeHandoverSession.d.ts +1 -0
- package/types/domain/entities/ClaudeHandoverSession.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +2 -1
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts +1 -0
- package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { mock } from 'jest-mock-extended';
|
|
2
|
+
import { IssueNoStatusUpdateUseCase } from './IssueNoStatusUpdateUseCase';
|
|
3
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
|
+
import { Issue } from '../entities/Issue';
|
|
5
|
+
import { FieldOption, Project } from '../entities/Project';
|
|
6
|
+
|
|
7
|
+
describe('IssueNoStatusUpdateUseCase', () => {
|
|
8
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
9
|
+
|
|
10
|
+
const awaitingWorkspaceStatus = mock<FieldOption>();
|
|
11
|
+
awaitingWorkspaceStatus.id = 'status-awaiting';
|
|
12
|
+
awaitingWorkspaceStatus.name = 'Awaiting Workspace';
|
|
13
|
+
|
|
14
|
+
const inProgressStatus = mock<FieldOption>();
|
|
15
|
+
inProgressStatus.id = 'status-in-progress';
|
|
16
|
+
inProgressStatus.name = 'In Progress';
|
|
17
|
+
|
|
18
|
+
const basicProject: Project = {
|
|
19
|
+
...mock<Project>(),
|
|
20
|
+
status: {
|
|
21
|
+
name: 'Status',
|
|
22
|
+
fieldId: 'statusFieldId',
|
|
23
|
+
statuses: [awaitingWorkspaceStatus, inProgressStatus],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const projectWithoutAwaitingWorkspace: Project = {
|
|
28
|
+
...mock<Project>(),
|
|
29
|
+
status: {
|
|
30
|
+
name: 'Status',
|
|
31
|
+
fieldId: 'statusFieldId',
|
|
32
|
+
statuses: [inProgressStatus],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const openNullStatusIssue: Issue = {
|
|
37
|
+
...mock<Issue>(),
|
|
38
|
+
isClosed: false,
|
|
39
|
+
status: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const openInProgressIssue: Issue = {
|
|
43
|
+
...mock<Issue>(),
|
|
44
|
+
isClosed: false,
|
|
45
|
+
status: 'In Progress',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const closedNullStatusIssue: Issue = {
|
|
49
|
+
...mock<Issue>(),
|
|
50
|
+
isClosed: true,
|
|
51
|
+
status: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let useCase: IssueNoStatusUpdateUseCase;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
useCase = new IssueNoStatusUpdateUseCase(mockIssueRepository);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('run', () => {
|
|
62
|
+
const testCases: {
|
|
63
|
+
name: string;
|
|
64
|
+
project: Project;
|
|
65
|
+
issues: Issue[];
|
|
66
|
+
expectedUpdateStatusCalls: [Project, Issue, string][];
|
|
67
|
+
}[] = [
|
|
68
|
+
{
|
|
69
|
+
name: 'should call updateStatus for open issues with null status',
|
|
70
|
+
project: basicProject,
|
|
71
|
+
issues: [openNullStatusIssue],
|
|
72
|
+
expectedUpdateStatusCalls: [
|
|
73
|
+
[basicProject, openNullStatusIssue, 'status-awaiting'],
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'should skip issues with an existing status',
|
|
78
|
+
project: basicProject,
|
|
79
|
+
issues: [openInProgressIssue],
|
|
80
|
+
expectedUpdateStatusCalls: [],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'should skip closed issues with null status',
|
|
84
|
+
project: basicProject,
|
|
85
|
+
issues: [closedNullStatusIssue],
|
|
86
|
+
expectedUpdateStatusCalls: [],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'should not call updateStatus when Awaiting Workspace status does not exist in project',
|
|
90
|
+
project: projectWithoutAwaitingWorkspace,
|
|
91
|
+
issues: [openNullStatusIssue],
|
|
92
|
+
expectedUpdateStatusCalls: [],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'should handle empty issues array',
|
|
96
|
+
project: basicProject,
|
|
97
|
+
issues: [],
|
|
98
|
+
expectedUpdateStatusCalls: [],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'should call updateStatus only for open null-status issues among mixed issues',
|
|
102
|
+
project: basicProject,
|
|
103
|
+
issues: [
|
|
104
|
+
openNullStatusIssue,
|
|
105
|
+
openInProgressIssue,
|
|
106
|
+
closedNullStatusIssue,
|
|
107
|
+
],
|
|
108
|
+
expectedUpdateStatusCalls: [
|
|
109
|
+
[basicProject, openNullStatusIssue, 'status-awaiting'],
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
testCases.forEach(
|
|
115
|
+
({ name, project, issues, expectedUpdateStatusCalls }) => {
|
|
116
|
+
it(name, async () => {
|
|
117
|
+
await useCase.run({ project, issues });
|
|
118
|
+
|
|
119
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toEqual(
|
|
120
|
+
expectedUpdateStatusCalls,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
it('should skip archived items and continue updating remaining issues', async () => {
|
|
127
|
+
const archivedIssue: Issue = {
|
|
128
|
+
...mock<Issue>(),
|
|
129
|
+
isClosed: false,
|
|
130
|
+
status: null,
|
|
131
|
+
};
|
|
132
|
+
const normalIssue: Issue = {
|
|
133
|
+
...mock<Issue>(),
|
|
134
|
+
isClosed: false,
|
|
135
|
+
status: null,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
mockIssueRepository.updateStatus
|
|
139
|
+
.mockRejectedValueOnce(
|
|
140
|
+
new Error('The item is archived and cannot be updated'),
|
|
141
|
+
)
|
|
142
|
+
.mockResolvedValueOnce(undefined);
|
|
143
|
+
|
|
144
|
+
await useCase.run({
|
|
145
|
+
project: basicProject,
|
|
146
|
+
issues: [archivedIssue, normalIssue],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toEqual([
|
|
150
|
+
[basicProject, archivedIssue, 'status-awaiting'],
|
|
151
|
+
[basicProject, normalIssue, 'status-awaiting'],
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should re-throw non-archived errors from updateStatus', async () => {
|
|
156
|
+
mockIssueRepository.updateStatus.mockRejectedValueOnce(
|
|
157
|
+
new Error('GraphQL rate limit exceeded'),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
useCase.run({ project: basicProject, issues: [openNullStatusIssue] }),
|
|
162
|
+
).rejects.toThrow('GraphQL rate limit exceeded');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Issue } from '../entities/Issue';
|
|
2
|
+
import { Project } from '../entities/Project';
|
|
3
|
+
import { AWAITING_WORKSPACE_STATUS_NAME } from '../entities/WorkflowStatus';
|
|
4
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
5
|
+
|
|
6
|
+
const isArchivedProjectItemError = (error: unknown): boolean => {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
return message.toLowerCase().includes('archived');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class IssueNoStatusUpdateUseCase {
|
|
12
|
+
constructor(
|
|
13
|
+
readonly issueRepository: Pick<IssueRepository, 'updateStatus'>,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
run = async (input: { project: Project; issues: Issue[] }): Promise<void> => {
|
|
17
|
+
const awaitingWorkspaceStatus = input.project.status.statuses.find(
|
|
18
|
+
(s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
|
|
19
|
+
);
|
|
20
|
+
if (!awaitingWorkspaceStatus) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const issue of input.issues) {
|
|
24
|
+
if (issue.isClosed || issue.status !== null) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await this.issueRepository.updateStatus(
|
|
29
|
+
input.project,
|
|
30
|
+
issue,
|
|
31
|
+
awaitingWorkspaceStatus.id,
|
|
32
|
+
);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (isArchivedProjectItemError(error)) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`IssueNoStatusUpdateUseCase: project item is archived and cannot be updated, skipping. issueUrl: ${issue.url}`,
|
|
37
|
+
);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -858,6 +858,126 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
858
858
|
});
|
|
859
859
|
});
|
|
860
860
|
|
|
861
|
+
describe('detectConflictEvenIfEvaluationSkipped option', () => {
|
|
862
|
+
it('should call findRelatedOpenPRs and return PULL_REQUEST_CONFLICTED when relatedOpenPrUrls is null and PR is conflicted', async () => {
|
|
863
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
864
|
+
createReadyPr('https://github.com/user/repo/pull/1', {
|
|
865
|
+
isConflicted: true,
|
|
866
|
+
}),
|
|
867
|
+
]);
|
|
868
|
+
|
|
869
|
+
const result = await evaluator.evaluate(
|
|
870
|
+
{
|
|
871
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
872
|
+
labels: [],
|
|
873
|
+
isPr: false,
|
|
874
|
+
agent: 'chore',
|
|
875
|
+
},
|
|
876
|
+
[],
|
|
877
|
+
{
|
|
878
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
879
|
+
relatedOpenPrUrls: null,
|
|
880
|
+
},
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
|
|
884
|
+
'https://github.com/user/repo/issues/1',
|
|
885
|
+
);
|
|
886
|
+
expect(result.rejections).toHaveLength(1);
|
|
887
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
|
|
888
|
+
expect(result.approvedPrUrl).toBeNull();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should call findRelatedOpenPRs and return no rejections when relatedOpenPrUrls is null and no PR is conflicted', async () => {
|
|
892
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
893
|
+
createReadyPr('https://github.com/user/repo/pull/1'),
|
|
894
|
+
]);
|
|
895
|
+
|
|
896
|
+
const result = await evaluator.evaluate(
|
|
897
|
+
{
|
|
898
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
899
|
+
labels: [],
|
|
900
|
+
isPr: false,
|
|
901
|
+
agent: 'chore',
|
|
902
|
+
},
|
|
903
|
+
[],
|
|
904
|
+
{
|
|
905
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
906
|
+
relatedOpenPrUrls: null,
|
|
907
|
+
},
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
|
|
911
|
+
'https://github.com/user/repo/issues/1',
|
|
912
|
+
);
|
|
913
|
+
expect(result.rejections).toHaveLength(0);
|
|
914
|
+
expect(result.approvedPrUrl).toBeNull();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('should use resolveOpenPrsFromUrls path and not call findRelatedOpenPRs when relatedOpenPrUrls is provided', async () => {
|
|
918
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue(
|
|
919
|
+
createReadyPr('https://github.com/user/repo/pull/5', {
|
|
920
|
+
isConflicted: true,
|
|
921
|
+
}),
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
const result = await evaluator.evaluate(
|
|
925
|
+
{
|
|
926
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
927
|
+
labels: [],
|
|
928
|
+
isPr: false,
|
|
929
|
+
agent: 'chore',
|
|
930
|
+
},
|
|
931
|
+
[],
|
|
932
|
+
{
|
|
933
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
934
|
+
relatedOpenPrUrls: ['https://github.com/user/repo/pull/5'],
|
|
935
|
+
},
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
939
|
+
expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
|
|
940
|
+
'https://github.com/user/repo/pull/5',
|
|
941
|
+
);
|
|
942
|
+
expect(result.rejections).toHaveLength(1);
|
|
943
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should emit only PULL_REQUEST_CONFLICTED and not PULL_REQUEST_IS_DRAFT or other types when evaluation is skipped', async () => {
|
|
947
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
948
|
+
createReadyPr('https://github.com/user/repo/pull/1', {
|
|
949
|
+
isConflicted: true,
|
|
950
|
+
isDraft: true,
|
|
951
|
+
isPassedAllCiJob: false,
|
|
952
|
+
isCiStateSuccess: false,
|
|
953
|
+
}),
|
|
954
|
+
]);
|
|
955
|
+
|
|
956
|
+
const result = await evaluator.evaluate(
|
|
957
|
+
{
|
|
958
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
959
|
+
labels: [],
|
|
960
|
+
isPr: false,
|
|
961
|
+
agent: 'chore',
|
|
962
|
+
},
|
|
963
|
+
[],
|
|
964
|
+
{
|
|
965
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
966
|
+
relatedOpenPrUrls: null,
|
|
967
|
+
},
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
expect(result.rejections).toHaveLength(1);
|
|
971
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
|
|
972
|
+
expect(result.rejections.map((r) => r.type)).not.toContain(
|
|
973
|
+
'PULL_REQUEST_IS_DRAFT',
|
|
974
|
+
);
|
|
975
|
+
expect(result.rejections.map((r) => r.type)).not.toContain(
|
|
976
|
+
'ANY_CI_JOB_FAILED_OR_IN_PROGRESS',
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
861
981
|
describe('change-target-must: label behavior', () => {
|
|
862
982
|
const prUrl = 'https://github.com/user/repo/pull/1';
|
|
863
983
|
|
|
@@ -37,6 +37,7 @@ export type EvaluateOptions = {
|
|
|
37
37
|
// The agent name treated as the developer agent for PR/CI/conflict checks.
|
|
38
38
|
// When null or undefined, defaults to 'developer'.
|
|
39
39
|
developerAgentName?: string | null;
|
|
40
|
+
detectConflictEvenIfEvaluationSkipped?: boolean;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export class IssueRejectionEvaluator {
|
|
@@ -186,6 +187,26 @@ export class IssueRejectionEvaluator {
|
|
|
186
187
|
approvedPrUrl = pr.url;
|
|
187
188
|
}
|
|
188
189
|
}
|
|
190
|
+
} else if (options.detectConflictEvenIfEvaluationSkipped) {
|
|
191
|
+
let prsToCheck: RelatedPullRequest[];
|
|
192
|
+
if (options.relatedOpenPrUrls != null) {
|
|
193
|
+
const resolved = await this.resolveOpenPrsFromUrls(
|
|
194
|
+
options.relatedOpenPrUrls,
|
|
195
|
+
options.resolvedOpenPrByUrl ?? null,
|
|
196
|
+
);
|
|
197
|
+
prsToCheck = resolved.prs;
|
|
198
|
+
} else {
|
|
199
|
+
prsToCheck = await this.issueRepository.findRelatedOpenPRs(issue.url);
|
|
200
|
+
}
|
|
201
|
+
for (const pr of prsToCheck) {
|
|
202
|
+
if (pr.isConflicted) {
|
|
203
|
+
rejections.push({
|
|
204
|
+
type: 'PULL_REQUEST_CONFLICTED',
|
|
205
|
+
detail: `PULL_REQUEST_CONFLICTED: ${pr.url}`,
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
return { rejections, approvedPrUrl };
|
|
@@ -35,6 +35,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
|
35
35
|
author: '',
|
|
36
36
|
closingIssueReferenceUrls: [],
|
|
37
37
|
agent: null,
|
|
38
|
+
stateReason: null,
|
|
38
39
|
...overrides,
|
|
39
40
|
});
|
|
40
41
|
|
|
@@ -67,6 +68,7 @@ const createMergedPr = (
|
|
|
67
68
|
author: '',
|
|
68
69
|
closingIssueReferenceUrls: [closingIssueUrl],
|
|
69
70
|
agent: null,
|
|
71
|
+
stateReason: null,
|
|
70
72
|
...overrides,
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -384,4 +386,36 @@ describe('QualityCheckAdvanceUseCase', () => {
|
|
|
384
386
|
'done-id',
|
|
385
387
|
);
|
|
386
388
|
});
|
|
389
|
+
|
|
390
|
+
it('does not advance an issue with stateReason REOPENED even when it has a merged PR', async () => {
|
|
391
|
+
const issue = createMockIssue({ stateReason: 'REOPENED' });
|
|
392
|
+
const mergedPr = createMergedPr(issue.url);
|
|
393
|
+
const project = createMockProject();
|
|
394
|
+
|
|
395
|
+
await useCase.run({
|
|
396
|
+
project,
|
|
397
|
+
issues: [issue, mergedPr],
|
|
398
|
+
evaluatedAt: FIXED_NOW,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('advances an issue whose stateReason is COMPLETED even though it was previously REOPENED', async () => {
|
|
405
|
+
const issue = createMockIssue({ stateReason: 'COMPLETED' });
|
|
406
|
+
const mergedPr = createMergedPr(issue.url);
|
|
407
|
+
const project = createMockProject();
|
|
408
|
+
|
|
409
|
+
await useCase.run({
|
|
410
|
+
project,
|
|
411
|
+
issues: [issue, mergedPr],
|
|
412
|
+
evaluatedAt: FIXED_NOW,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
416
|
+
project,
|
|
417
|
+
issue,
|
|
418
|
+
'done-id',
|
|
419
|
+
);
|
|
420
|
+
});
|
|
387
421
|
});
|
|
@@ -45,6 +45,7 @@ export class QualityCheckAdvanceUseCase {
|
|
|
45
45
|
(issue) =>
|
|
46
46
|
issue.status === qualityCheckStatusName &&
|
|
47
47
|
!issue.isClosed &&
|
|
48
|
+
issue.stateReason !== 'REOPENED' &&
|
|
48
49
|
issue.dependedIssueUrls.length === 0 &&
|
|
49
50
|
!issueReactivationTriggerIsPending(issue, evaluatedAt) &&
|
|
50
51
|
issueUrlsWithMergedPr.has(issue.url),
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
|
|
2
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
|
+
import { Issue } from '../entities/Issue';
|
|
4
|
+
import { Project } from '../entities/Project';
|
|
5
|
+
import {
|
|
6
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
7
|
+
DONE_STATUS_NAME,
|
|
8
|
+
} from '../entities/WorkflowStatus';
|
|
9
|
+
|
|
10
|
+
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
11
|
+
|
|
12
|
+
const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
13
|
+
nameWithOwner: 'user/repo',
|
|
14
|
+
number: 1,
|
|
15
|
+
title: 'Test Issue',
|
|
16
|
+
state: 'OPEN',
|
|
17
|
+
status: DONE_STATUS_NAME,
|
|
18
|
+
story: 'Default Story',
|
|
19
|
+
nextActionDate: null,
|
|
20
|
+
nextActionHour: null,
|
|
21
|
+
estimationMinutes: null,
|
|
22
|
+
dependedIssueUrls: [],
|
|
23
|
+
completionDate50PercentConfidence: null,
|
|
24
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
25
|
+
assignees: [],
|
|
26
|
+
labels: [],
|
|
27
|
+
org: 'user',
|
|
28
|
+
repo: 'repo',
|
|
29
|
+
body: '',
|
|
30
|
+
itemId: 'item-1',
|
|
31
|
+
isPr: false,
|
|
32
|
+
isInProgress: false,
|
|
33
|
+
isClosed: false,
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
author: '',
|
|
36
|
+
closingIssueReferenceUrls: [],
|
|
37
|
+
agent: null,
|
|
38
|
+
stateReason: 'REOPENED',
|
|
39
|
+
...overrides,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const createMockProject = (): Project => ({
|
|
43
|
+
id: 'project-1',
|
|
44
|
+
url: 'https://github.com/orgs/user/projects/1',
|
|
45
|
+
databaseId: 1,
|
|
46
|
+
name: 'Test Project',
|
|
47
|
+
status: {
|
|
48
|
+
name: 'Status',
|
|
49
|
+
fieldId: 'status-field-id',
|
|
50
|
+
statuses: [
|
|
51
|
+
{
|
|
52
|
+
id: 'awaiting-workspace-id',
|
|
53
|
+
name: AWAITING_WORKSPACE_STATUS_NAME,
|
|
54
|
+
color: 'BLUE',
|
|
55
|
+
description: '',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'preparation-id',
|
|
59
|
+
name: 'Preparation',
|
|
60
|
+
color: 'YELLOW',
|
|
61
|
+
description: '',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'done-id',
|
|
65
|
+
name: DONE_STATUS_NAME,
|
|
66
|
+
color: 'PURPLE',
|
|
67
|
+
description: '',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
nextActionDate: null,
|
|
72
|
+
nextActionHour: null,
|
|
73
|
+
story: null,
|
|
74
|
+
remainingEstimationMinutes: null,
|
|
75
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
76
|
+
completionDate50PercentConfidence: null,
|
|
77
|
+
agent: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('ReopenedDoneIssueRevertUseCase', () => {
|
|
81
|
+
let useCase: ReopenedDoneIssueRevertUseCase;
|
|
82
|
+
let mockIssueRepository: Mocked<Pick<IssueRepository, 'updateStatus'>>;
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
mockIssueRepository = {
|
|
86
|
+
updateStatus: jest.fn(),
|
|
87
|
+
};
|
|
88
|
+
useCase = new ReopenedDoneIssueRevertUseCase(mockIssueRepository);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('resets a Done issue with stateReason REOPENED to Awaiting Workspace', async () => {
|
|
92
|
+
const issue = createMockIssue();
|
|
93
|
+
const project = createMockProject();
|
|
94
|
+
|
|
95
|
+
await useCase.run({ project, issues: [issue] });
|
|
96
|
+
|
|
97
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
98
|
+
project,
|
|
99
|
+
issue,
|
|
100
|
+
'awaiting-workspace-id',
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not reset a Done issue with stateReason COMPLETED', async () => {
|
|
105
|
+
const issue = createMockIssue({ stateReason: 'COMPLETED' });
|
|
106
|
+
const project = createMockProject();
|
|
107
|
+
|
|
108
|
+
await useCase.run({ project, issues: [issue] });
|
|
109
|
+
|
|
110
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not reset a Done issue with stateReason null', async () => {
|
|
114
|
+
const issue = createMockIssue({ stateReason: null });
|
|
115
|
+
const project = createMockProject();
|
|
116
|
+
|
|
117
|
+
await useCase.run({ project, issues: [issue] });
|
|
118
|
+
|
|
119
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not reset a PR item even when Done and stateReason is REOPENED', async () => {
|
|
123
|
+
const prIssue = createMockIssue({
|
|
124
|
+
isPr: true,
|
|
125
|
+
url: 'https://github.com/user/repo/pull/10',
|
|
126
|
+
});
|
|
127
|
+
const project = createMockProject();
|
|
128
|
+
|
|
129
|
+
await useCase.run({ project, issues: [prIssue] });
|
|
130
|
+
|
|
131
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not reset an issue that is not in Done status', async () => {
|
|
135
|
+
const issue = createMockIssue({ status: 'Awaiting Workspace' });
|
|
136
|
+
const project = createMockProject();
|
|
137
|
+
|
|
138
|
+
await useCase.run({ project, issues: [issue] });
|
|
139
|
+
|
|
140
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('logs an error and returns 0 when Awaiting Workspace status option is missing', async () => {
|
|
144
|
+
const issue = createMockIssue();
|
|
145
|
+
const project = createMockProject();
|
|
146
|
+
project.status.statuses = project.status.statuses.filter(
|
|
147
|
+
(s) => s.name !== AWAITING_WORKSPACE_STATUS_NAME,
|
|
148
|
+
);
|
|
149
|
+
const consoleSpy = jest
|
|
150
|
+
.spyOn(console, 'error')
|
|
151
|
+
.mockImplementation(() => {});
|
|
152
|
+
|
|
153
|
+
const result = await useCase.run({ project, issues: [issue] });
|
|
154
|
+
|
|
155
|
+
expect(result).toBe(0);
|
|
156
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
157
|
+
consoleSpy.mockRestore();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('collects updateStatus failures and re-throws as AggregateError after processing all items', async () => {
|
|
161
|
+
const issue1 = createMockIssue({
|
|
162
|
+
number: 1,
|
|
163
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
164
|
+
itemId: 'item-1',
|
|
165
|
+
});
|
|
166
|
+
const issue2 = createMockIssue({
|
|
167
|
+
number: 2,
|
|
168
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
169
|
+
itemId: 'item-2',
|
|
170
|
+
});
|
|
171
|
+
const project = createMockProject();
|
|
172
|
+
const updateError = new Error('updateStatus failed for issue1');
|
|
173
|
+
mockIssueRepository.updateStatus
|
|
174
|
+
.mockRejectedValueOnce(updateError)
|
|
175
|
+
.mockResolvedValueOnce(undefined);
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
useCase.run({ project, issues: [issue1, issue2] }),
|
|
179
|
+
).rejects.toBeInstanceOf(AggregateError);
|
|
180
|
+
|
|
181
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(2);
|
|
182
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
183
|
+
project,
|
|
184
|
+
issue2,
|
|
185
|
+
'awaiting-workspace-id',
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Issue } from '../entities/Issue';
|
|
2
|
+
import { Project } from '../entities/Project';
|
|
3
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
|
+
import {
|
|
5
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
6
|
+
DONE_STATUS_NAME,
|
|
7
|
+
} from '../entities/WorkflowStatus';
|
|
8
|
+
|
|
9
|
+
export class ReopenedDoneIssueRevertUseCase {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly issueRepository: Pick<IssueRepository, 'updateStatus'>,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
run = async (params: {
|
|
15
|
+
project: Project;
|
|
16
|
+
issues: Issue[];
|
|
17
|
+
}): Promise<number> => {
|
|
18
|
+
const awaitingWorkspaceStatusOption = params.project.status.statuses.find(
|
|
19
|
+
(s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
|
|
20
|
+
);
|
|
21
|
+
if (!awaitingWorkspaceStatusOption) {
|
|
22
|
+
console.error(
|
|
23
|
+
`Awaiting Workspace status option '${AWAITING_WORKSPACE_STATUS_NAME}' not found in project.`,
|
|
24
|
+
);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const itemsToRevert = params.issues.filter(
|
|
29
|
+
(issue) =>
|
|
30
|
+
issue.status === DONE_STATUS_NAME &&
|
|
31
|
+
issue.stateReason === 'REOPENED' &&
|
|
32
|
+
!issue.isPr,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
let revertedCount = 0;
|
|
36
|
+
const errors: unknown[] = [];
|
|
37
|
+
for (const issue of itemsToRevert) {
|
|
38
|
+
try {
|
|
39
|
+
await this.issueRepository.updateStatus(
|
|
40
|
+
params.project,
|
|
41
|
+
issue,
|
|
42
|
+
awaitingWorkspaceStatusOption.id,
|
|
43
|
+
);
|
|
44
|
+
revertedCount++;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
errors.push(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (errors.length > 0) {
|
|
50
|
+
throw new AggregateError(
|
|
51
|
+
errors,
|
|
52
|
+
`Failed to revert ${errors.length} issue(s) from ${DONE_STATUS_NAME} to ${AWAITING_WORKSPACE_STATUS_NAME}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return revertedCount;
|
|
56
|
+
};
|
|
57
|
+
}
|