jira-sprinter 1.0.3 → 2.0.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": "1.0.3",
3
+ "version": "2.0.0",
4
4
  "description": "Small CLI tool to manage sprints in JIRA Board",
5
5
  "main": "src/main.ts",
6
6
  "type": "commonjs",
@@ -16,7 +16,7 @@
16
16
  "update-snapshots": "vitest run --update",
17
17
  "all": "yarn && yarn run build && yarn run format && yarn test"
18
18
  },
19
- "packageManager": "yarn@4.9.3",
19
+ "packageManager": "yarn@4.12.0",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/redhat-plumbers-in-action/sprinter.git"
@@ -34,22 +34,22 @@
34
34
  "scrum"
35
35
  ],
36
36
  "dependencies": {
37
- "@inquirer/checkbox": "^4.3.0",
38
- "@inquirer/select": "4.4.0",
37
+ "@inquirer/checkbox": "^5.1.2",
38
+ "@inquirer/select": "5.1.2",
39
39
  "@total-typescript/ts-reset": "0.6.1",
40
40
  "chalk": "5.6.2",
41
- "commander": "14.0.1",
42
- "dotenv": "17.2.3",
43
- "jira.js": "5.2.2",
44
- "zod": "4.1.12"
41
+ "commander": "14.0.3",
42
+ "dotenv": "17.3.1",
43
+ "jira.js": "5.3.1",
44
+ "zod": "4.3.6"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/node": "24.7.2",
48
- "@vitest/coverage-v8": "3.2.4",
49
- "esbuild": "0.25.11",
50
- "prettier": "3.6.2",
47
+ "@types/node": "25.5.0",
48
+ "@vitest/coverage-v8": "4.1.0",
49
+ "esbuild": "0.27.4",
50
+ "prettier": "3.8.1",
51
51
  "ts-node": "10.9.2",
52
52
  "typescript": "5.9.3",
53
- "vitest": "3.2.4"
53
+ "vitest": "4.1.0"
54
54
  }
55
55
  }
package/src/cli.ts CHANGED
@@ -5,7 +5,12 @@ import checkbox from '@inquirer/checkbox';
5
5
 
6
6
  import { Jira } from './jira';
7
7
  import { Logger } from './logger';
8
- import { getDefaultValue, getOptions, raise, tokenUnavailable } from './util';
8
+ import {
9
+ getDefaultValue,
10
+ getOptions,
11
+ getUserFromLogin,
12
+ tokenUnavailable,
13
+ } from './util';
9
14
 
10
15
  import { SearchResults } from 'jira.js/dist/esm/types/agile/models';
