jira-sprinter 1.1.0 → 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.1.0",
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.1.0');
31
+ .version('2.0.0');
27
32
 
28
33
  program
29
34
  .option('-b, --board [board]', 'Jira Board ID', getDefaultValue('BOARD'))
@@ -52,10 +57,11 @@ const runProgram = async () => {
52
57
 
53
58
  const token = process.env.JIRA_API_TOKEN ?? tokenUnavailable();
54
59
  const jira = new Jira(
55
- 'https://issues.redhat.com',
60
+ 'https://redhat.atlassian.net',
56
61
  token,
57
62
  options.dry,
58
- logger
63
+ logger,
64
+ getUserFromLogin()
59
65
  );
60
66
 
61
67
  const version = await jira.getVersion();
@@ -93,11 +99,6 @@ const runProgram = async () => {
93
99
  process.exit(0);
94
100
  }
95
101
 
96
- // TODO:
97
- // Show issue - allow to split it into tasks
98
- // Allow to set story points and assignee
99
- // add task into sprint
100
-
101
102
  for (const issue of issues) {
102
103
  logger.log(
103
104
  `\n${issueTypeSchema.parse(issue.fields?.issuetype.name)} ${issue.key} - ${chalk.bold(issueStatusSchema.parse(issue.fields?.status.name))} - ${chalk.italic(issue.fields?.assignee?.displayName ?? '')}`
@@ -110,12 +111,44 @@ const runProgram = async () => {
110
111
  );
111
112
 
112
113
  const availableTasks = [
113
- { name: 'DEV Task', value: 39396, checked: true },
114
- { name: 'QE Task', value: 39400, checked: true },
115
- { name: 'Upstream', value: 39395 },
116
- { name: 'Root Cause Analysis Task', value: 40950 },
117
- { name: 'Preliminary Testing Task', value: 39398 },
118
- { 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
+ },
119
152
  ];
120
153
 
121
154
  let answer: number[] = [];
@@ -123,11 +156,21 @@ const runProgram = async () => {
123
156
  answer = await checkbox({
124
157
  message: `Split ${chalk.bold(issue.key)} into following tasks:\n`,
125
158
  choices: [
126
- ...availableTasks.map(task => ({
127
- name: colorTaskSchema(task.name),
128
- value: task.value,
129
- checked: task.checked ?? false,
130
- })),
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
+ }),
131
174
  new Separator(),
132
175
  { name: 'SKIP', value: -1 },
133
176
  { name: 'EXIT', value: -2 },
@@ -171,7 +214,6 @@ const runProgram = async () => {
171
214
  }
172
215
  }
173
216
 
174
- // loop through tasks and and set sprint, assignee and story points
175
217
  for (const task of tasks) {
176
218
  // Skip QE Task
177
219
  if (task.fields.summary.includes('QE Task')) {
@@ -240,9 +282,8 @@ const runProgram = async () => {
240
282
  );
241
283
  }
242
284
 
243
- // update task
244
285
  await jira.setValues(task.key, {
245
- assignee: issue.fields?.assignee?.emailAddress,
286
+ assignee: issue.fields?.assignee?.accountId,
246
287
  size: storyPointsAnswer,
247
288
  sprint:
248
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
  }
@@ -158,7 +188,7 @@ export class Jira {
158
188
  }
159
189
  ) {
160
190
  const assigneeValue = values.assignee
161
- ? { [this.fields.assignee]: { name: values.assignee } }
191
+ ? { [this.fields.assignee]: { accountId: values.assignee } }
162
192
  : {};
163
193
  const storyPointsValue =
164
194
  values.size !== undefined
@@ -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
+ });