jira-sprinter 2.3.0 → 2.4.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/package.json +4 -2
- package/src/auto.ts +188 -0
- package/src/cli.ts +27 -1
- package/src/jira.ts +23 -19
- package/src/pp-sync.ts +73 -0
- package/src/product-pages.ts +89 -0
- package/src/schema/deadlines.ts +135 -0
- package/src/util.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jira-sprinter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Small CLI tool to manage sprints in JIRA Board",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"test": "tests"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "esbuild ./src/main.js --bundle --outdir=dist --platform=node --target=node20.0.0 --packages=bundle",
|
|
12
|
+
"build": "esbuild ./src/main.js --bundle --outdir=dist --platform=node --target=node20.0.0 --packages=bundle --external:kerberos",
|
|
13
13
|
"format": "prettier --write '**/*.ts'",
|
|
14
14
|
"format-check": "prettier --check '**/*.ts'",
|
|
15
15
|
"test": "vitest run --coverage",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"commander": "15.0.0",
|
|
42
42
|
"dotenv": "17.4.2",
|
|
43
43
|
"jira.js": "5.4.0",
|
|
44
|
+
"kerberos": "^7.0.0",
|
|
45
|
+
"product-pages": "^1.0.0",
|
|
44
46
|
"zod": "4.4.3"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
package/src/auto.ts
CHANGED
|
@@ -3,9 +3,20 @@ import { OptionValues } from 'commander';
|
|
|
3
3
|
|
|
4
4
|
import { Logger } from './logger';
|
|
5
5
|
import { Jira } from './jira';
|
|
6
|
+
import { ProductPages } from './product-pages';
|
|
7
|
+
import {
|
|
8
|
+
readDeadlines,
|
|
9
|
+
DEFAULT_DEADLINES_PATH,
|
|
10
|
+
computePreliminaryTestingDueDate,
|
|
11
|
+
computeQeTaskDueDate,
|
|
12
|
+
classifyScheduleTasks,
|
|
13
|
+
SCHEDULE_TASK_REGEX,
|
|
14
|
+
type ReleaseDeadlines,
|
|
15
|
+
} from './schema/deadlines';
|
|
6
16
|
|
|
7
17
|
export async function runAuto(options: OptionValues): Promise<void> {
|
|
8
18
|
const logger = new Logger(!!options.nocolor);
|
|
19
|
+
const deadlinesFile: string = options.deadlinesFile ?? DEFAULT_DEADLINES_PATH;
|
|
9
20
|
|
|
10
21
|
const jira = await Jira.getInstance(options.dry, logger, options.assignee);
|
|
11
22
|
|
|
@@ -18,6 +29,14 @@ export async function runAuto(options: OptionValues): Promise<void> {
|
|
|
18
29
|
`project = RHEL and issuetype in (Bug, Story, Vulnerability) and status != Closed`
|
|
19
30
|
);
|
|
20
31
|
|
|
32
|
+
const uniqueReleases = extractUniqueReleases(boardIssues);
|
|
33
|
+
const deadlinesDb = await loadDeadlines(
|
|
34
|
+
uniqueReleases,
|
|
35
|
+
deadlinesFile,
|
|
36
|
+
!!options.dry,
|
|
37
|
+
logger
|
|
38
|
+
);
|
|
39
|
+
|
|
21
40
|
const preliminaryTestingRequested = boardIssues.filter(
|
|
22
41
|
issue =>
|
|
23
42
|
issue.fields?.[jira.fields.preliminaryTesting]?.value === 'Requested' &&
|
|
@@ -90,12 +109,30 @@ export async function runAuto(options: OptionValues): Promise<void> {
|
|
|
90
109
|
logger.log(` ${chalk.green('Nothing to do')}`);
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
type DueDateJob = {
|
|
113
|
+
parentKey: string;
|
|
114
|
+
taskName: string;
|
|
115
|
+
summaryPrefix: string;
|
|
116
|
+
dueDate: string;
|
|
117
|
+
};
|
|
118
|
+
const dueDateJobs: DueDateJob[] = [];
|
|
119
|
+
|
|
93
120
|
for (const issue of preliminaryTestingRequested) {
|
|
94
121
|
if (!issue.key) {
|
|
95
122
|
continue;
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
await jira.createTasks(issue.key, [jira.preliminaryTestingTask.value]);
|
|
126
|
+
|
|
127
|
+
const dueDate = resolveDueDate(issue, 'preliminary', deadlinesDb, logger);
|
|
128
|
+
if (dueDate) {
|
|
129
|
+
dueDateJobs.push({
|
|
130
|
+
parentKey: issue.key,
|
|
131
|
+
taskName: jira.preliminaryTestingTask.name,
|
|
132
|
+
summaryPrefix: jira.preliminaryTestingTask.summary,
|
|
133
|
+
dueDate,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
99
136
|
}
|
|
100
137
|
|
|
101
138
|
for (const issue of preliminaryTestingFailed) {
|
|
@@ -128,6 +165,16 @@ export async function runAuto(options: OptionValues): Promise<void> {
|
|
|
128
165
|
}
|
|
129
166
|
|
|
130
167
|
await jira.createTasks(issue.key, [jira.qeTask.value]);
|
|
168
|
+
|
|
169
|
+
const dueDate = resolveDueDate(issue, 'qe', deadlinesDb, logger);
|
|
170
|
+
if (dueDate) {
|
|
171
|
+
dueDateJobs.push({
|
|
172
|
+
parentKey: issue.key,
|
|
173
|
+
taskName: jira.qeTask.name,
|
|
174
|
+
summaryPrefix: jira.qeTask.summary,
|
|
175
|
+
dueDate,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
131
178
|
}
|
|
132
179
|
|
|
133
180
|
for (const issue of issuesInReleasePending) {
|
|
@@ -150,5 +197,146 @@ export async function runAuto(options: OptionValues): Promise<void> {
|
|
|
150
197
|
await jira.closeTask(qeTask.outwardIssue?.key);
|
|
151
198
|
}
|
|
152
199
|
|
|
200
|
+
const dueDateResults = await Promise.allSettled(
|
|
201
|
+
dueDateJobs.map(job =>
|
|
202
|
+
setDueDateOnSplitTask(
|
|
203
|
+
jira,
|
|
204
|
+
job.parentKey,
|
|
205
|
+
job.taskName,
|
|
206
|
+
job.summaryPrefix,
|
|
207
|
+
job.dueDate,
|
|
208
|
+
logger
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
for (const result of dueDateResults) {
|
|
213
|
+
if (result.status === 'rejected') {
|
|
214
|
+
logger.log(chalk.yellow(` Due date operation failed: ${result.reason}`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
153
218
|
process.exit(0);
|
|
154
219
|
}
|
|
220
|
+
|
|
221
|
+
function extractUniqueReleases(
|
|
222
|
+
issues: { fields?: Record<string, any> }[]
|
|
223
|
+
): string[] {
|
|
224
|
+
const releases = new Set<string>();
|
|
225
|
+
for (const issue of issues) {
|
|
226
|
+
const fixVersions = issue.fields?.fixVersions;
|
|
227
|
+
if (Array.isArray(fixVersions)) {
|
|
228
|
+
for (const v of fixVersions as { name?: string }[]) {
|
|
229
|
+
if (v?.name) releases.add(v.name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return [...releases];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function loadDeadlines(
|
|
237
|
+
releases: string[],
|
|
238
|
+
deadlinesFile: string,
|
|
239
|
+
dry: boolean,
|
|
240
|
+
logger: Logger
|
|
241
|
+
): Promise<Record<string, ReleaseDeadlines>> {
|
|
242
|
+
const pp = ProductPages.getInstance(dry, logger);
|
|
243
|
+
|
|
244
|
+
// If authentication fails, fall back to the entire cached file.
|
|
245
|
+
try {
|
|
246
|
+
const whoami = await pp.whoami();
|
|
247
|
+
logger.log(chalk.dim(`Authenticated as: ${whoami.username}`));
|
|
248
|
+
} catch {
|
|
249
|
+
logger.log(
|
|
250
|
+
chalk.yellow('Product Pages unavailable, using cached deadlines')
|
|
251
|
+
);
|
|
252
|
+
const cached = readDeadlines(deadlinesFile);
|
|
253
|
+
if (!cached) {
|
|
254
|
+
logger.log(chalk.red(`No cached deadlines found at ${deadlinesFile}`));
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
logger.log(chalk.dim(`Using cached data from ${cached.updated_at}`));
|
|
258
|
+
return cached.releases;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Fetch per-release — skip individual failures so a single bad release
|
|
262
|
+
// does not discard fresh data already retrieved for other releases.
|
|
263
|
+
const result: Record<string, ReleaseDeadlines> = {};
|
|
264
|
+
for (const release of releases) {
|
|
265
|
+
try {
|
|
266
|
+
const tasks = await pp.getScheduleTasks(release, {
|
|
267
|
+
name__regex: SCHEDULE_TASK_REGEX,
|
|
268
|
+
});
|
|
269
|
+
result[release] = classifyScheduleTasks(tasks);
|
|
270
|
+
} catch {
|
|
271
|
+
logger.log(
|
|
272
|
+
chalk.yellow(` Could not fetch deadlines for ${release}, skipping`)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function setDueDateOnSplitTask(
|
|
280
|
+
jira: Jira,
|
|
281
|
+
parentKey: string,
|
|
282
|
+
taskName: string,
|
|
283
|
+
summaryPrefix: string,
|
|
284
|
+
dueDate: string,
|
|
285
|
+
logger: Logger
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
for (let attempt = 1; attempt <= 10; attempt++) {
|
|
288
|
+
const tasks = await jira.getlinkedTasks(parentKey, [taskName]);
|
|
289
|
+
const splitTask = tasks.find(t =>
|
|
290
|
+
t.fields?.summary?.startsWith(summaryPrefix)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (splitTask?.key) {
|
|
294
|
+
await jira.setDueDate(splitTask.key, dueDate);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (attempt < 10) {
|
|
299
|
+
logger.log(chalk.dim(` Waiting for ${taskName} on ${parentKey}...`));
|
|
300
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
logger.log(
|
|
305
|
+
chalk.yellow(
|
|
306
|
+
` ${taskName} not found for ${parentKey} — could not set due date`
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveDueDate(
|
|
312
|
+
issue: { key?: string; fields?: Record<string, any> },
|
|
313
|
+
taskType: 'preliminary' | 'qe',
|
|
314
|
+
deadlinesDb: Record<string, ReleaseDeadlines>,
|
|
315
|
+
logger: Logger
|
|
316
|
+
): string | null {
|
|
317
|
+
const fixVersion = issue.fields?.fixVersions?.[0]?.name;
|
|
318
|
+
if (!fixVersion) {
|
|
319
|
+
logger.log(
|
|
320
|
+
chalk.dim(` No fixVersion on ${issue.key} — skipping due date`)
|
|
321
|
+
);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const deadlines = deadlinesDb[fixVersion];
|
|
326
|
+
if (!deadlines) {
|
|
327
|
+
logger.log(
|
|
328
|
+
chalk.dim(
|
|
329
|
+
` No deadlines for ${fixVersion} — skipping due date on ${issue.key}`
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const isZStream = fixVersion.endsWith('.z');
|
|
336
|
+
|
|
337
|
+
if (taskType === 'preliminary') {
|
|
338
|
+
return computePreliminaryTestingDueDate(deadlines, isZStream);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return computeQeTaskDueDate(deadlines, isZStream);
|
|
342
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Jira } from './jira';
|
|
|
7
7
|
import { Logger } from './logger';
|
|
8
8
|
import { getDefaultValue, getOptions } from './util';
|
|
9
9
|
import { runAuto } from './auto';
|
|
10
|
+
import { runPpSync } from './pp-sync';
|
|
10
11
|
|
|
11
12
|
import { SearchResults } from 'jira.js/dist/esm/types/agile/models';
|
|
12
13
|
import {
|
|
@@ -24,7 +25,7 @@ export function cli(): Command {
|
|
|
24
25
|
program
|
|
25
26
|
.name('jira-sprinter')
|
|
26
27
|
.description('🏃 Small CLI tool to manage sprints in JIRA Board')
|
|
27
|
-
.version('2.
|
|
28
|
+
.version('2.4.0');
|
|
28
29
|
|
|
29
30
|
program.addCommand(
|
|
30
31
|
new Command('auto')
|
|
@@ -42,11 +43,36 @@ export function cli(): Command {
|
|
|
42
43
|
'Jira Components',
|
|
43
44
|
getDefaultValue('COMPONENTS')
|
|
44
45
|
)
|
|
46
|
+
.option(
|
|
47
|
+
'--deadlines-file [path]',
|
|
48
|
+
'Path to deadlines JSON file',
|
|
49
|
+
getDefaultValue('DEADLINES_FILE')
|
|
50
|
+
)
|
|
45
51
|
.action(async (_opts, command) => {
|
|
46
52
|
await runAuto(command.optsWithGlobals());
|
|
47
53
|
})
|
|
48
54
|
);
|
|
49
55
|
|
|
56
|
+
program.addCommand(
|
|
57
|
+
new Command('pp-sync')
|
|
58
|
+
.description(
|
|
59
|
+
'Fetch REL_PREP and ITM 26 deadlines from Product Pages and save to a local file'
|
|
60
|
+
)
|
|
61
|
+
.argument(
|
|
62
|
+
'<releases...>',
|
|
63
|
+
'Release shortnames (e.g. rhel-9.9 rhel-9.8.z)'
|
|
64
|
+
)
|
|
65
|
+
.option('-x, --dry', 'dry run', getDefaultValue('DRY'))
|
|
66
|
+
.option(
|
|
67
|
+
'--deadlines-file [path]',
|
|
68
|
+
'Path to deadlines JSON file',
|
|
69
|
+
getDefaultValue('DEADLINES_FILE')
|
|
70
|
+
)
|
|
71
|
+
.action(async (releases: string[], _opts, command) => {
|
|
72
|
+
await runPpSync(releases, command.optsWithGlobals());
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
50
76
|
program
|
|
51
77
|
.option('-b, --board [board]', 'Jira Board ID', getDefaultValue('BOARD'))
|
|
52
78
|
.option(
|
package/src/jira.ts
CHANGED
|
@@ -180,6 +180,7 @@ export class Jira {
|
|
|
180
180
|
'assignee',
|
|
181
181
|
'priority',
|
|
182
182
|
'components',
|
|
183
|
+
'fixVersions',
|
|
183
184
|
this.fields.storyPoints,
|
|
184
185
|
this.fields.severity,
|
|
185
186
|
this.fields.preliminaryTesting,
|
|
@@ -199,26 +200,12 @@ export class Jira {
|
|
|
199
200
|
this.logger.log(
|
|
200
201
|
` ${chalk.dim(`Fetching linked tasks for ${issue} (dry-run)`)}`
|
|
201
202
|
);
|
|
202
|
-
return
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
summary: '[DEV Task] Test Task',
|
|
207
|
-
},
|
|
203
|
+
return expectedTasks.map((name, i) => ({
|
|
204
|
+
key: `${issue}-SPLIT-${i + 1}`,
|
|
205
|
+
fields: {
|
|
206
|
+
summary: `[${name}]: ${issue} (dry-run)`,
|
|
208
207
|
},
|
|
209
|
-
|
|
210
|
-
key: 'RHEL-1235',
|
|
211
|
-
fields: {
|
|
212
|
-
summary: '[QE Task] Test Task',
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
key: 'RHEL-1236',
|
|
217
|
-
fields: {
|
|
218
|
-
summary: '[Upstream] Test Task',
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
] as unknown as Issue[];
|
|
208
|
+
})) as unknown as Issue[];
|
|
222
209
|
}
|
|
223
210
|
|
|
224
211
|
const response =
|
|
@@ -300,6 +287,23 @@ export class Jira {
|
|
|
300
287
|
});
|
|
301
288
|
}
|
|
302
289
|
|
|
290
|
+
async setDueDate(issue: string, dueDate: string) {
|
|
291
|
+
if (this.dry) {
|
|
292
|
+
this.logger.log(
|
|
293
|
+
` ${chalk.dim(`Setting due date ${dueDate} on ${issue} (dry-run)`)}`
|
|
294
|
+
);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.logger.log(
|
|
299
|
+
` ${chalk.cyan(`Setting due date ${dueDate} on ${issue}`)}`
|
|
300
|
+
);
|
|
301
|
+
await this.api.issues.editIssue({
|
|
302
|
+
issueIdOrKey: issue,
|
|
303
|
+
fields: { duedate: dueDate },
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
303
307
|
getIssueURL(issue: string) {
|
|
304
308
|
return `${this.instance}/browse/${issue}`;
|
|
305
309
|
}
|
package/src/pp-sync.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { OptionValues } from 'commander';
|
|
3
|
+
|
|
4
|
+
import { Logger } from './logger';
|
|
5
|
+
import { ProductPages } from './product-pages';
|
|
6
|
+
import {
|
|
7
|
+
readDeadlines,
|
|
8
|
+
writeDeadlines,
|
|
9
|
+
DEFAULT_DEADLINES_PATH,
|
|
10
|
+
classifyScheduleTasks,
|
|
11
|
+
SCHEDULE_TASK_REGEX,
|
|
12
|
+
type DeadlinesFile,
|
|
13
|
+
ReleaseDeadlines,
|
|
14
|
+
} from './schema/deadlines';
|
|
15
|
+
|
|
16
|
+
export async function runPpSync(
|
|
17
|
+
releases: string[],
|
|
18
|
+
options: OptionValues
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const logger = new Logger(!!options.nocolor);
|
|
21
|
+
const pp = ProductPages.getInstance(!!options.dry, logger);
|
|
22
|
+
const filePath: string = options.deadlinesFile ?? DEFAULT_DEADLINES_PATH;
|
|
23
|
+
|
|
24
|
+
const whoami = await pp.whoami();
|
|
25
|
+
logger.log(chalk.dim(`Authenticated as: ${whoami.username}`));
|
|
26
|
+
|
|
27
|
+
const existing = readDeadlines(filePath);
|
|
28
|
+
const mergedReleases: Record<string, ReleaseDeadlines> = {
|
|
29
|
+
...(existing?.releases ?? {}),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (const release of releases) {
|
|
33
|
+
logger.log(`\n${chalk.cyan('Syncing')} ${chalk.bold(release)}...`);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const tasks = await pp.getScheduleTasks(release, {
|
|
37
|
+
name__regex: SCHEDULE_TASK_REGEX,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const deadlines = classifyScheduleTasks(tasks);
|
|
41
|
+
mergedReleases[release] = deadlines;
|
|
42
|
+
|
|
43
|
+
if (deadlines.rel_prep.length > 0) {
|
|
44
|
+
for (const entry of deadlines.rel_prep) {
|
|
45
|
+
logger.log(
|
|
46
|
+
` ${chalk.cyan(entry.name)}: ${chalk.bold(entry.date_finish)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
logger.log(` REL_PREP: ${chalk.dim('not found')}`);
|
|
51
|
+
}
|
|
52
|
+
const itm26 = deadlines.itm_26
|
|
53
|
+
? chalk.bold(deadlines.itm_26)
|
|
54
|
+
: chalk.dim('not found');
|
|
55
|
+
logger.log(` ITM 26: ${itm26}`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.log(chalk.yellow(` Failed to sync ${release}: ${error}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data: DeadlinesFile = {
|
|
62
|
+
updated_at: new Date().toISOString(),
|
|
63
|
+
releases: mergedReleases,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (options.dry) {
|
|
67
|
+
logger.log(chalk.dim(`\nDry run — would write to ${filePath}`));
|
|
68
|
+
logger.log(chalk.dim(JSON.stringify(data, null, 2)));
|
|
69
|
+
} else {
|
|
70
|
+
writeDeadlines(data, filePath);
|
|
71
|
+
logger.log(`\n${chalk.green('Saved')} ${chalk.underline(filePath)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import ProductPagesAPI, {
|
|
2
|
+
isError,
|
|
3
|
+
type ReleasesScheduleTasksResponse,
|
|
4
|
+
type WhoamiResponse,
|
|
5
|
+
type ScheduleTasksQueryOptions,
|
|
6
|
+
} from 'product-pages';
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
import { Logger } from './logger';
|
|
11
|
+
|
|
12
|
+
const PP_INSTANCE = 'https://pp.engineering.redhat.com/api/v7';
|
|
13
|
+
|
|
14
|
+
export class ProductPages {
|
|
15
|
+
readonly api: ProductPagesAPI;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
readonly dry: boolean,
|
|
19
|
+
readonly logger: Logger,
|
|
20
|
+
instance: string = PP_INSTANCE
|
|
21
|
+
) {
|
|
22
|
+
this.api = new ProductPagesAPI(instance, { type: 'kerberos' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async whoami(): Promise<WhoamiResponse> {
|
|
26
|
+
if (this.dry) {
|
|
27
|
+
this.logger.log(chalk.dim('Fetching whoami (dry-run)'));
|
|
28
|
+
return { username: 'dry-run@redhat.com' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const response = await this.api.whoami();
|
|
32
|
+
|
|
33
|
+
if (isError(response)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Product Pages whoami failed: ${(response as any).message}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getScheduleTasks(
|
|
43
|
+
release: string,
|
|
44
|
+
options: ScheduleTasksQueryOptions
|
|
45
|
+
): Promise<ReleasesScheduleTasksResponse> {
|
|
46
|
+
if (this.dry) {
|
|
47
|
+
this.logger.log(
|
|
48
|
+
chalk.dim(`Fetching schedule tasks for ${release} (dry-run)`)
|
|
49
|
+
);
|
|
50
|
+
// Return example tasks that match the REL_PREP and ITM 26 patterns so
|
|
51
|
+
// the dry-run accurately simulates the due-date logic.
|
|
52
|
+
const soon = new Date();
|
|
53
|
+
soon.setDate(soon.getDate() + 10);
|
|
54
|
+
const soonStr = `${soon.getFullYear()}-${String(soon.getMonth() + 1).padStart(2, '0')}-${String(soon.getDate()).padStart(2, '0')}`;
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
id: 1,
|
|
58
|
+
name: 'Package Advisory REL_PREP Deadline (dry-run)',
|
|
59
|
+
path: ['REL_PREP'],
|
|
60
|
+
date_start: soonStr,
|
|
61
|
+
date_finish: soonStr,
|
|
62
|
+
release_shortname: release,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 2,
|
|
66
|
+
name: 'ITM 26 DevTestDoc (dry-run)',
|
|
67
|
+
path: ['ITM 26'],
|
|
68
|
+
date_start: soonStr,
|
|
69
|
+
date_finish: soonStr,
|
|
70
|
+
release_shortname: release,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const response = await this.api.releasesScheduleTasks(release, options);
|
|
76
|
+
|
|
77
|
+
if (isError(response)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Product Pages schedule tasks failed: ${(response as any).message}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return response;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static getInstance(dry: boolean, logger: Logger): ProductPages {
|
|
87
|
+
return new ProductPages(dry, logger);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const relPrepEntrySchema = z.object({
|
|
7
|
+
name: z.string(),
|
|
8
|
+
date_finish: z.string(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type RelPrepEntry = z.infer<typeof relPrepEntrySchema>;
|
|
12
|
+
|
|
13
|
+
const releaseDeadlinesSchema = z.object({
|
|
14
|
+
rel_prep: z.array(relPrepEntrySchema),
|
|
15
|
+
itm_26: z.string().nullable(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const deadlinesFileSchema = z.object({
|
|
19
|
+
updated_at: z.string(),
|
|
20
|
+
releases: z.record(z.string(), releaseDeadlinesSchema),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type ReleaseDeadlines = z.infer<typeof releaseDeadlinesSchema>;
|
|
24
|
+
export type DeadlinesFile = z.infer<typeof deadlinesFileSchema>;
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_DEADLINES_PATH = path.resolve(
|
|
27
|
+
os.homedir(),
|
|
28
|
+
'.config',
|
|
29
|
+
'jira-sprinter',
|
|
30
|
+
'deadlines.json'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const SCHEDULE_TASK_REGEX =
|
|
34
|
+
'.*(Package Advisory REL_PREP Deadline|ITM 26 DevTestDoc).*';
|
|
35
|
+
|
|
36
|
+
export function readDeadlines(
|
|
37
|
+
filePath: string = DEFAULT_DEADLINES_PATH
|
|
38
|
+
): DeadlinesFile | null {
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
return deadlinesFileSchema.parse(JSON.parse(raw));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatDate(date: Date): string {
|
|
48
|
+
const y = date.getFullYear();
|
|
49
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
50
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
51
|
+
return `${y}-${m}-${d}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function classifyScheduleTasks(
|
|
55
|
+
tasks: { name: string; date_finish: string }[]
|
|
56
|
+
): ReleaseDeadlines {
|
|
57
|
+
const deadlines: ReleaseDeadlines = { rel_prep: [], itm_26: null };
|
|
58
|
+
for (const task of tasks) {
|
|
59
|
+
if (task.name.includes('REL_PREP')) {
|
|
60
|
+
deadlines.rel_prep.push({
|
|
61
|
+
name: task.name,
|
|
62
|
+
date_finish: task.date_finish,
|
|
63
|
+
});
|
|
64
|
+
} else if (task.name.includes('ITM 26')) {
|
|
65
|
+
deadlines.itm_26 = task.date_finish;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return deadlines;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function closestFutureDate(
|
|
72
|
+
dates: string[],
|
|
73
|
+
today: Date = new Date()
|
|
74
|
+
): string | null {
|
|
75
|
+
const todayStr = formatDate(today);
|
|
76
|
+
const future = dates.filter(d => d >= todayStr).sort();
|
|
77
|
+
return future.length > 0 ? future[0] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function computePreliminaryTestingDueDate(
|
|
81
|
+
deadlines: ReleaseDeadlines,
|
|
82
|
+
isZStream: boolean,
|
|
83
|
+
today: Date = new Date()
|
|
84
|
+
): string | null {
|
|
85
|
+
const twoWeeks = new Date(today);
|
|
86
|
+
twoWeeks.setDate(twoWeeks.getDate() + 14);
|
|
87
|
+
const twoWeeksStr = formatDate(twoWeeks);
|
|
88
|
+
|
|
89
|
+
if (isZStream) {
|
|
90
|
+
const closest = closestFutureDate(
|
|
91
|
+
deadlines.rel_prep.map(e => e.date_finish),
|
|
92
|
+
today
|
|
93
|
+
);
|
|
94
|
+
if (!closest) return twoWeeksStr;
|
|
95
|
+
return closest < twoWeeksStr ? closest : twoWeeksStr;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (deadlines.itm_26) {
|
|
99
|
+
const todayStr = formatDate(today);
|
|
100
|
+
if (deadlines.itm_26 >= todayStr && deadlines.itm_26 < twoWeeksStr) {
|
|
101
|
+
return deadlines.itm_26;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return twoWeeksStr;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function computeQeTaskDueDate(
|
|
109
|
+
deadlines: ReleaseDeadlines,
|
|
110
|
+
isZStream: boolean,
|
|
111
|
+
today: Date = new Date()
|
|
112
|
+
): string | null {
|
|
113
|
+
if (isZStream) {
|
|
114
|
+
return closestFutureDate(
|
|
115
|
+
deadlines.rel_prep.map(e => e.date_finish),
|
|
116
|
+
today
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (deadlines.itm_26 && deadlines.itm_26 >= formatDate(today)) {
|
|
121
|
+
return deadlines.itm_26;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function writeDeadlines(
|
|
128
|
+
data: DeadlinesFile,
|
|
129
|
+
filePath: string = DEFAULT_DEADLINES_PATH
|
|
130
|
+
): void {
|
|
131
|
+
const dir = path.dirname(filePath);
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
|
|
134
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
135
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -22,7 +22,14 @@ export function isDefaultValuesDisabled(): boolean {
|
|
|
22
22
|
|
|
23
23
|
export function getDefaultValue(
|
|
24
24
|
envName:
|
|
25
|
-
|
|
25
|
+
| 'ASSIGNEE'
|
|
26
|
+
| 'BOARD'
|
|
27
|
+
| 'TEAM'
|
|
28
|
+
| 'COMPONENTS'
|
|
29
|
+
| 'NOCOLOR'
|
|
30
|
+
| 'DRY'
|
|
31
|
+
| 'YOLO'
|
|
32
|
+
| 'DEADLINES_FILE'
|
|
26
33
|
) {
|
|
27
34
|
if (isDefaultValuesDisabled()) {
|
|
28
35
|
return undefined;
|