11
16
  import {
@@ -15,7 +20,7 @@ import {
15
20
  colorSizeSchema,
16
21
  Size,
17
22
  } from './schema/jira';
18
- import { Issue } from 'jira.js/dist/esm/types/version2/models';
23
+ import { Issue } from 'jira.js/dist/esm/types/version3/models/issue';
19
24
 
20
25
  export function cli(): Command {
21
26
  const program = new Command();
@@ -23,7 +28,7 @@ export function cli(): Command {
23
28
  program
24
29
  .name('jira-sprinter')
25
30
  .description('🏃 Small CLI tool to manage sprints in JIRA Board')
26
- .version('1.0.3');
31
+ .version('2.0.0');
27
32
 
28
33
  program
29
34
  .option('-b, --board [board]', 'Jira Board ID', getDefaultValue('BOARD'))
@@ -32,6 +37,11 @@ export function cli(): Command {
32
37
  'Jira Assignee',
33
38
  getDefaultValue('ASSIGNEE')
34
39
  )
40
+ .option(
41
+ '-y, --yolo',
42
+ 'YOLO mode, dangerously skip all questions, apply default values (use with caution!)',
43
+ getDefaultValue('YOLO')
44
+ )
35
45
  .option('-n, --nocolor', 'Disable color output', getDefaultValue('NOCOLOR'))
36
46
  .option('-x, --dry', 'dry run', getDefaultValue('DRY'));
37
47
 
@@ -47,10 +57,11 @@ const runProgram = async () => {
47
57
 
48
58
  const token = process.env.JIRA_API_TOKEN ?? tokenUnavailable();
49
59
  const jira = new Jira(
50
- 'https://issues.redhat.com',
60
+ 'https://redhat.atlassian.net',
51
61
  token,
52
62
  options.dry,
53
- logger
63
+ logger,
64
+ getUserFromLogin()
54
65
  );
55
66
 
56
67
  const version = await jira.getVersion();
@@ -88,11 +99,6 @@ const runProgram = async () => {
88
99
  process.exit(0);
89
100
  }
90
101
 
91
- // TODO:
92
- // Show issue - allow to split it into tasks
93
- // Allow to set story points and assignee
94
- // add task into sprint
95
-
96
102
  for (const issue of issues) {
97
103
  logger.log(
98
104
  `\n${issueTypeSchema.parse(issue.fields?.issuetype.name)} ${issue.key} - ${chalk.bold(issueStatusSchema.parse(issue.fields?.status.name))} - ${chalk.italic(issue.fields?.assignee?.displayName ?? '')}`
@@ -105,36 +111,87 @@ const runProgram = async () => {
105
111
  );
106
112
 
107
113
  const availableTasks = [
108
- { name: 'DEV Task', value: 39396, checked: true },
109
- { name: 'QE Task', value: 39400, checked: true },
110
- { name: 'Upstream', value: 39395 },
111
- { name: 'Root Cause Analysis Task', value: 40950 },
112
- { name: 'Preliminary Testing Task', value: 39398 },
113
- { name: 'Integration Testing', value: 48270 },
114
+ {
115
+ name: 'DEV Task',
116
+ summary: '[DEV Task]:',
117
+ label: 'dev_task',
118
+ value: 14476,
119
+ checked: true,
120
+ },
121
+ {
122
+ name: 'QE Task',
123
+ summary: '[QE Task]:',
124
+ label: 'qe_task',
125
+ value: 14480,
126
+ checked: true,
127
+ },
128
+ {
129
+ name: 'Upstream',
130
+ summary: '[Upstream]:',
131
+ label: 'upstream_task',
132
+ value: 14475,
133
+ },
134
+ {
135
+ name: 'Root Cause Analysis Task',
136
+ summary: '[Root Cause Analysis Task]:',
137
+ label: 'root_cause_analysis_task',
138
+ value: 14483,
139
+ },
140
+ {
141
+ name: 'Preliminary Testing Task',
142
+ summary: '[Preliminary Testing Task]:',
143
+ label: 'preliminary_testing_task',
144
+ value: 14478,
145
+ },
146
+ {
147
+ name: 'Integration Testing',
148
+ summary: '[Integration Testing Task]:',
149
+ label: 'integration_testing_task',
150
+ value: 14481,
151
+ },
114
152
  ];
115
153
 
116
- const answer = await checkbox({
117
- message: `Split ${chalk.bold(issue.key)} into following tasks:\n`,
118
- choices: [
119
- ...availableTasks.map(task => ({
120
- name: colorTaskSchema(task.name),
121
- value: task.value,
122
- checked: task.checked ?? false,
123
- })),
124
- new Separator(),
125
- { name: 'SKIP', value: -1 },
126
- { name: 'EXIT', value: -2 },
127
- ],
128
- loop: false,
129
- pageSize: 10,
130
- });
131
-
132
- if (answer.includes(-1)) {
133
- continue;
154
+ let answer: number[] = [];
155
+ if (!options.yolo) {
156
+ answer = await checkbox({
157
+ message: `Split ${chalk.bold(issue.key)} into following tasks:\n`,
158
+ choices: [
159
+ ...availableTasks.map(task => {
160
+ const isTaskOpen = issue.fields?.issuelinks?.some(
161
+ l =>
162
+ l.type?.outward === 'split to' &&
163
+ l.outwardIssue?.fields?.summary.startsWith(task.summary) &&
164
+ l.outwardIssue?.fields?.status.name !== 'Closed'
165
+ );
166
+
167
+ return {
168
+ name: colorTaskSchema(task.name),
169
+ value: task.value,
170
+ checked: task.checked && !isTaskOpen,
171
+ disabled: isTaskOpen,
172
+ };
173
+ }),
174
+ new Separator(),
175
+ { name: 'SKIP', value: -1 },
176
+ { name: 'EXIT', value: -2 },
177
+ ],
178
+ loop: false,
179
+ pageSize: 10,
180
+ });
181
+
182
+ if (answer.includes(-1)) {
183
+ continue;
184
+ }
185
+
186
+ if (answer.includes(-2)) {
187
+ process.exit(0);
188
+ }
134
189
  }
135
190
 
136
- if (answer.includes(-2)) {
137
- process.exit(0);
191
+ if (options.yolo) {
192
+ answer = availableTasks
193
+ .filter(task => task.checked ?? false)
194
+ .map(task => task.value);
138
195
  }
139
196
 
140
197
  // Create tasks
@@ -157,7 +214,6 @@ const runProgram = async () => {
157
214
  }
158
215
  }
159
216
 
160
- // loop through tasks and and set sprint, assignee and story points
161
217
  for (const task of tasks) {
162
218
  // Skip QE Task
163
219
  if (task.fields.summary.includes('QE Task')) {
@@ -165,57 +221,69 @@ const runProgram = async () => {
165
221
  }
166
222
 
167
223
  logger.log(`${chalk.italic(task.fields.summary)}`);
168
- const storyPointsAnswer: Size = await select({
169
- message: 'Story Points',
170
- choices: [
171
- {
172
- name: colorSizeSchema.parse(0),
173
- value: 0,
174
- },
175
- {
176
- name: colorSizeSchema.parse(1),
177
- value: 1,
178
- },
179
- {
180
- name: colorSizeSchema.parse(2),
181
- value: 2,
182
- },
183
- {
184
- name: colorSizeSchema.parse(3),
185
- value: 3,
186
- },
187
- {
188
- name: colorSizeSchema.parse(5),
189
- value: 5,
190
- },
191
- {
192
- name: colorSizeSchema.parse(8),
193
- value: 8,
194
- },
195
- {
196
- name: colorSizeSchema.parse(13),
197
- value: 13,
198
- },
199
- ],
200
- default: issue.fields?.[jira.fields.storyPoints] ?? 3,
201
- pageSize: 6,
202
- loop: false,
203
- });
204
224
 
205
- const addToSprintAnswer = await select({
206
- message: 'Add to sprint',
207
- choices: [
208
- { name: `${chalk.green('Yes')}`, value: true },
209
- { name: `${chalk.red('No')}`, value: false },
210
- ],
211
- default: true,
212
- pageSize: 2,
213
- loop: false,
214
- });
225
+ let storyPointsAnswer: Size | undefined;
226
+ let addToSprintAnswer: boolean | undefined;
227
+ if (!options.yolo) {
228
+ storyPointsAnswer = await select({
229
+ message: 'Story Points',
230
+ choices: [
231
+ {
232
+ name: colorSizeSchema.parse(0),
233
+ value: 0,
234
+ },
235
+ {
236
+ name: colorSizeSchema.parse(1),
237
+ value: 1,
238
+ },
239
+ {
240
+ name: colorSizeSchema.parse(2),
241
+ value: 2,
242
+ },
243
+ {
244
+ name: colorSizeSchema.parse(3),
245
+ value: 3,
246
+ },
247
+ {
248
+ name: colorSizeSchema.parse(5),
249
+ value: 5,
250
+ },
251
+ {
252
+ name: colorSizeSchema.parse(8),
253
+ value: 8,
254
+ },
255
+ {
256
+ name: colorSizeSchema.parse(13),
257
+ value: 13,
258
+ },
259
+ ],
260
+ default: issue.fields?.[jira.fields.storyPoints] ?? 3,
261
+ pageSize: 6,
262
+ loop: false,
263
+ });
264
+
265
+ addToSprintAnswer = await select({
266
+ message: 'Add to sprint',
267
+ choices: [
268
+ { name: `${chalk.green('Yes')}`, value: true },
269
+ { name: `${chalk.red('No')}`, value: false },
270
+ ],
271
+ default: true,
272
+ pageSize: 2,
273
+ loop: false,
274
+ });
275
+ }
276
+
277
+ if (options.yolo) {
278
+ storyPointsAnswer = issue.fields?.[jira.fields.storyPoints];
279
+ addToSprintAnswer = sprintOrBacklog != -1;
280
+ logger.log(
281
+ `Setting story points to ${colorSizeSchema.parse(storyPointsAnswer)} and adding it to the sprint.`
282
+ );
283
+ }
215
284
 
216
- // update task
217
285
  await jira.setValues(task.key, {
218
- assignee: issue.fields?.assignee?.emailAddress,
286
+ assignee: issue.fields?.assignee?.accountId,
219
287
  size: storyPointsAnswer,
220
288
  sprint:
221
289
  addToSprintAnswer && sprintOrBacklog != -1
package/src/jira.ts CHANGED
@@ -1,35 +1,38 @@
1
- import { AgileClient, Version2Client } from 'jira.js';
1
+ import { AgileClient, Version3Client } from 'jira.js';
2
2
  import { SearchResults, Sprint } from 'jira.js/dist/esm/types/agile/models';
3
+ import { Issue } from 'jira.js/dist/esm/types/version3/models/issue';
3
4
 
4
5
  import { raise } from './util';
5
6
  import { Size } from './schema/jira';
6
7
  import { Logger } from './logger';
7
8
 
8
9
  export class Jira {
9
- readonly api: Version2Client;
10
+ readonly api: Version3Client;
10
11
  readonly agile: AgileClient;
11
12
  readonly fields = {
12
- automation: 'customfield_12316240',
13
+ automation: 'customfield_10553',
13
14
  assignee: 'assignee',
14
15
  priority: 'priority',
15
- severity: 'customfield_12316142',
16
- sprint: 'customfield_12310940',
17
- storyPoints: 'customfield_12310243',
16
+ severity: 'customfield_10840',
17
+ sprint: 'customfield_10020',
18
+ storyPoints: 'customfield_10028',
18
19
  };
19
20
 
20
- readonly issuesWithoutTasksJQL = `issueFunction not in linkedIssuesOf("type = Task AND (summary ~ 'DEV Task' OR summary ~ 'QE Task')") AND type not in (Task, Epic) AND project = "RHEL"`;
21
+ readonly issuesWithoutTasksJQL = `(labels not in (upstream_task, dev_task, qe_task, root_cause_analysis_task, preliminary_testing_task, integration_testing_task) OR labels is EMPTY) AND type in (Bug, "Story", Vulnerability) AND Project = RHEL AND statusCategory != Done`;
21
22
 
22
23
  constructor(
23
24
  readonly instance: string,
24
25
  apiToken: string,
25
26
  readonly dry: boolean,
26
- readonly logger: Logger
27
+ readonly logger: Logger,
28
+ email: string
27
29
  ) {
28
- this.api = new Version2Client({
30
+ this.api = new Version3Client({
29
31
  host: instance,
30
32
  authentication: {
31
- oauth2: {
32
- accessToken: apiToken,
33
+ basic: {
34
+ email,
35
+ apiToken,
33
36
  },
34
37
  },
35
38
  });
@@ -37,8 +40,9 @@ export class Jira {
37
40
  this.agile = new AgileClient({
38
41
  host: instance,
39
42
  authentication: {
40
- oauth2: {
41
- accessToken: apiToken,
43
+ basic: {
44
+ email,
45
+ apiToken,
42
46
  },
43
47
  },
44
48
  });
@@ -78,11 +82,12 @@ export class Jira {
78
82
  'assignee',
79
83
  'priority',
80
84
  'components',
81
- // sprint
82
85
  this.fields.storyPoints,
83
86
  this.fields.severity,
87
+ 'issuelinks',
84
88
  ],
85
89
  });
90
+
86
91
  return response.issues;
87
92
  }
88
93
 
@@ -115,18 +120,43 @@ export class Jira {
115
120
  }
116
121
 
117
122
  async getlinkedTasks(issue: string, expectedTasks: string[]) {
118
- const response = await this.api.issueSearch.searchForIssuesUsingJqlPost({
119
- jql: `issue in linkedIssues("${issue}") AND type = Task AND status = New AND (${this.composeTaskSummaryJQL(expectedTasks)})`,
120
- fields: [
121
- 'id',
122
- 'issuetype',
123
- 'status',
124
- 'components',
125
- 'summary',
126
- 'assignee',
127
- this.fields.storyPoints,
128
- ],
129
- });
123
+ if (this.dry) {
124
+ this.logger.log(`Would get linked tasks for issue: ${issue}`);
125
+ return [
126
+ {
127
+ key: 'RHEL-1234',
128
+ fields: {
129
+ summary: '[DEV Task] Test Task',
130
+ },
131
+ },
132
+ {
133
+ key: 'RHEL-1235',
134
+ fields: {
135
+ summary: '[QE Task] Test Task',
136
+ },
137
+ },
138
+ {
139
+ key: 'RHEL-1236',
140
+ fields: {
141
+ summary: '[Upstream] Test Task',
142
+ },
143
+ },
144
+ ] as unknown as Issue[];
145
+ }
146
+
147
+ const response =
148
+ await this.api.issueSearch.searchForIssuesUsingJqlEnhancedSearchPost({
149
+ jql: `issue in linkedIssues("${issue}") AND type = Task AND status = New AND (${this.composeTaskSummaryJQL(expectedTasks)})`,
150
+ fields: [
151
+ 'id',
152
+ 'issuetype',
153
+ 'status',
154
+ 'components',
155
+ 'summary',
156
+ 'assignee',
157
+ this.fields.storyPoints,
158
+ ],
159
+ });
130
160
 
131
161
  return response.issues ?? [];
132
162
  }
@@ -139,6 +169,7 @@ export class Jira {
139
169
  return;
140
170
  }
141
171
 
172
+ this.logger.log(`Creating tasks: ${tasks.join(', ')} for issue: ${issue}`);
142
173
  await this.api.issues.editIssue({
143
174
  issueIdOrKey: issue,
144
175
  fields: {
@@ -157,7 +188,7 @@ export class Jira {
157
188
  }
158
189
  ) {
159
190
  const assigneeValue = values.assignee
160
- ? { [this.fields.assignee]: { name: values.assignee } }
191
+ ? { [this.fields.assignee]: { accountId: values.assignee } }
161
192
  : {};
162
193
  const storyPointsValue =
163
194
  values.size !== undefined
package/src/util.ts CHANGED
@@ -21,7 +21,7 @@ export function isDefaultValuesDisabled(): boolean {
21
21
  }
22
22
 
23
23
  export function getDefaultValue(
24
- envName: 'ASSIGNEE' | 'BOARD' | 'NOCOLOR' | 'DRY'
24
+ envName: 'ASSIGNEE' | 'BOARD' | 'NOCOLOR' | 'DRY' | 'YOLO'
25
25
  ) {
26
26
  if (isDefaultValuesDisabled()) {
27
27
  return undefined;
@@ -37,6 +37,10 @@ export function getDefaultValue(
37
37
  return false;
38
38
  }
39
39
 
40
+ if (envName === 'YOLO' && !value) {
41
+ return false;
42
+ }
43
+
40
44
  return value;
41
45
  }
42
46
 
@@ -0,0 +1,13 @@
1
+ import { configDefaults, defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ dir: './test',
6
+
7
+ exclude: [...configDefaults.exclude],
8
+
9
+ coverage: {
10
+ include: ['src/**/*.ts'],
11
+ },
12
+ },
13
+ });