fixdiscover 1.2.1 → 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": "fixdiscover",
3
- "version": "1.2.1",
3
+ "version": "2.0.0",
4
4
  "description": "Small CLI tool to search for Jira issues with linked PRs and Issues that are fixed in an upstream projects.",
5
5
  "main": "src/main.ts",
6
6
  "type": "commonjs",
@@ -9,14 +9,14 @@
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=node24.0.0 --packages=bundle",
13
13
  "format": "prettier --write '**/*.ts'",
14
14
  "format-check": "prettier --check '**/*.ts'",
15
15
  "test": "vitest run --coverage",
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.14.1",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/redhat-plumbers-in-action/storypointer.git"
@@ -34,25 +34,25 @@
34
34
  "patch"
35
35
  ],
36
36
  "dependencies": {
37
- "@actions/core": "^1.11.1",
38
- "@octokit/core": "^7.0.5",
39
- "@octokit/plugin-throttling": "^11.0.2",
40
- "@octokit/types": "^15.0.0",
37
+ "@actions/core": "^3.0.1",
38
+ "@octokit/core": "^7.0.6",
39
+ "@octokit/plugin-throttling": "^11.0.3",
40
+ "@octokit/types": "^16.0.0",
41
41
  "@total-typescript/ts-reset": "0.6.1",
42
- "bugzilla": "^3.1.4",
42
+ "bugzilla": "^3.1.6",
43
43
  "chalk": "5.6.2",
44
- "commander": "14.0.1",
45
- "dotenv": "17.2.3",
46
- "jira.js": "5.2.2",
47
- "zod": "4.1.11"
44
+ "commander": "14.0.3",
45
+ "dotenv": "17.4.2",
46
+ "jira.js": "5.3.1",
47
+ "zod": "4.4.3"
48
48
  },
49
49
  "devDependencies": {
50
- "@types/node": "24.6.1",
51
- "@vitest/coverage-v8": "3.2.4",
52
- "esbuild": "0.25.10",
53
- "prettier": "3.6.2",
50
+ "@types/node": "25.9.1",
51
+ "@vitest/coverage-v8": "4.1.7",
52
+ "esbuild": "0.28.0",
53
+ "prettier": "3.8.3",
54
54
  "ts-node": "10.9.2",
55
- "typescript": "5.9.3",
56
- "vitest": "3.2.4"
55
+ "typescript": "6.0.3",
56
+ "vitest": "4.1.7"
57
57
  }
58
58
  }
package/src/cli.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { Command } from 'commander';
2
- import { Comment } from 'jira.js/out/version2/models';
3
2
 
4
3
  import { Bugzilla } from './bugzilla';
5
4
  import { Jira } from './jira';
6
5
  import { Logger } from './logger';
7
6
  import { getOctokit } from './octokit';
8
- import { getDefaultValue, getOptions, tokenUnavailable } from './util';
7
+ import { getDefaultValue, getOptions, raise, tokenUnavailable } from './util';
9
8
  import { LinkFinder } from './linkfinder';
10
9
 
11
10
  import { IssueLinks, LinkObject } from './schema/link';
