jira-sprinter 1.0.1
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/LICENSE +674 -0
- package/README.md +75 -0
- package/dist/main.js +49516 -0
- package/package.json +55 -0
- package/src/cli.ts +233 -0
- package/src/jira.ts +179 -0
- package/src/logger.ts +14 -0
- package/src/main.ts +24 -0
- package/src/schema/jira.ts +107 -0
- package/src/util.ts +49 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jira-sprinter",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Small CLI tool to manage sprints in JIRA Board",
|
|
5
|
+
"main": "src/main.ts",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": "./dist/main.js",
|
|
8
|
+
"directories": {
|
|
9
|
+
"test": "tests"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "esbuild ./src/main.js --bundle --outdir=dist --platform=node --target=node20.0.0 --packages=bundle",
|
|
13
|
+
"format": "prettier --write '**/*.ts'",
|
|
14
|
+
"format-check": "prettier --check '**/*.ts'",
|
|
15
|
+
"test": "vitest run --coverage",
|
|
16
|
+
"update-snapshots": "vitest run --update",
|
|
17
|
+
"all": "yarn && yarn run build && yarn run format && yarn test"
|
|
18
|
+
},
|
|
19
|
+
"packageManager": "yarn@4.9.3",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/redhat-plumbers-in-action/sprinter.git"
|
|
23
|
+
},
|
|
24
|
+
"author": "jamacku@redhat.com",
|
|
25
|
+
"license": "GPL-3.0+",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/redhat-plumbers-in-action/sprinter/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/redhat-plumbers-in-action/sprinter#readme",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"cli",
|
|
32
|
+
"jira",
|
|
33
|
+
"agile",
|
|
34
|
+
"scrum"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@inquirer/checkbox": "^4.3.0",
|
|
38
|
+
"@inquirer/select": "4.4.0",
|
|
39
|
+
"@total-typescript/ts-reset": "0.6.1",
|
|
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"
|
|
45
|
+
},
|
|
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",
|
|
51
|
+
"ts-node": "10.9.2",
|
|
52
|
+
"typescript": "5.9.3",
|
|
53
|
+
"vitest": "3.2.4"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import select, { Separator } from '@inquirer/select';
|
|
4
|
+
import checkbox from '@inquirer/checkbox';
|
|
5
|
+
|
|
6
|
+
import { Jira } from './jira';
|
|
7
|
+
import { Logger } from './logger';
|
|
8
|
+
import { getDefaultValue, getOptions, raise, tokenUnavailable } from './util';
|
|
9
|
+
|
|
10
|
+
import { SearchResults } from 'jira.js/dist/esm/types/agile/models';
|
|
11
|
+
import {
|
|
12
|
+
colorTaskSchema,
|
|
13
|
+
issueStatusSchema,
|
|
14
|
+
issueTypeSchema,
|
|
15
|
+
colorSizeSchema,
|
|
16
|
+
Size,
|
|
17
|
+
} from './schema/jira';
|
|
18
|
+
import { Issue } from 'jira.js/dist/esm/types/version2/models';
|
|
19
|
+
|
|
20
|
+
export function cli(): Command {
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name('jira-sprinter')
|
|
25
|
+
.description('🏃 Small CLI tool to manage sprints in JIRA Board')
|
|
26
|
+
.version('1.0.0');
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.option('-b, --board [board]', 'Jira Board ID', getDefaultValue('BOARD'))
|
|
30
|
+
.option(
|
|
31
|
+
'-a, --assignee [assignee]',
|
|
32
|
+
'Jira Assignee',
|
|
33
|
+
getDefaultValue('ASSIGNEE')
|
|
34
|
+
)
|
|
35
|
+
.option('-n, --nocolor', 'Disable color output', getDefaultValue('NOCOLOR'))
|
|
36
|
+
.option('-x, --dry', 'dry run', getDefaultValue('DRY'));
|
|
37
|
+
|
|
38
|
+
return program;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const runProgram = async () => {
|
|
42
|
+
const program = cli();
|
|
43
|
+
program.parse();
|
|
44
|
+
|
|
45
|
+
const options = getOptions(program.opts());
|
|
46
|
+
const logger = new Logger(!!options.nocolor);
|
|
47
|
+
|
|
48
|
+
const token = process.env.JIRA_API_TOKEN ?? tokenUnavailable();
|
|
49
|
+
const jira = new Jira(
|
|
50
|
+
'https://issues.redhat.com',
|
|
51
|
+
token,
|
|
52
|
+
options.dry,
|
|
53
|
+
logger
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const version = await jira.getVersion();
|
|
57
|
+
console.debug(`JIRA Version: ${version}`);
|
|
58
|
+
|
|
59
|
+
const sprints = await jira.getSprints(+options.board);
|
|
60
|
+
|
|
61
|
+
const sprintOrBacklog = await select({
|
|
62
|
+
message: 'Pick issues to process from sprint or backlog',
|
|
63
|
+
choices: [
|
|
64
|
+
...sprints.map(sprint => ({
|
|
65
|
+
name: `${sprint.name} (${sprint.state === 'active' ? chalk.green(sprint.state) : chalk.yellow(sprint.state)})`,
|
|
66
|
+
value: sprint.id,
|
|
67
|
+
})),
|
|
68
|
+
{
|
|
69
|
+
name: `${chalk.bold('Backlog')}`,
|
|
70
|
+
value: -1,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
default: -1,
|
|
74
|
+
pageSize: 5,
|
|
75
|
+
loop: false,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let issues: SearchResults['issues'] = [];
|
|
79
|
+
if (sprintOrBacklog === -1) {
|
|
80
|
+
issues = await jira.getBacklog(+options.board, options.assignee);
|
|
81
|
+
} else {
|
|
82
|
+
issues = await jira.getIssuesInSprint(sprintOrBacklog, options.assignee);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (issues.length === 0) {
|
|
86
|
+
logger.log(`${chalk.green('No issues found')}.`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// TODO:
|
|
91
|
+
// Show issue - allow to split it into tasks
|
|
92
|
+
// Allow to set story points and assignee
|
|
93
|
+
// add task into sprint
|
|
94
|
+
|
|
95
|
+
for (const issue of issues) {
|
|
96
|
+
logger.log(
|
|
97
|
+
`\n${issueTypeSchema.parse(issue.fields?.issuetype.name)} ${issue.key} - ${chalk.bold(issueStatusSchema.parse(issue.fields?.status.name))} - ${chalk.italic(issue.fields?.assignee?.displayName ?? '')}`
|
|
98
|
+
);
|
|
99
|
+
logger.log(
|
|
100
|
+
`${chalk.underline((issue.fields?.components ?? []).map(component => component.name).join(', ') || 'NO COMPONENT')} - ${chalk.italic(issue.fields?.summary ?? '')}`
|
|
101
|
+
);
|
|
102
|
+
logger.log(
|
|
103
|
+
`See more: ${chalk.italic.underline(jira.getIssueURL(issue.key ?? ''))}\n`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const availableTasks = [
|
|
107
|
+
{ name: 'DEV Task', value: 39396, checked: true },
|
|
108
|
+
{ name: 'QE Task', value: 39400, checked: true },
|
|
109
|
+
{ name: 'Upstream', value: 39395 },
|
|
110
|
+
{ name: 'Root Cause Analysis Task', value: 40950 },
|
|
111
|
+
{ name: 'Preliminary Testing Task', value: 39398 },
|
|
112
|
+
{ name: 'Integration Testing', value: 48270 },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const answer = await checkbox({
|
|
116
|
+
message: `Split ${chalk.bold(issue.key)} into following tasks:\n`,
|
|
117
|
+
choices: [
|
|
118
|
+
...availableTasks.map(task => ({
|
|
119
|
+
name: colorTaskSchema(task.name),
|
|
120
|
+
value: task.value,
|
|
121
|
+
checked: task.checked ?? false,
|
|
122
|
+
})),
|
|
123
|
+
new Separator(),
|
|
124
|
+
{ name: 'SKIP', value: -1 },
|
|
125
|
+
{ name: 'EXIT', value: -2 },
|
|
126
|
+
],
|
|
127
|
+
loop: false,
|
|
128
|
+
pageSize: 10,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (answer.includes(-1)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (answer.includes(-2)) {
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create tasks
|
|
140
|
+
await jira.createTasks(issue.key!, answer);
|
|
141
|
+
|
|
142
|
+
let tasks: Issue[] = [];
|
|
143
|
+
// wait for tasks to be created
|
|
144
|
+
for (let attempt = 1; attempt <= 10; attempt++) {
|
|
145
|
+
tasks = await jira.getlinkedTasks(
|
|
146
|
+
issue.key!,
|
|
147
|
+
answer.map(
|
|
148
|
+
task => availableTasks.find(t => t.value === task)?.name ?? ''
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(tasks) && tasks.length >= answer.length) break;
|
|
153
|
+
if (attempt < 10) {
|
|
154
|
+
logger.log(`Waiting for tasks to be created...`);
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// loop through tasks and and set sprint, assignee and story points
|
|
160
|
+
for (const task of tasks) {
|
|
161
|
+
// Skip QE Task
|
|
162
|
+
if (task.fields.summary.includes('QE Task')) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
logger.log(`${chalk.italic(task.fields.summary)}`);
|
|
167
|
+
const storyPointsAnswer: Size = await select({
|
|
168
|
+
message: 'Story Points',
|
|
169
|
+
choices: [
|
|
170
|
+
{
|
|
171
|
+
name: colorSizeSchema.parse(0),
|
|
172
|
+
value: 0,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: colorSizeSchema.parse(1),
|
|
176
|
+
value: 1,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: colorSizeSchema.parse(2),
|
|
180
|
+
value: 2,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: colorSizeSchema.parse(3),
|
|
184
|
+
value: 3,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: colorSizeSchema.parse(5),
|
|
188
|
+
value: 5,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: colorSizeSchema.parse(8),
|
|
192
|
+
value: 8,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: colorSizeSchema.parse(13),
|
|
196
|
+
value: 13,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
default: issue.fields?.[jira.fields.storyPoints] ?? 3,
|
|
200
|
+
pageSize: 6,
|
|
201
|
+
loop: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const addToSprintAnswer = await select({
|
|
205
|
+
message: 'Add to sprint',
|
|
206
|
+
choices: [
|
|
207
|
+
{ name: `${chalk.green('Yes')}`, value: true },
|
|
208
|
+
{ name: `${chalk.red('No')}`, value: false },
|
|
209
|
+
],
|
|
210
|
+
default: true,
|
|
211
|
+
pageSize: 2,
|
|
212
|
+
loop: false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// update task
|
|
216
|
+
await jira.setValues(task.key, {
|
|
217
|
+
assignee: issue.fields?.assignee?.emailAddress,
|
|
218
|
+
size: storyPointsAnswer,
|
|
219
|
+
sprint:
|
|
220
|
+
addToSprintAnswer && sprintOrBacklog != -1
|
|
221
|
+
? sprintOrBacklog
|
|
222
|
+
: undefined,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
logger.log(
|
|
227
|
+
`Dropping ${chalk.bold(issue.key)} from sprint and setting story points to ${chalk.bold(0)}...`
|
|
228
|
+
);
|
|
229
|
+
await jira.setValues(issue.key!, { size: 0, sprint: null });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export default runProgram;
|
package/src/jira.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AgileClient, Version2Client } from 'jira.js';
|
|
2
|
+
import { SearchResults, Sprint } from 'jira.js/dist/esm/types/agile/models';
|
|
3
|
+
|
|
4
|
+
import { raise } from './util';
|
|
5
|
+
import { Size } from './schema/jira';
|
|
6
|
+
import { Logger } from './logger';
|
|
7
|
+
|
|
8
|
+
export class Jira {
|
|
9
|
+
readonly api: Version2Client;
|
|
10
|
+
readonly agile: AgileClient;
|
|
11
|
+
readonly fields = {
|
|
12
|
+
automation: 'customfield_12316240',
|
|
13
|
+
assignee: 'assignee',
|
|
14
|
+
priority: 'priority',
|
|
15
|
+
severity: 'customfield_12316142',
|
|
16
|
+
sprint: 'customfield_12310940',
|
|
17
|
+
storyPoints: 'customfield_12310243',
|
|
18
|
+
};
|
|
19
|
+
|
|
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
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
readonly instance: string,
|
|
24
|
+
apiToken: string,
|
|
25
|
+
readonly dry: boolean,
|
|
26
|
+
readonly logger: Logger
|
|
27
|
+
) {
|
|
28
|
+
this.api = new Version2Client({
|
|
29
|
+
host: instance,
|
|
30
|
+
authentication: {
|
|
31
|
+
oauth2: {
|
|
32
|
+
accessToken: apiToken,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.agile = new AgileClient({
|
|
38
|
+
host: instance,
|
|
39
|
+
authentication: {
|
|
40
|
+
oauth2: {
|
|
41
|
+
accessToken: apiToken,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getVersion(): Promise<string> {
|
|
48
|
+
const response = await this.api.serverInfo.getServerInfo();
|
|
49
|
+
return response.version ?? raise('Jira.getVersion(): missing version.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getSprints(boardId: number): Promise<Sprint[]> {
|
|
53
|
+
const response = await this.agile.board.getAllSprints({
|
|
54
|
+
boardId: boardId,
|
|
55
|
+
state: 'active,future',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return response.values;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getIssuesInSprint(
|
|
62
|
+
sprintId: number,
|
|
63
|
+
assignee: string | undefined = undefined,
|
|
64
|
+
issuesWithoutTasks: boolean = true
|
|
65
|
+
): Promise<SearchResults['issues']> {
|
|
66
|
+
let jql = assignee ? `assignee = "${assignee}"` : '';
|
|
67
|
+
jql += issuesWithoutTasks ? ` AND ${this.issuesWithoutTasksJQL}` : '';
|
|
68
|
+
|
|
69
|
+
const response = await this.agile.sprint.getIssuesForSprint({
|
|
70
|
+
sprintId: +sprintId,
|
|
71
|
+
jql,
|
|
72
|
+
maxResults: 500,
|
|
73
|
+
fields: [
|
|
74
|
+
'id',
|
|
75
|
+
'issuetype',
|
|
76
|
+
'status',
|
|
77
|
+
'summary',
|
|
78
|
+
'assignee',
|
|
79
|
+
'priority',
|
|
80
|
+
'components',
|
|
81
|
+
// sprint
|
|
82
|
+
this.fields.storyPoints,
|
|
83
|
+
this.fields.severity,
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
return response.issues;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getBacklog(
|
|
90
|
+
boardId: number,
|
|
91
|
+
assignee?: string
|
|
92
|
+
): Promise<SearchResults['issues']> {
|
|
93
|
+
const response = await this.agile.board.getIssuesForBacklog({
|
|
94
|
+
boardId: boardId,
|
|
95
|
+
jql: assignee ? `assignee = "${assignee}"` : undefined,
|
|
96
|
+
maxResults: 500,
|
|
97
|
+
fields: [
|
|
98
|
+
'id',
|
|
99
|
+
'issuetype',
|
|
100
|
+
'status',
|
|
101
|
+
'summary',
|
|
102
|
+
'assignee',
|
|
103
|
+
'priority',
|
|
104
|
+
'components',
|
|
105
|
+
this.fields.storyPoints,
|
|
106
|
+
this.fields.severity,
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return response.issues;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
composeTaskSummaryJQL(expectedTasks: string[]) {
|
|
114
|
+
return `summary ~ "\\\\[${expectedTasks.join('\\\\]: " OR summary ~ "\\\\[')}\\\\]: "`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
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
|
+
});
|
|
130
|
+
|
|
131
|
+
return response.issues ?? [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async createTasks(issue: string, tasks: number[]) {
|
|
135
|
+
if (this.dry) {
|
|
136
|
+
this.logger.log(
|
|
137
|
+
`Would create tasks: ${tasks.join(', ')} for issue: ${issue}`
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await this.api.issues.editIssue({
|
|
143
|
+
issueIdOrKey: issue,
|
|
144
|
+
fields: {
|
|
145
|
+
// Jira expects multi-select values as objects with id/value
|
|
146
|
+
[this.fields.automation]: tasks.map(id => ({ id: String(id) })),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async setValues(
|
|
152
|
+
issue: string,
|
|
153
|
+
values: {
|
|
154
|
+
assignee?: string;
|
|
155
|
+
size?: Size;
|
|
156
|
+
sprint?: number | null;
|
|
157
|
+
}
|
|
158
|
+
) {
|
|
159
|
+
const assigneeValue = values.assignee
|
|
160
|
+
? { [this.fields.assignee]: { name: values.assignee } }
|
|
161
|
+
: {};
|
|
162
|
+
const storyPointsValue = values.size
|
|
163
|
+
? { [this.fields.storyPoints]: values.size }
|
|
164
|
+
: {};
|
|
165
|
+
const sprintValue =
|
|
166
|
+
values.sprint === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { [this.fields.sprint]: values.sprint };
|
|
169
|
+
|
|
170
|
+
await this.api.issues.editIssue({
|
|
171
|
+
issueIdOrKey: issue,
|
|
172
|
+
fields: { ...assigneeValue, ...storyPointsValue, ...sprintValue },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getIssueURL(issue: string) {
|
|
177
|
+
return `${this.instance}/browse/${issue}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
static readonly colorRegex = /\[\d+m/gm;
|
|
3
|
+
|
|
4
|
+
constructor(readonly noColor: boolean = false) {}
|
|
5
|
+
|
|
6
|
+
log(message: string): void {
|
|
7
|
+
if (!this.noColor) {
|
|
8
|
+
console.log(message);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(message.replace(Logger.colorRegex, ''));
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
|
|
6
|
+
import '@total-typescript/ts-reset';
|
|
7
|
+
|
|
8
|
+
import runProgram from './cli';
|
|
9
|
+
|
|
10
|
+
dotenv.config({
|
|
11
|
+
path: [
|
|
12
|
+
`${path.resolve(process.cwd(), '.env')}`,
|
|
13
|
+
`${path.resolve(os.homedir(), '.config', 'jira-sprinter', '.env')}`,
|
|
14
|
+
`${path.resolve(os.homedir(), '.env.jira-sprinter')}`,
|
|
15
|
+
`${path.resolve(os.homedir(), '.env')}`,
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
runProgram();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const sizeSchema = z.union([
|
|
5
|
+
z.literal(0), // gray
|
|
6
|
+
z.literal(1), // green
|
|
7
|
+
z.literal(2), // green
|
|
8
|
+
z.literal(3), // yellow
|
|
9
|
+
z.literal(5), // yellow bold
|
|
10
|
+
z.literal(8), // red
|
|
11
|
+
z.literal(13), // red bold
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const colorSizeSchema = sizeSchema.transform(val => {
|
|
15
|
+
switch (val) {
|
|
16
|
+
case 0:
|
|
17
|
+
return chalk.gray(val);
|
|
18
|
+
case 1:
|
|
19
|
+
return chalk.green(val);
|
|
20
|
+
case 2:
|
|
21
|
+
return chalk.green(val);
|
|
22
|
+
case 3:
|
|
23
|
+
return chalk.yellow(val);
|
|
24
|
+
case 5:
|
|
25
|
+
return chalk.yellow.bold(val);
|
|
26
|
+
case 8:
|
|
27
|
+
return chalk.red(val);
|
|
28
|
+
case 13:
|
|
29
|
+
return chalk.red.bold(val);
|
|
30
|
+
default:
|
|
31
|
+
return val;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type Size = z.infer<typeof sizeSchema>;
|
|
36
|
+
|
|
37
|
+
export const colorTaskSchema = (task: string) => {
|
|
38
|
+
switch (task) {
|
|
39
|
+
case 'DEV Task':
|
|
40
|
+
return chalk.green(task);
|
|
41
|
+
case 'Upstream':
|
|
42
|
+
return chalk.green(task);
|
|
43
|
+
case 'Root Cause Analysis Task':
|
|
44
|
+
return chalk.green(task);
|
|
45
|
+
case 'QE Task':
|
|
46
|
+
return chalk.yellow(task);
|
|
47
|
+
case 'Preliminary Testing Task':
|
|
48
|
+
return chalk.blue(task);
|
|
49
|
+
case 'Integration Testing':
|
|
50
|
+
return chalk.blue(task);
|
|
51
|
+
default:
|
|
52
|
+
return task;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const issueIdSchema = z.string().regex(/^RHEL-\d+$/);
|
|
57
|
+
|
|
58
|
+
export type IssueID = z.infer<typeof issueIdSchema>;
|
|
59
|
+
|
|
60
|
+
export const issueTypeSchema = z
|
|
61
|
+
.union([
|
|
62
|
+
z.literal('Task'),
|
|
63
|
+
z.literal('Bug'),
|
|
64
|
+
z.literal('Story'),
|
|
65
|
+
z.literal('Epic'),
|
|
66
|
+
z.string(),
|
|
67
|
+
])
|
|
68
|
+
.transform(val => {
|
|
69
|
+
switch (val) {
|
|
70
|
+
case 'Task':
|
|
71
|
+
return '☑️';
|
|
72
|
+
case 'Bug':
|
|
73
|
+
return '🐛';
|
|
74
|
+
case 'Story':
|
|
75
|
+
return '🎁';
|
|
76
|
+
case 'Epic':
|
|
77
|
+
return '⚡';
|
|
78
|
+
default:
|
|
79
|
+
return val;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const issueStatusSchema = z
|
|
84
|
+
.union([
|
|
85
|
+
z.literal('New'),
|
|
86
|
+
z.literal('Planning'),
|
|
87
|
+
z.literal('In Progress'),
|
|
88
|
+
z.literal('Integration'),
|
|
89
|
+
z.literal('Release Pending'),
|
|
90
|
+
z.string(),
|
|
91
|
+
])
|
|
92
|
+
.transform(val => {
|
|
93
|
+
switch (val) {
|
|
94
|
+
case 'New':
|
|
95
|
+
return chalk.cyan(val);
|
|
96
|
+
case 'Planning':
|
|
97
|
+
return chalk.cyan(val);
|
|
98
|
+
case 'In Progress':
|
|
99
|
+
return chalk.blue(val);
|
|
100
|
+
case 'Integration':
|
|
101
|
+
return chalk.green(val);
|
|
102
|
+
case 'Release Pending':
|
|
103
|
+
return chalk.green(val);
|
|
104
|
+
default:
|
|
105
|
+
return val;
|
|
106
|
+
}
|
|
107
|
+
});
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { OptionValues } from 'commander';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
export function raise(error: string): never {
|
|
5
|
+
throw new Error(error);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function tokenUnavailable(): never {
|
|
9
|
+
return raise(
|
|
10
|
+
`JIRA_API_TOKEN not set.\nPlease set the JIRA_API_TOKEN environment variable in '~/.config/sprinter/.env' or '~/.env.sprinter' or '~/.env.'`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getUserFromLogin(): string {
|
|
15
|
+
const login = os.userInfo().username;
|
|
16
|
+
return `${login}@redhat.com`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDefaultValuesDisabled(): boolean {
|
|
20
|
+
return process.env['NODEFAULTS'] ? true : false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDefaultValue(
|
|
24
|
+
envName: 'ASSIGNEE' | 'BOARD' | 'NOCOLOR' | 'DRY'
|
|
25
|
+
) {
|
|
26
|
+
if (isDefaultValuesDisabled()) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const value = process.env[envName];
|
|
31
|
+
|
|
32
|
+
if (envName === 'ASSIGNEE' && !value) {
|
|
33
|
+
return getUserFromLogin();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (envName === 'NOCOLOR' && !value) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getOptions(inputs: OptionValues): OptionValues {
|
|
44
|
+
return {
|
|
45
|
+
...inputs,
|
|
46
|
+
assignee: inputs.assignee || getDefaultValue('ASSIGNEE'),
|
|
47
|
+
board: inputs.board || getDefaultValue('BOARD'),
|
|
48
|
+
};
|
|
49
|
+
}
|