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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-sprinter",
3
- "version": "2.3.0",
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.3.0');
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
- key: 'RHEL-1234',
205
- fields: {
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
- 'ASSIGNEE' | 'BOARD' | 'TEAM' | 'COMPONENTS' | 'NOCOLOR' | 'DRY' | 'YOLO'
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;