github-issue-tower-defence-management 1.44.10 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/bin/adapter/entry-points/cli/index.js +0 -3
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +0 -2
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +0 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/BaseGitHubRepository.js +5 -22
- package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +40 -0
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +17 -2
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -2
- package/src/adapter/entry-points/cli/index.test.ts +0 -3
- package/src/adapter/entry-points/cli/index.ts +2 -3
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +0 -6
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +0 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +0 -6
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -2
- package/src/adapter/repositories/BaseGitHubRepository.test.ts +3 -48
- package/src/adapter/repositories/BaseGitHubRepository.ts +5 -33
- package/src/adapter/repositories/GraphqlProjectRepository.test.ts +72 -0
- package/src/adapter/repositories/GraphqlProjectRepository.ts +57 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +58 -3
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +125 -7
- package/src/domain/usecases/StartPreparationUseCase.ts +32 -3
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +7 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/BaseGitHubRepository.d.ts +0 -1
- package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts +5 -2
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +3 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/repositories/CheerioProjectRepository.js +0 -45
- package/bin/adapter/repositories/CheerioProjectRepository.js.map +0 -1
- package/src/adapter/repositories/CheerioProjectRepository.test.ts +0 -122
- package/src/adapter/repositories/CheerioProjectRepository.ts +0 -65
- package/types/adapter/repositories/CheerioProjectRepository.d.ts +0 -17
- package/types/adapter/repositories/CheerioProjectRepository.d.ts.map +0 -1
|
@@ -25,7 +25,6 @@ import { RestIssueRepository } from '../../repositories/issue/RestIssueRepositor
|
|
|
25
25
|
import { GraphqlProjectItemRepository } from '../../repositories/issue/GraphqlProjectItemRepository';
|
|
26
26
|
import { ApiV3CheerioRestIssueRepository } from '../../repositories/issue/ApiV3CheerioRestIssueRepository';
|
|
27
27
|
import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
|
|
28
|
-
import { CheerioProjectRepository } from '../../repositories/CheerioProjectRepository';
|
|
29
28
|
import { BaseGitHubRepository } from '../../repositories/BaseGitHubRepository';
|
|
30
29
|
import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
|
|
31
30
|
import { OauthAPIProxyClaudeRepository } from '../../repositories/OauthAPIProxyClaudeRepository';
|
|
@@ -257,7 +256,7 @@ program
|
|
|
257
256
|
);
|
|
258
257
|
const projectRepository = {
|
|
259
258
|
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
260
|
-
|
|
259
|
+
|
|
261
260
|
prepareStatus: async (
|
|
262
261
|
_name: string,
|
|
263
262
|
project: Project,
|
|
@@ -490,7 +489,7 @@ program
|
|
|
490
489
|
);
|
|
491
490
|
const projectRepository = {
|
|
492
491
|
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
493
|
-
|
|
492
|
+
|
|
494
493
|
prepareStatus: async (
|
|
495
494
|
_name: string,
|
|
496
495
|
project: Project,
|
|
@@ -2,7 +2,6 @@ import fs from 'fs';
|
|
|
2
2
|
import YAML from 'yaml';
|
|
3
3
|
|
|
4
4
|
jest.mock('fs');
|
|
5
|
-
jest.mock('gh-cookie', () => ({ getCookieContent: jest.fn() }));
|
|
6
5
|
jest.mock('../../repositories/LocalStorageRepository');
|
|
7
6
|
jest.mock('../../repositories/GraphqlProjectRepository');
|
|
8
7
|
jest.mock('../../repositories/issue/ApiV3IssueRepository');
|
|
@@ -11,7 +10,6 @@ jest.mock('../../repositories/issue/GraphqlProjectItemRepository');
|
|
|
11
10
|
jest.mock('../../repositories/issue/ApiV3CheerioRestIssueRepository');
|
|
12
11
|
jest.mock('../../repositories/LocalStorageCacheRepository');
|
|
13
12
|
jest.mock('../../repositories/BaseGitHubRepository');
|
|
14
|
-
jest.mock('../../repositories/CheerioProjectRepository');
|
|
15
13
|
|
|
16
14
|
const mockRun = jest.fn().mockResolvedValue({
|
|
17
15
|
project: {},
|
|
@@ -28,14 +26,12 @@ jest.mock('../../../domain/usecases/GetStoryObjectMapUseCase', () => ({
|
|
|
28
26
|
|
|
29
27
|
import { GetStoryObjectMapUseCaseHandler } from './GetStoryObjectMapUseCaseHandler';
|
|
30
28
|
import { GraphqlProjectRepository } from '../../repositories/GraphqlProjectRepository';
|
|
31
|
-
import { CheerioProjectRepository } from '../../repositories/CheerioProjectRepository';
|
|
32
29
|
import { ApiV3IssueRepository } from '../../repositories/issue/ApiV3IssueRepository';
|
|
33
30
|
import { RestIssueRepository } from '../../repositories/issue/RestIssueRepository';
|
|
34
31
|
import { GraphqlProjectItemRepository } from '../../repositories/issue/GraphqlProjectItemRepository';
|
|
35
32
|
import { ApiV3CheerioRestIssueRepository } from '../../repositories/issue/ApiV3CheerioRestIssueRepository';
|
|
36
33
|
|
|
37
34
|
const MockedGraphqlProjectRepository = jest.mocked(GraphqlProjectRepository);
|
|
38
|
-
const MockedCheerioProjectRepository = jest.mocked(CheerioProjectRepository);
|
|
39
35
|
const MockedApiV3IssueRepository = jest.mocked(ApiV3IssueRepository);
|
|
40
36
|
const MockedRestIssueRepository = jest.mocked(RestIssueRepository);
|
|
41
37
|
const MockedGraphqlProjectItemRepository = jest.mocked(
|
|
@@ -122,7 +118,6 @@ describe('GetStoryObjectMapUseCaseHandler', () => {
|
|
|
122
118
|
|
|
123
119
|
for (const MockedClass of [
|
|
124
120
|
MockedGraphqlProjectRepository,
|
|
125
|
-
MockedCheerioProjectRepository,
|
|
126
121
|
MockedApiV3IssueRepository,
|
|
127
122
|
MockedRestIssueRepository,
|
|
128
123
|
MockedGraphqlProjectItemRepository,
|
|
@@ -159,7 +154,6 @@ describe('GetStoryObjectMapUseCaseHandler', () => {
|
|
|
159
154
|
|
|
160
155
|
for (const MockedClass of [
|
|
161
156
|
MockedGraphqlProjectRepository,
|
|
162
|
-
MockedCheerioProjectRepository,
|
|
163
157
|
MockedApiV3IssueRepository,
|
|
164
158
|
MockedRestIssueRepository,
|
|
165
159
|
MockedGraphqlProjectItemRepository,
|
|
@@ -11,7 +11,6 @@ import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCach
|
|
|
11
11
|
import { Issue } from '../../../domain/entities/Issue';
|
|
12
12
|
import { Project } from '../../../domain/entities/Project';
|
|
13
13
|
import { BaseGitHubRepository } from '../../repositories/BaseGitHubRepository';
|
|
14
|
-
import { CheerioProjectRepository } from '../../repositories/CheerioProjectRepository';
|
|
15
14
|
import { GetStoryObjectMapUseCase } from '../../../domain/usecases/GetStoryObjectMapUseCase';
|
|
16
15
|
import { StoryObjectMap } from '../../../domain/entities/StoryObjectMap';
|
|
17
16
|
|
|
@@ -65,7 +64,6 @@ export class GetStoryObjectMapUseCaseHandler {
|
|
|
65
64
|
];
|
|
66
65
|
const projectRepository = {
|
|
67
66
|
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
68
|
-
...new CheerioProjectRepository(...githubRepositoryParams),
|
|
69
67
|
};
|
|
70
68
|
const apiV3IssueRepository = new ApiV3IssueRepository(
|
|
71
69
|
...githubRepositoryParams,
|
|
@@ -3,12 +3,10 @@ import YAML from 'yaml';
|
|
|
3
3
|
import type { HandleScheduledEventUseCase } from '../../../domain/usecases/HandleScheduledEventUseCase';
|
|
4
4
|
|
|
5
5
|
jest.mock('fs');
|
|
6
|
-
jest.mock('gh-cookie', () => ({ getCookieContent: jest.fn() }));
|
|
7
6
|
jest.mock('../../repositories/SystemDateRepository');
|
|
8
7
|
jest.mock('../../repositories/LocalStorageRepository');
|
|
9
8
|
jest.mock('../../repositories/GoogleSpreadsheetRepository');
|
|
10
9
|
jest.mock('../../repositories/GraphqlProjectRepository');
|
|
11
|
-
jest.mock('../../repositories/CheerioProjectRepository');
|
|
12
10
|
jest.mock('../../repositories/issue/ApiV3IssueRepository');
|
|
13
11
|
jest.mock('../../repositories/issue/RestIssueRepository');
|
|
14
12
|
jest.mock('../../repositories/issue/GraphqlProjectItemRepository');
|
|
@@ -118,14 +116,12 @@ jest.mock('../../repositories/FetchWebhookRepository', () => ({
|
|
|
118
116
|
|
|
119
117
|
import { HandleScheduledEventUseCaseHandler } from './HandleScheduledEventUseCaseHandler';
|
|
120
118
|
import { GraphqlProjectRepository } from '../../repositories/GraphqlProjectRepository';
|
|
121
|
-
import { CheerioProjectRepository } from '../../repositories/CheerioProjectRepository';
|
|
122
119
|
import { ApiV3IssueRepository } from '../../repositories/issue/ApiV3IssueRepository';
|
|
123
120
|
import { RestIssueRepository } from '../../repositories/issue/RestIssueRepository';
|
|
124
121
|
import { GraphqlProjectItemRepository } from '../../repositories/issue/GraphqlProjectItemRepository';
|
|
125
122
|
import { ApiV3CheerioRestIssueRepository } from '../../repositories/issue/ApiV3CheerioRestIssueRepository';
|
|
126
123
|
|
|
127
124
|
const MockedGraphqlProjectRepository = jest.mocked(GraphqlProjectRepository);
|
|
128
|
-
const MockedCheerioProjectRepository = jest.mocked(CheerioProjectRepository);
|
|
129
125
|
const MockedApiV3IssueRepository = jest.mocked(ApiV3IssueRepository);
|
|
130
126
|
const MockedRestIssueRepository = jest.mocked(RestIssueRepository);
|
|
131
127
|
const MockedGraphqlProjectItemRepository = jest.mocked(
|
|
@@ -211,7 +207,6 @@ describe('HandleScheduledEventUseCaseHandler', () => {
|
|
|
211
207
|
|
|
212
208
|
for (const MockedClass of [
|
|
213
209
|
MockedGraphqlProjectRepository,
|
|
214
|
-
MockedCheerioProjectRepository,
|
|
215
210
|
MockedApiV3IssueRepository,
|
|
216
211
|
MockedRestIssueRepository,
|
|
217
212
|
MockedGraphqlProjectItemRepository,
|
|
@@ -248,7 +243,6 @@ describe('HandleScheduledEventUseCaseHandler', () => {
|
|
|
248
243
|
|
|
249
244
|
for (const MockedClass of [
|
|
250
245
|
MockedGraphqlProjectRepository,
|
|
251
|
-
MockedCheerioProjectRepository,
|
|
252
246
|
MockedApiV3IssueRepository,
|
|
253
247
|
MockedRestIssueRepository,
|
|
254
248
|
MockedGraphqlProjectItemRepository,
|
|
@@ -29,7 +29,6 @@ import { ConvertCheckboxToIssueInStoryIssueUseCase } from '../../../domain/useca
|
|
|
29
29
|
import { ChangeStatusByStoryColorUseCase } from '../../../domain/usecases/ChangeStatusByStoryColorUseCase';
|
|
30
30
|
import { SetNoStoryIssueToStoryUseCase } from '../../../domain/usecases/SetNoStoryIssueToStoryUseCase';
|
|
31
31
|
import { CreateNewStoryByLabelUseCase } from '../../../domain/usecases/CreateNewStoryByLabelUseCase';
|
|
32
|
-
import { CheerioProjectRepository } from '../../repositories/CheerioProjectRepository';
|
|
33
32
|
import { ProjectRepository } from '../../../domain/usecases/adapter-interfaces/ProjectRepository';
|
|
34
33
|
import { AssignNoAssigneeIssueToManagerUseCase } from '../../../domain/usecases/AssignNoAssigneeIssueToManagerUseCase';
|
|
35
34
|
import { UpdateIssueStatusByLabelUseCase } from '../../../domain/usecases/UpdateIssueStatusByLabelUseCase';
|
|
@@ -177,7 +176,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
177
176
|
];
|
|
178
177
|
const projectRepository: ProjectRepository = {
|
|
179
178
|
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
180
|
-
...new CheerioProjectRepository(...githubRepositoryParams),
|
|
181
179
|
prepareStatus: async (
|
|
182
180
|
_name: string,
|
|
183
181
|
project: Project,
|
|
@@ -19,12 +19,6 @@ import fs from 'fs';
|
|
|
19
19
|
import { BaseGitHubRepository } from './BaseGitHubRepository';
|
|
20
20
|
import resetAllMocks = jest.resetAllMocks;
|
|
21
21
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
22
|
-
|
|
23
|
-
const mockGetCookieContent = jest.fn<Promise<unknown>, unknown[]>();
|
|
24
|
-
jest.mock('gh-cookie', () => ({
|
|
25
|
-
getCookieContent: (...args: unknown[]): Promise<unknown> =>
|
|
26
|
-
mockGetCookieContent(...args),
|
|
27
|
-
}));
|
|
28
22
|
describe('BaseGitHubRepository', () => {
|
|
29
23
|
const jsonFilePath = './tmp/github.com.cookies.json';
|
|
30
24
|
const localStorageRepository = new LocalStorageRepository();
|
|
@@ -133,7 +127,6 @@ describe('BaseGitHubRepository', () => {
|
|
|
133
127
|
'dummy-password',
|
|
134
128
|
'dummy-authenticator-key',
|
|
135
129
|
);
|
|
136
|
-
this.cookieRefreshRetryDelayMs = 0;
|
|
137
130
|
}
|
|
138
131
|
}
|
|
139
132
|
|
|
@@ -153,8 +146,6 @@ describe('BaseGitHubRepository', () => {
|
|
|
153
146
|
beforeEach(() => {
|
|
154
147
|
mockKyGet.mockReset().mockReturnValue({ text: mockKyGetText });
|
|
155
148
|
mockKyGetText.mockReset();
|
|
156
|
-
mockGetCookieContent.mockReset();
|
|
157
|
-
mockGetCookieContent.mockResolvedValue(validCookieJson);
|
|
158
149
|
fs.writeFileSync(refreshCookieJsonFilePath, validCookieJson);
|
|
159
150
|
});
|
|
160
151
|
|
|
@@ -182,10 +173,7 @@ describe('BaseGitHubRepository', () => {
|
|
|
182
173
|
it('should fail when HTML contains username in content but not in user-login meta tag (not logged in)', async () => {
|
|
183
174
|
const repository = new RefreshTestRepository();
|
|
184
175
|
const notLoggedInHtml = `<html><head><meta name="user-login" content=""></head><body><h1>${ghUserName}</h1><p>Public profile</p></body></html>`;
|
|
185
|
-
mockKyGetText
|
|
186
|
-
.mockResolvedValueOnce(notLoggedInHtml)
|
|
187
|
-
.mockResolvedValueOnce(notLoggedInHtml)
|
|
188
|
-
.mockResolvedValueOnce(notLoggedInHtml);
|
|
176
|
+
mockKyGetText.mockResolvedValueOnce(notLoggedInHtml);
|
|
189
177
|
|
|
190
178
|
await expect(repository.refreshCookie()).rejects.toThrow(
|
|
191
179
|
'Failed to refresh cookie',
|
|
@@ -210,47 +198,14 @@ describe('BaseGitHubRepository', () => {
|
|
|
210
198
|
);
|
|
211
199
|
});
|
|
212
200
|
|
|
213
|
-
it('should throw when
|
|
201
|
+
it('should throw when the authentication check fails', async () => {
|
|
214
202
|
const repository = new RefreshTestRepository();
|
|
215
203
|
const notLoggedInHtml = `<html><head><meta name="user-login" content=""></head><body></body></html>`;
|
|
216
|
-
mockKyGetText
|
|
217
|
-
.mockResolvedValueOnce(notLoggedInHtml)
|
|
218
|
-
.mockResolvedValueOnce(notLoggedInHtml)
|
|
219
|
-
.mockResolvedValueOnce(notLoggedInHtml);
|
|
204
|
+
mockKyGetText.mockResolvedValueOnce(notLoggedInHtml);
|
|
220
205
|
|
|
221
206
|
await expect(repository.refreshCookie()).rejects.toThrow(
|
|
222
207
|
'Failed to refresh cookie',
|
|
223
208
|
);
|
|
224
209
|
});
|
|
225
|
-
|
|
226
|
-
it('should reset cookie cache before regenerating so new cookie is used', async () => {
|
|
227
|
-
const repository = new RefreshTestRepository();
|
|
228
|
-
mockKyGetText
|
|
229
|
-
.mockResolvedValueOnce(
|
|
230
|
-
`<html><head><meta name="user-login" content=""></head><body></body></html>`,
|
|
231
|
-
)
|
|
232
|
-
.mockResolvedValueOnce(
|
|
233
|
-
`<html><head><meta name="user-login" content="${ghUserName}"></head><body></body></html>`,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
await expect(repository.refreshCookie()).resolves.toBeUndefined();
|
|
237
|
-
expect(mockKyGet).toHaveBeenCalledTimes(2);
|
|
238
|
-
expect(mockGetCookieContent).toHaveBeenCalledTimes(1);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should succeed on third attempt after two failed cookie refresh attempts', async () => {
|
|
242
|
-
const repository = new RefreshTestRepository();
|
|
243
|
-
const notLoggedInHtml = `<html><head><meta name="user-login" content=""></head><body></body></html>`;
|
|
244
|
-
mockKyGetText
|
|
245
|
-
.mockResolvedValueOnce(notLoggedInHtml)
|
|
246
|
-
.mockResolvedValueOnce(notLoggedInHtml)
|
|
247
|
-
.mockResolvedValueOnce(
|
|
248
|
-
`<html><head><meta name="user-login" content="${ghUserName}"></head><body></body></html>`,
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
await expect(repository.refreshCookie()).resolves.toBeUndefined();
|
|
252
|
-
expect(mockKyGet).toHaveBeenCalledTimes(3);
|
|
253
|
-
expect(mockGetCookieContent).toHaveBeenCalledTimes(2);
|
|
254
|
-
});
|
|
255
210
|
});
|
|
256
211
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { promises as fsPromises } from 'fs';
|
|
2
2
|
import { serialize } from 'cookie';
|
|
3
|
-
import { getCookieContent } from 'gh-cookie';
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
6
5
|
import ky from 'ky';
|
|
@@ -18,7 +17,6 @@ interface Cookie {
|
|
|
18
17
|
|
|
19
18
|
export class BaseGitHubRepository {
|
|
20
19
|
cookie: string | null;
|
|
21
|
-
protected cookieRefreshRetryDelayMs = 3000;
|
|
22
20
|
constructor(
|
|
23
21
|
readonly localStorageRepository: LocalStorageRepository,
|
|
24
22
|
readonly jsonFilePath: string = './tmp/github.com.cookies.json',
|
|
@@ -82,19 +80,7 @@ export class BaseGitHubRepository {
|
|
|
82
80
|
};
|
|
83
81
|
protected createCookieStringFromFile = async (): Promise<string> => {
|
|
84
82
|
if (!fs.existsSync(this.jsonFilePath)) {
|
|
85
|
-
|
|
86
|
-
!this.ghUserName ||
|
|
87
|
-
!this.ghUserPassword ||
|
|
88
|
-
!this.ghAuthenticatorKey
|
|
89
|
-
) {
|
|
90
|
-
throw new Error('No cookie file and no credentials provided');
|
|
91
|
-
}
|
|
92
|
-
const cookie = await getCookieContent(
|
|
93
|
-
this.ghUserName,
|
|
94
|
-
this.ghUserPassword,
|
|
95
|
-
this.ghAuthenticatorKey,
|
|
96
|
-
);
|
|
97
|
-
this.localStorageRepository.write(this.jsonFilePath, cookie);
|
|
83
|
+
throw new Error('No cookie file found');
|
|
98
84
|
}
|
|
99
85
|
const data = await fsPromises.readFile(this.jsonFilePath, {
|
|
100
86
|
encoding: 'utf-8',
|
|
@@ -169,24 +155,10 @@ export class BaseGitHubRepository {
|
|
|
169
155
|
);
|
|
170
156
|
}
|
|
171
157
|
const profileUrl = `https://github.com/${this.ghUserName}`;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await new Promise<void>((resolve) =>
|
|
177
|
-
setTimeout(resolve, this.cookieRefreshRetryDelayMs),
|
|
178
|
-
);
|
|
179
|
-
this.localStorageRepository.remove(this.jsonFilePath);
|
|
180
|
-
this.cookie = null;
|
|
181
|
-
}
|
|
182
|
-
const headers = await this.createHeader();
|
|
183
|
-
const html = await ky.get(profileUrl, { headers }).text();
|
|
184
|
-
if (
|
|
185
|
-
html.includes(`meta name="user-login" content="${this.ghUserName}"`)
|
|
186
|
-
) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
158
|
+
const headers = await this.createHeader();
|
|
159
|
+
const html = await ky.get(profileUrl, { headers }).text();
|
|
160
|
+
if (!html.includes(`meta name="user-login" content="${this.ghUserName}"`)) {
|
|
161
|
+
throw new Error('Failed to refresh cookie');
|
|
189
162
|
}
|
|
190
|
-
throw new Error('Failed to refresh cookie');
|
|
191
163
|
};
|
|
192
164
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GraphqlProjectRepository } from './GraphqlProjectRepository';
|
|
2
2
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
3
|
+
import { FieldOption, Project } from '../../domain/entities/Project';
|
|
3
4
|
|
|
4
5
|
describe('GraphqlProjectRepository', () => {
|
|
5
6
|
const localStorageRepository = new LocalStorageRepository();
|
|
@@ -29,6 +30,77 @@ describe('GraphqlProjectRepository', () => {
|
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
describe('updateStoryList', () => {
|
|
34
|
+
const storyFieldId = 'PVTSSF_lAHOAGJHa84AFhgFzg1oBms';
|
|
35
|
+
const existingStories: FieldOption[] = [
|
|
36
|
+
{ id: 'af410dae', name: 'story1', color: 'GRAY', description: '' },
|
|
37
|
+
{
|
|
38
|
+
id: '696ccdef',
|
|
39
|
+
name: 'Workflow Management',
|
|
40
|
+
color: 'GRAY',
|
|
41
|
+
description: '',
|
|
42
|
+
},
|
|
43
|
+
{ id: '4fa21881', name: 'test', color: 'GRAY', description: '' },
|
|
44
|
+
];
|
|
45
|
+
const testProject: Project = {
|
|
46
|
+
id: projectId,
|
|
47
|
+
url: projectUrl,
|
|
48
|
+
databaseId: 1447941,
|
|
49
|
+
name: 'V2 project on owner for testing',
|
|
50
|
+
completionDate50PercentConfidence: null,
|
|
51
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
52
|
+
nextActionDate: {
|
|
53
|
+
fieldId: 'PVTF_lAHOAGJHa84AFhgFzgVlnK4',
|
|
54
|
+
name: 'NextActionDate',
|
|
55
|
+
},
|
|
56
|
+
nextActionHour: null,
|
|
57
|
+
remainingEstimationMinutes: null,
|
|
58
|
+
status: {
|
|
59
|
+
fieldId: 'PVTSSF_lAHOAGJHa84AFhgFzgDLt0c',
|
|
60
|
+
name: 'Status',
|
|
61
|
+
statuses: [],
|
|
62
|
+
},
|
|
63
|
+
story: {
|
|
64
|
+
fieldId: storyFieldId,
|
|
65
|
+
databaseId: 224921195,
|
|
66
|
+
name: 'Story',
|
|
67
|
+
stories: existingStories,
|
|
68
|
+
workflowManagementStory: {
|
|
69
|
+
id: '696ccdef',
|
|
70
|
+
name: 'Workflow Management',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
it('should add a new option while preserving all existing options', async () => {
|
|
76
|
+
const newOption: Omit<FieldOption, 'id'> & { id: null } = {
|
|
77
|
+
id: null,
|
|
78
|
+
name: 'test-story-from-graphql-unit-test',
|
|
79
|
+
color: 'BLUE',
|
|
80
|
+
description: 'created by graphql unit test',
|
|
81
|
+
};
|
|
82
|
+
const inputList: Parameters<typeof repository.updateStoryList>['1'] = [
|
|
83
|
+
...existingStories,
|
|
84
|
+
newOption,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const result = await repository.updateStoryList(testProject, inputList);
|
|
88
|
+
|
|
89
|
+
expect(result).toHaveLength(existingStories.length + 1);
|
|
90
|
+
existingStories.forEach((existing) => {
|
|
91
|
+
const found = result.find((r) => r.id === existing.id);
|
|
92
|
+
expect(found).toEqual(existing);
|
|
93
|
+
});
|
|
94
|
+
const added = result.find((r) => r.name === newOption.name);
|
|
95
|
+
expect(added).toBeDefined();
|
|
96
|
+
expect(added?.color).toEqual(newOption.color);
|
|
97
|
+
expect(added?.description).toEqual(newOption.description);
|
|
98
|
+
expect(added?.id).toBeDefined();
|
|
99
|
+
|
|
100
|
+
await repository.updateStoryList(testProject, existingStories);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
32
104
|
describe('getProject', () => {
|
|
33
105
|
it('should retrieve project details', async () => {
|
|
34
106
|
const project = await repository.getProject(projectId);
|
|
@@ -7,7 +7,10 @@ import { normalizeFieldName } from './utils';
|
|
|
7
7
|
export class GraphqlProjectRepository
|
|
8
8
|
extends BaseGitHubRepository
|
|
9
9
|
implements
|
|
10
|
-
Pick<
|
|
10
|
+
Pick<
|
|
11
|
+
ProjectRepository,
|
|
12
|
+
'getProject' | 'findProjectIdByUrl' | 'getByUrl' | 'updateStoryList'
|
|
13
|
+
>
|
|
11
14
|
{
|
|
12
15
|
extractProjectFromUrl = (
|
|
13
16
|
projectUrl: string,
|
|
@@ -306,4 +309,57 @@ export class GraphqlProjectRepository
|
|
|
306
309
|
}
|
|
307
310
|
return project;
|
|
308
311
|
};
|
|
312
|
+
updateStoryList = async (
|
|
313
|
+
project: Project,
|
|
314
|
+
newStoryList: (Omit<FieldOption, 'id'> & {
|
|
315
|
+
id: FieldOption['id'] | null;
|
|
316
|
+
})[],
|
|
317
|
+
): Promise<FieldOption[]> => {
|
|
318
|
+
if (!project.story) {
|
|
319
|
+
throw new Error('Project has no story field');
|
|
320
|
+
}
|
|
321
|
+
const mutation = `mutation UpdateStoryOptions($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
|
|
322
|
+
updateProjectV2Field(input: {
|
|
323
|
+
fieldId: $fieldId
|
|
324
|
+
singleSelectOptions: $options
|
|
325
|
+
}) {
|
|
326
|
+
projectV2Field {
|
|
327
|
+
... on ProjectV2SingleSelectField {
|
|
328
|
+
options {
|
|
329
|
+
id
|
|
330
|
+
name
|
|
331
|
+
color
|
|
332
|
+
description
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}`;
|
|
338
|
+
const variables = {
|
|
339
|
+
fieldId: project.story.fieldId,
|
|
340
|
+
options: newStoryList.map(({ id, name, color, description }) => ({
|
|
341
|
+
...(id !== null ? { id } : {}),
|
|
342
|
+
name,
|
|
343
|
+
color,
|
|
344
|
+
description,
|
|
345
|
+
})),
|
|
346
|
+
};
|
|
347
|
+
const response = await ky
|
|
348
|
+
.post('https://api.github.com/graphql', {
|
|
349
|
+
json: { query: mutation, variables },
|
|
350
|
+
headers: {
|
|
351
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
.json<{
|
|
355
|
+
data: {
|
|
356
|
+
updateProjectV2Field: {
|
|
357
|
+
projectV2Field: {
|
|
358
|
+
options: FieldOption[];
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
}>();
|
|
363
|
+
return response.data.updateProjectV2Field.projectV2Field.options;
|
|
364
|
+
};
|
|
309
365
|
}
|
|
@@ -26,6 +26,7 @@ type TimelineItem = {
|
|
|
26
26
|
url?: string;
|
|
27
27
|
number?: number;
|
|
28
28
|
state?: string;
|
|
29
|
+
createdAt?: string;
|
|
29
30
|
mergeable?: string;
|
|
30
31
|
headRefName?: string;
|
|
31
32
|
baseRefName?: string;
|
|
@@ -693,6 +694,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
693
694
|
return {
|
|
694
695
|
url: prUrl,
|
|
695
696
|
branchName: headRefName ?? null,
|
|
697
|
+
createdAt: new Date(0),
|
|
696
698
|
isConflicted,
|
|
697
699
|
isPassedAllCiJob,
|
|
698
700
|
isCiStateSuccess,
|
|
@@ -731,6 +733,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
731
733
|
url
|
|
732
734
|
number
|
|
733
735
|
state
|
|
736
|
+
createdAt
|
|
734
737
|
mergeable
|
|
735
738
|
headRefName
|
|
736
739
|
baseRefName
|
|
@@ -855,11 +858,17 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
855
858
|
const pr = item.source;
|
|
856
859
|
const prUrl = pr.url || '';
|
|
857
860
|
const baseRefName = pr.baseRefName ?? pr.baseRef?.name;
|
|
858
|
-
|
|
859
|
-
relatedPRsMap.set(
|
|
861
|
+
const prStatus = this.computePrStatus(
|
|
860
862
|
prUrl,
|
|
861
|
-
|
|
863
|
+
pr.headRefName,
|
|
864
|
+
baseRefName,
|
|
865
|
+
pr,
|
|
862
866
|
);
|
|
867
|
+
|
|
868
|
+
relatedPRsMap.set(prUrl, {
|
|
869
|
+
...prStatus,
|
|
870
|
+
createdAt: pr.createdAt ? new Date(pr.createdAt) : new Date(0),
|
|
871
|
+
});
|
|
863
872
|
}
|
|
864
873
|
|
|
865
874
|
hasNextPage = issueData.timelineItems.pageInfo.hasNextPage;
|
|
@@ -1017,4 +1026,50 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1017
1026
|
|
|
1018
1027
|
return this.computePrStatus(pr.url, pr.headRefName, pr.baseRefName, pr);
|
|
1019
1028
|
};
|
|
1029
|
+
|
|
1030
|
+
closePullRequest = async (prUrl: string): Promise<void> => {
|
|
1031
|
+
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|
|
1032
|
+
const response = await fetch(
|
|
1033
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
|
|
1034
|
+
{
|
|
1035
|
+
method: 'PATCH',
|
|
1036
|
+
headers: {
|
|
1037
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1038
|
+
'Content-Type': 'application/json',
|
|
1039
|
+
},
|
|
1040
|
+
body: JSON.stringify({ state: 'closed' }),
|
|
1041
|
+
},
|
|
1042
|
+
);
|
|
1043
|
+
if (!response.ok) {
|
|
1044
|
+
throw new Error(`Failed to close PR ${prUrl}: HTTP ${response.status}`);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
deletePullRequestBranch = async (
|
|
1049
|
+
prUrl: string,
|
|
1050
|
+
branchName: string,
|
|
1051
|
+
): Promise<void> => {
|
|
1052
|
+
const { owner, repo } = this.parseIssueUrl(prUrl);
|
|
1053
|
+
const response = await fetch(
|
|
1054
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branchName)}`,
|
|
1055
|
+
{
|
|
1056
|
+
method: 'DELETE',
|
|
1057
|
+
headers: {
|
|
1058
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
if (!response.ok && response.status !== 422) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`Failed to delete branch ${branchName} for PR ${prUrl}: HTTP ${response.status}`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
createCommentByUrl = async (
|
|
1070
|
+
issueOrPrUrl: string,
|
|
1071
|
+
commentBody: string,
|
|
1072
|
+
): Promise<void> => {
|
|
1073
|
+
await this.restIssueRepository.createComment(issueOrPrUrl, commentBody);
|
|
1074
|
+
};
|
|
1020
1075
|
}
|
|
@@ -82,6 +82,7 @@ const createMockProject = (): Project => ({
|
|
|
82
82
|
const createPassingPr = () => ({
|
|
83
83
|
url: 'https://github.com/user/repo/pull/5',
|
|
84
84
|
branchName: 'i1',
|
|
85
|
+
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
85
86
|
isConflicted: false,
|
|
86
87
|
isPassedAllCiJob: true,
|
|
87
88
|
isCiStateSuccess: true,
|