@@ -18,7 +17,7 @@ export function cli(): Command {
18
17
  .description(
19
18
  '🔍 A small CLI tool is used to search for Jira issues with linked PRs and issues that are fixed in upstream projects'
20
19
  )
21
- .version('1.2.1');
20
+ .version('2.0.0');
22
21
 
23
22
  program
24
23
  .requiredOption(
@@ -31,6 +30,7 @@ export function cli(): Command {
31
30
  'upstream project',
32
31
  getDefaultValue('UPSTREAM')
33
32
  )
33
+ .option('-l, --login <email>', 'Jira login email', getDefaultValue('LOGIN'))
34
34
  .option('--migrate', 'migrate links from Bugzilla to Jira')
35
35
  .option('-n, --nocolor', 'disable color output', getDefaultValue('NOCOLOR'))
36
36
  .option('-x, --dry', 'dry run', getDefaultValue('DRY'));
@@ -46,7 +46,18 @@ const runProgram = async () => {
46
46
  const logger = new Logger(!!options.nocolor);
47
47
 
48
48
  const jiraToken = process.env.JIRA_API_TOKEN ?? tokenUnavailable('jira');
49
- const jira = new Jira('https://issues.redhat.com', jiraToken, options.dry);
49
+ const jiraLogin =
50
+ typeof options.login === 'string' && options.login
51
+ ? options.login
52
+ : raise(
53
+ 'Jira login email is required. Use --login <email> or set the LOGIN environment variable.'
54
+ );
55
+ const jira = new Jira(
56
+ 'https://redhat.atlassian.net',
57
+ jiraToken,
58
+ options.dry,
59
+ jiraLogin
60
+ );
50
61
 
51
62
  const bugzillaToken = options.migrate
52
63
  ? (process.env.BUGZILLA_API_TOKEN ?? tokenUnavailable('bugzilla'))
@@ -68,19 +79,19 @@ const runProgram = async () => {
68
79
  for (const issue of issues) {
69
80
  let links: LinkObject[] = [];
70
81
 
71
- const bugzillaBug: { bugid: number } | null =
72
- issue.fields[jira.fields.bugzillaBug];
73
-
74
82
  const externalLinks = await jira.getLinks(issue.id);
83
+ const bugzillaBugId = jira.getBugzillaBugId(externalLinks);
84
+
85
+ const renderedComments =
86
+ (issue.renderedFields as Record<string, any>)?.comment?.comments ?? [];
75
87
 
76
- for (const comment of issue.fields.comment.comments as (Comment & {
77
- body?: string;
78
- })[]) {
79
- if (!comment?.body) {
88
+ for (const comment of renderedComments) {
89
+ const body: unknown = comment?.body;
90
+ if (typeof body !== 'string' || !body) {
80
91
  continue;
81
92
  }
82
93
 
83
- const commentLinks = linkFinder.getLinks(comment.body);
94
+ const commentLinks = linkFinder.getLinks(body);
84
95
 
85
96
  if (commentLinks) {
86
97
  links.push(...(await linkFinder.checkUpstream(commentLinks, octokit)));
@@ -95,8 +106,8 @@ const runProgram = async () => {
95
106
  }
96
107
  }
97
108
 
98
- if (options.migrate && bugzillaBug !== null) {
99
- const bug = (await bugzilla.getBugs(bugzillaBug.bugid))[0];
109
+ if (options.migrate && bugzillaBugId !== null) {
110
+ const bug = (await bugzilla.getBugs(bugzillaBugId))[0];
100
111
 
101
112
  let linksForMigration: LinkObject[] = [];
102
113
 
@@ -138,7 +149,10 @@ const runProgram = async () => {
138
149
  await jira.setLabels(issue.key, ['backport']);
139
150
  data.push({
140
151
  key: jira.getIssueURL(issue.key),
141
- bz: bugzillaBug ? bugzilla.getIssueURL(bugzillaBug.bugid) : undefined,
152
+ bz:
153
+ bugzillaBugId !== null
154
+ ? bugzilla.getIssueURL(bugzillaBugId)
155
+ : undefined,
142
156
  links,
143
157
  });
144
158
  }
package/src/jira.ts CHANGED
@@ -1,12 +1,9 @@
1
- import { Version2Client } from 'jira.js';
1
+ import { Version3Client } from 'jira.js';
2
2
 
3
3
  import { raise } from './util';
4
4
 
5
5
  export class Jira {
6
- readonly api: Version2Client;
7
- readonly fields = {
8
- bugzillaBug: 'customfield_12316840',
9
- };
6
+ readonly api: Version3Client;
10
7
 
11
8
  readonly baseJQL = 'Project = RHEL AND statusCategory = "To Do"';
12
9
  JQL = '';
@@ -14,12 +11,16 @@ export class Jira {
14
11
  constructor(
15
12
  readonly instance: string,
16
13
  apiToken: string,
17
- readonly dry: boolean
14
+ readonly dry: boolean,
15
+ email: string
18
16
  ) {
19
- this.api = new Version2Client({
17
+ this.api = new Version3Client({
20
18
  host: instance,
21
19
  authentication: {
22
- personalAccessToken: apiToken,
20
+ basic: {
21
+ email,
22
+ apiToken,
23
+ },
23
24
  },
24
25
  });
25
26
  }
@@ -34,19 +35,14 @@ export class Jira {
34
35
  this.JQL += component ? ` AND component = ${component}` : '';
35
36
  this.JQL += ' ORDER BY id DESC';
36
37
 
37
- const response = await this.api.issueSearch.searchForIssuesUsingJqlPost({
38
- jql: this.JQL,
39
- fields: [
40
- 'id',
41
- 'issuetype',
42
- 'summary',
43
- 'assignee',
44
- 'comment',
45
- this.fields.bugzillaBug,
46
- ],
47
- // We should paginate this, let's set 300 for now.
48
- maxResults: 300,
49
- });
38
+ const response =
39
+ await this.api.issueSearch.searchForIssuesUsingJqlEnhancedSearchPost({
40
+ jql: this.JQL,
41
+ fields: ['id', 'issuetype', 'summary', 'assignee', 'comment'],
42
+ expand: 'renderedFields',
43
+ // We should paginate this, let's set 300 for now.
44
+ maxResults: 300,
45
+ });
50
46
 
51
47
  return response.issues ?? raise('Jira.getIssues(): missing issues.');
52
48
  }
@@ -59,6 +55,21 @@ export class Jira {
59
55
  return response ?? [];
60
56
  }
61
57
 
58
+ static readonly bugzillaUrlRegex =
59
+ /^https?:\/\/bugzilla\.redhat\.com\/show_bug\.cgi\?id=(\d+)$/;
60
+
61
+ getBugzillaBugId(
62
+ links: Awaited<ReturnType<typeof this.getLinks>>
63
+ ): number | null {
64
+ for (const link of links) {
65
+ const match = link.object?.url?.match(Jira.bugzillaUrlRegex);
66
+ if (match) {
67
+ return Number(match[1]);
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
62
73
  async setLabels(key: string, labels: string[]) {
63
74
  if (this.dry) {
64
75
  //console.debug(`DRY: setLabels(${key}, ${labels})`);
package/src/util.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { OptionValues } from 'commander';
2
+ import os from 'node:os';
2
3
 
3
4
  export function raise(error: string): never {
4
5
  throw new Error(error);
@@ -23,12 +24,21 @@ export function tokenUnavailable(type: 'jira' | 'bugzilla' | 'github'): never {
23
24
  );
24
25
  }
25
26
 
27
+ export function getUserFromLogin(): string | undefined {
28
+ try {
29
+ const login = os.userInfo().username;
30
+ return `${login}@redhat.com`;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
26
36
  export function isDefaultValuesDisabled(): boolean {
27
37
  return process.env['NODEFAULTS'] ? true : false;
28
38
  }
29
39
 
30
40
  export function getDefaultValue(
31
- envName: 'COMPONENT' | 'UPSTREAM' | 'NOCOLOR' | 'DRY'
41
+ envName: 'COMPONENT' | 'UPSTREAM' | 'NOCOLOR' | 'DRY' | 'LOGIN'
32
42
  ) {
33
43
  if (isDefaultValuesDisabled()) {
34
44
  return undefined;
@@ -36,6 +46,10 @@ export function getDefaultValue(
36
46
 
37
47
  const value = process.env[envName];
38
48
 
49
+ if (envName === 'LOGIN' && !value) {
50
+ return getUserFromLogin();
51
+ }
52
+
39
53
  if ((envName === 'NOCOLOR' || envName === 'DRY') && !value) {
40
54
  return false;
41
55
  }
@@ -48,6 +62,7 @@ export function getOptions(inputs: OptionValues): OptionValues {
48
62
  ...inputs,
49
63
  component: inputs.component || getDefaultValue('COMPONENT'),
50
64
  upstream: inputs.upstream || getDefaultValue('UPSTREAM'),
65
+ login: inputs.login || getDefaultValue('LOGIN'),
51
66
  };
52
67
  }
53
68
 
@@ -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
+ });