@cmflow/cli 2.0.0-rc.1 → 2.0.0-rc.3
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/.yarn/install-state.gz +0 -0
- package/bin/cm-jira-releases.js +16 -0
- package/bin/cm.js +1 -0
- package/package.json +6 -2
- package/src/commands/index.js +1 -0
- package/src/commands/jira-releases.js +86 -0
- package/src/common/back/http/http-proxy.js +15 -0
- package/src/common/back/http/http-request.js +65 -0
- package/src/common/back/http/http-request.spec.js +47 -0
- package/src/common/back/jira/interfaces/JiraIIssue.d.ts +100 -0
- package/src/common/back/jira/jira-get-releases-and-issues.js +33 -0
- package/src/common/back/jira/jira-issue-move-to.js +39 -0
- package/src/common/back/jira/jira-releases.js +38 -0
- package/src/common/back/jira/jira-request.js +17 -0
- package/src/common/back/jira/jira-request.spec.js +53 -0
- package/src/common/back/jira/jira-search.js +17 -0
- package/src/common/back/jira/jira-search.spec.js +43 -0
- package/src/common/get-config.js +9 -1
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import commander from "commander";
|
|
3
|
+
|
|
4
|
+
import { commands, runCommand } from "../src/index.js";
|
|
5
|
+
|
|
6
|
+
commander
|
|
7
|
+
.usage("cm jira-releases [options]")
|
|
8
|
+
.alias("cm jira-releases")
|
|
9
|
+
.option("-c, --component <compoent>", "Jira component filter")
|
|
10
|
+
.option("-c, --project-id <projectId>", "Jira projectId filter")
|
|
11
|
+
.option("-u, --update", "Update jira releases status if it's necessary", (v, t) => t + 1, 0)
|
|
12
|
+
.option("-v, --verbose", "Enable verbose log", (v, t) => t + 1, 0)
|
|
13
|
+
.option("-t, --tag <tag>", "The tag version you want to check or/and update")
|
|
14
|
+
.parse(process.argv);
|
|
15
|
+
|
|
16
|
+
runCommand(commands.JiraReleases, commander);
|
package/bin/cm.js
CHANGED
|
@@ -21,6 +21,7 @@ commander
|
|
|
21
21
|
.command("rebase", "Rebase the current branch from production")
|
|
22
22
|
.command("merge", "Merge current branch into PRODUCTION_BRANCH")
|
|
23
23
|
.command("fetch", "Download objects and refs from repository (--all and --prune)")
|
|
24
|
+
.command("jira-releases", "Check jira releases status from given status (and update releases statuses if --update is set)")
|
|
24
25
|
.command("clean", "Clean deployed docker image on Docker hub")
|
|
25
26
|
.command("config", "Configure git workspace with expected information")
|
|
26
27
|
.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmflow/cli",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.3",
|
|
4
4
|
"description": "An awesome Git Flow",
|
|
5
5
|
"author": "camfou",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,9 +38,12 @@
|
|
|
38
38
|
"fs-extra": "^10.1.0",
|
|
39
39
|
"globby": "11.1.0",
|
|
40
40
|
"has-yarn": "^2.1.0",
|
|
41
|
+
"https-proxy-agent": "7.0.6",
|
|
41
42
|
"inquirer": "9.2.19",
|
|
42
43
|
"listr": "0.14.3",
|
|
43
44
|
"lodash": "4.17.21",
|
|
45
|
+
"moment": "^2.30.1",
|
|
46
|
+
"node-fetch": "3.3.2",
|
|
44
47
|
"rxjs": "7.8.1",
|
|
45
48
|
"semantic-release": ">=24.2.3",
|
|
46
49
|
"semver": "7.7.1",
|
|
@@ -83,7 +86,8 @@
|
|
|
83
86
|
"lint_ci": "eslint --format junit --output-file ./reports/eslint.xml",
|
|
84
87
|
"lint_fix": "eslint --fix",
|
|
85
88
|
"release": "node bin/cmrelease.js",
|
|
86
|
-
"release_dry_run": "node bin/cmrelease.js --dry-run"
|
|
89
|
+
"release_dry_run": "node bin/cmrelease.js --dry-run",
|
|
90
|
+
"jira_releases": "node --env-file=.env bin/cm.js jira-releases --project-id CMAB --component API --tag 0.3238.2 --verbose"
|
|
87
91
|
},
|
|
88
92
|
"husky": {
|
|
89
93
|
"hooks": {
|
package/src/commands/index.js
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import figures from "figures";
|
|
3
|
+
import moment from "moment";
|
|
4
|
+
|
|
5
|
+
import { jiraGetReleasesAndIssues } from "../common/back/jira/jira-get-releases-and-issues.js";
|
|
6
|
+
import { jiraIssueMoveTo } from "../common/back/jira/jira-issue-move-to.js";
|
|
7
|
+
import { updateJiraRelease } from "../common/back/jira/jira-releases.js";
|
|
8
|
+
|
|
9
|
+
const TYPES = {
|
|
10
|
+
Story: "feat",
|
|
11
|
+
"Sous-tâche": "tech",
|
|
12
|
+
Bug: "fix"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class JiraReleases {
|
|
16
|
+
mapContext(commander, context) {
|
|
17
|
+
return {
|
|
18
|
+
...context,
|
|
19
|
+
projectId: commander.projectId,
|
|
20
|
+
update: commander.update,
|
|
21
|
+
component: commander.component,
|
|
22
|
+
tag: commander.tag.replace(/^version-/, "").replace(/^v/, "")
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getTasks(context) {
|
|
27
|
+
if (!context.tag) {
|
|
28
|
+
console.log(chalk.red(figures.cross), "Missing tag version. Skipping release notes generation");
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
title: `List all releases under ${context.tag} for ${context.component} component and ${context.projectId} project`,
|
|
35
|
+
task: async () => {
|
|
36
|
+
context.releases = await jiraGetReleasesAndIssues(context.projectId, context.component, context.tag);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: `Update ${context.tag} on ${context.component} component`,
|
|
41
|
+
skip: () => !context.update,
|
|
42
|
+
task: async () => {
|
|
43
|
+
for (const release of context.releases) {
|
|
44
|
+
release.released = true;
|
|
45
|
+
release.releaseDate = moment().format("YYYY-MM-DD");
|
|
46
|
+
|
|
47
|
+
await updateJiraRelease(release);
|
|
48
|
+
|
|
49
|
+
await Promise.all(
|
|
50
|
+
release.issues.map((issue) => {
|
|
51
|
+
return jiraIssueMoveTo(issue, {
|
|
52
|
+
id: "18",
|
|
53
|
+
name: "Livré en production"
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
success(context) {
|
|
64
|
+
console.log(chalk.green(figures.tick), "Release notes generated successfully");
|
|
65
|
+
console.log("\n");
|
|
66
|
+
|
|
67
|
+
const note = `Bonjour,
|
|
68
|
+
Nous allons livrer en production la version ${context.tag} de l'${context.component}
|
|
69
|
+
Au menu de cette livraison :
|
|
70
|
+
|
|
71
|
+
${context.releases
|
|
72
|
+
.map((release) => {
|
|
73
|
+
const issues = release.issues.map((issue) => {
|
|
74
|
+
return ` - ${TYPES[issue.fields.issuetype.name] || issue.fields.issuetype}(${issue.key}): ${issue.fields.summary}`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (context.releases.length > 1 ? "- v" + release.version + ":\n" : "") + issues.join(",\n");
|
|
78
|
+
})
|
|
79
|
+
.join("\n\n")}
|
|
80
|
+
|
|
81
|
+
L'équipe API
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
console.log(note);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
2
|
+
|
|
3
|
+
import { getConfig } from "../../get-config.js";
|
|
4
|
+
|
|
5
|
+
const { HTTPS_PROXY } = getConfig();
|
|
6
|
+
|
|
7
|
+
export const getHttpProxy = (opts) => {
|
|
8
|
+
if (HTTPS_PROXY) {
|
|
9
|
+
console.debug({
|
|
10
|
+
message: `Using proxy agent for ${opts.callee} requests`,
|
|
11
|
+
value: HTTPS_PROXY
|
|
12
|
+
});
|
|
13
|
+
return new HttpsProxyAgent(HTTPS_PROXY, opts);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
|
|
3
|
+
import { getHttpProxy } from "./http-proxy.js";
|
|
4
|
+
|
|
5
|
+
// export type HttpRequestOptions = {
|
|
6
|
+
// rejectUnauthorized?: boolean;
|
|
7
|
+
// callee?: string;
|
|
8
|
+
// params?: Record<string, string>;
|
|
9
|
+
// } & fetch.RequestInit;
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object & RequestInit} HttpRequestOptions
|
|
12
|
+
* @property {boolean} [rejectUnauthorized] - Whether to reject unauthorized SSL certificates
|
|
13
|
+
* @property {string} [callee] - The name of the caller
|
|
14
|
+
* @property {Record<string, string>} [params] - Query parameters to be included in the request
|
|
15
|
+
* @property {string} [method] - HTTP method (default: 'get')
|
|
16
|
+
* @property {Record<string, string>} [headers] - Custom headers to be included in the request
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param endpoint {string}
|
|
22
|
+
* @param options {HttpRequestOptions}
|
|
23
|
+
* @return {Promise<*>}
|
|
24
|
+
*/
|
|
25
|
+
export async function httpRequest(endpoint, options = {}) {
|
|
26
|
+
const { callee, headers, method = "get", params = {}, rejectUnauthorized } = options;
|
|
27
|
+
const agent = getHttpProxy({
|
|
28
|
+
rejectUnauthorized,
|
|
29
|
+
callee
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const queryParams = new URLSearchParams(params);
|
|
33
|
+
const url = `${endpoint}${queryParams.size ? "?" + queryParams : ""}`;
|
|
34
|
+
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method,
|
|
37
|
+
headers: {
|
|
38
|
+
...headers,
|
|
39
|
+
"Content-Type": "application/json"
|
|
40
|
+
},
|
|
41
|
+
agent,
|
|
42
|
+
...options
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
console.error({
|
|
47
|
+
...options,
|
|
48
|
+
message: `Fail to retrieved data from ${callee}`,
|
|
49
|
+
url,
|
|
50
|
+
method,
|
|
51
|
+
status: response.status,
|
|
52
|
+
response: await response.json()
|
|
53
|
+
});
|
|
54
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.info({
|
|
58
|
+
message: `Successfully retrieved data from ${callee}`,
|
|
59
|
+
url,
|
|
60
|
+
method,
|
|
61
|
+
status: response.status
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return response.json();
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
|
|
3
|
+
import { getHttpProxy } from "./http-proxy.js";
|
|
4
|
+
import { httpRequest } from "./http-request.js";
|
|
5
|
+
|
|
6
|
+
vi.mock("node-fetch");
|
|
7
|
+
vi.mock("./http-proxy.js");
|
|
8
|
+
|
|
9
|
+
describe("httpRequest", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should create an agent in production with proxy", async () => {
|
|
15
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
16
|
+
status: 200,
|
|
17
|
+
headers: { "content-type": "application/json" },
|
|
18
|
+
ok: true,
|
|
19
|
+
async json() {
|
|
20
|
+
return { data: "test" };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = await httpRequest("http://example.com/test-endpoint", {
|
|
25
|
+
callee: "test"
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(getHttpProxy).toHaveBeenCalledWith({
|
|
29
|
+
rejectUnauthorized: undefined,
|
|
30
|
+
callee: "test"
|
|
31
|
+
});
|
|
32
|
+
expect(result).toEqual({ data: "test" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should throw an error if the response is not ok", async () => {
|
|
36
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
37
|
+
status: 500,
|
|
38
|
+
headers: { "content-type": "application/json" },
|
|
39
|
+
ok: false,
|
|
40
|
+
async json() {
|
|
41
|
+
return { data: "test" };
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await expect(httpRequest("http://example.com/test-endpoint", { callee: "test" })).rejects.toThrow("HTTP error! status: 500");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ADFDocument } from './ADFDocument.js';
|
|
2
|
+
import type { JiraChangelog } from './JiraChangelogIssueResponse.js';
|
|
3
|
+
|
|
4
|
+
export interface JiraStatus {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface JiraUser {
|
|
10
|
+
accountId?: string;
|
|
11
|
+
displayName?: string;
|
|
12
|
+
emailAddress?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateJiraComment {
|
|
16
|
+
body: ADFDocument;
|
|
17
|
+
author: JiraUser;
|
|
18
|
+
notifyUsers?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JiraComment extends CreateJiraComment {
|
|
22
|
+
id: string;
|
|
23
|
+
body: ADFDocument;
|
|
24
|
+
author: JiraUser;
|
|
25
|
+
created: string;
|
|
26
|
+
updated?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface JiraAttachment {
|
|
30
|
+
id: string;
|
|
31
|
+
filename: string;
|
|
32
|
+
content: string; // URL de téléchargement
|
|
33
|
+
size: number;
|
|
34
|
+
mimeType?: string;
|
|
35
|
+
created: string;
|
|
36
|
+
author: JiraUser;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface JiraIssueLink {
|
|
40
|
+
id: string;
|
|
41
|
+
type: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string; // Ex: "relates to", "blocks"
|
|
44
|
+
inward: string; // Ex: "is blocked by"
|
|
45
|
+
outward: string; // Ex: "blocks"
|
|
46
|
+
};
|
|
47
|
+
inwardIssue?: Partial<JiraIssue>; // Issue liée en amont
|
|
48
|
+
outwardIssue?: Partial<JiraIssue>; // Issue liée en aval
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CreateJiraFieldsIssue {
|
|
52
|
+
summary: string; // Titre de l'issue
|
|
53
|
+
description?: ADFDocument; // Description détaillée
|
|
54
|
+
labels?: string[]; // Liste des labels
|
|
55
|
+
issuetype: {
|
|
56
|
+
name: string; // Type d'issue (ex: "Bug", "Task")
|
|
57
|
+
};
|
|
58
|
+
project: {
|
|
59
|
+
key: string;
|
|
60
|
+
};
|
|
61
|
+
assignee?: Partial<JiraUser>; // Utilisateur assigné
|
|
62
|
+
reporter?: Partial<JiraUser>; // Créateur de l'issue
|
|
63
|
+
status?: Omit<JiraStatus, 'name'>;
|
|
64
|
+
components?: { name: string }[];
|
|
65
|
+
|
|
66
|
+
[customFieldId: `customfield_${string}`]: unknown; // Autres champs personnalisés
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CreateJiraIssue {
|
|
70
|
+
fields: CreateJiraFieldsIssue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface JiraIssue {
|
|
74
|
+
id: string; // ID unique de l'issue
|
|
75
|
+
key: string; // Clé de l'issue (ex: "PROJ-123")
|
|
76
|
+
self: string; // URL de l'issue
|
|
77
|
+
|
|
78
|
+
fields: CreateJiraFieldsIssue & {
|
|
79
|
+
issuetype: {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string; // Type d'issue (ex: "Bug", "Task")
|
|
82
|
+
};
|
|
83
|
+
project: {
|
|
84
|
+
id: string;
|
|
85
|
+
key: string;
|
|
86
|
+
name: string;
|
|
87
|
+
};
|
|
88
|
+
comment?: {
|
|
89
|
+
comments: JiraComment[];
|
|
90
|
+
total: number;
|
|
91
|
+
};
|
|
92
|
+
status: JiraStatus;
|
|
93
|
+
attachment?: JiraAttachment[]; // Liste des pièces jointes
|
|
94
|
+
issuelinks?: JiraIssueLink[]; // Relations avec d'autres issues
|
|
95
|
+
created: string; // Date de création
|
|
96
|
+
updated: string; // Dernière mise à jour
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type JiraIssueWithChangelog = JiraIssue & { changelog: JiraChangelog };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
|
|
3
|
+
import { getJiraReleases, getJiraVersionRelatedIssues } from "./jira-releases.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns all releases under the tag version for the given project and component.
|
|
7
|
+
* @param projectId {string}
|
|
8
|
+
* @param component {string}
|
|
9
|
+
* @param tagVersion {string}
|
|
10
|
+
*/
|
|
11
|
+
export async function jiraGetReleasesAndIssues(projectId, component, tagVersion) {
|
|
12
|
+
const releases = await getJiraReleases(projectId);
|
|
13
|
+
|
|
14
|
+
const promises = releases
|
|
15
|
+
.filter((release) => {
|
|
16
|
+
release.version = release.name
|
|
17
|
+
.replace(component + "-", "")
|
|
18
|
+
.replace("version-", "")
|
|
19
|
+
.replace("v", "");
|
|
20
|
+
try {
|
|
21
|
+
return semver.lte(release.version, tagVersion);
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.map(async (release) => {
|
|
27
|
+
const { issues } = await getJiraVersionRelatedIssues(projectId, release.name);
|
|
28
|
+
release.issues = issues;
|
|
29
|
+
return release;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return Promise.all(promises);
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jiraRequest } from "./jira-request.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moves a JIRA issue to the correct state using the specified transition.
|
|
5
|
+
* @param issue
|
|
6
|
+
* @param toTransition
|
|
7
|
+
* @return {Promise<void>}
|
|
8
|
+
*/
|
|
9
|
+
export async function jiraIssueMoveTo(issue, toTransition) {
|
|
10
|
+
/**
|
|
11
|
+
* {
|
|
12
|
+
* transitions: {
|
|
13
|
+
* id: string;
|
|
14
|
+
* name: string;
|
|
15
|
+
* to: { id: string; name: string };
|
|
16
|
+
* }[];
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
const { transitions } = await jiraRequest(`/rest/api/3/issue/${issue.key}/transitions`);
|
|
20
|
+
const transition = transitions.find((transition) => {
|
|
21
|
+
return transition.id === toTransition.id;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (transition) {
|
|
25
|
+
await jiraRequest(`/rest/api/3/issue/${issue.key}/transitions`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Accept: "application/json"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
transition: {
|
|
33
|
+
id: transition.id,
|
|
34
|
+
notifyUsers: false
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jiraRequest } from "./jira-request.js";
|
|
2
|
+
import { jiraSearch } from "./jira-search.js";
|
|
3
|
+
|
|
4
|
+
export async function getJiraReleases(projectId) {
|
|
5
|
+
const endpoint = `/rest/api/3/project/${projectId}/versions?expand`;
|
|
6
|
+
const response = await jiraRequest(endpoint);
|
|
7
|
+
|
|
8
|
+
if (!response || !Array.isArray(response)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return response;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function updateJiraRelease(release) {
|
|
16
|
+
const endpoint = `/rest/api/3/version/${release.id}`;
|
|
17
|
+
|
|
18
|
+
await jiraRequest(endpoint, {
|
|
19
|
+
method: "PUT",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
Accept: "application/json"
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
id: release.id,
|
|
26
|
+
description: release.description,
|
|
27
|
+
name: release.name,
|
|
28
|
+
archived: release.archived,
|
|
29
|
+
released: release.released,
|
|
30
|
+
projectId: release.projectId,
|
|
31
|
+
releaseDate: release.releaseDate
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getJiraVersionRelatedIssues(projectId, tagVersion) {
|
|
37
|
+
return jiraSearch(`project = "${projectId}" AND fixVersion="${tagVersion}" ORDER BY created DESC`);
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getConfig } from "../../get-config.js";
|
|
2
|
+
import { httpRequest } from "../http/http-request.js";
|
|
3
|
+
|
|
4
|
+
export async function jiraRequest(endpoint, opts = {}) {
|
|
5
|
+
const { JIRA_API, JIRA_TOKEN, JIRA_EMAIL } = getConfig();
|
|
6
|
+
const jiraUrl = `${JIRA_API}${endpoint}`;
|
|
7
|
+
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_TOKEN}`).toString("base64");
|
|
8
|
+
|
|
9
|
+
return httpRequest(jiraUrl, {
|
|
10
|
+
...opts,
|
|
11
|
+
callee: "JIRA",
|
|
12
|
+
headers: {
|
|
13
|
+
...opts.headers,
|
|
14
|
+
Authorization: `Basic ${auth}`
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { httpRequest } from "../http/http-request.js";
|
|
2
|
+
import { jiraRequest } from "./jira-request.js";
|
|
3
|
+
|
|
4
|
+
vi.mock("../http/http-request.js");
|
|
5
|
+
|
|
6
|
+
describe("jiraRequest", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should make a GET request to the JIRA API", async () => {
|
|
12
|
+
const mockResponse = { data: "test" };
|
|
13
|
+
vi.mocked(httpRequest).mockResolvedValue(mockResponse);
|
|
14
|
+
|
|
15
|
+
const result = await jiraRequest("/test-endpoint");
|
|
16
|
+
|
|
17
|
+
expect(httpRequest).toHaveBeenCalledWith(
|
|
18
|
+
expect.stringContaining("/test-endpoint"),
|
|
19
|
+
expect.objectContaining({
|
|
20
|
+
headers: expect.objectContaining({
|
|
21
|
+
Authorization: expect.stringContaining("Basic ")
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
expect(result).toEqual(mockResponse);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should make a POST request to the JIRA API", async () => {
|
|
29
|
+
const mockResponse = { data: "test" };
|
|
30
|
+
vi.mocked(httpRequest).mockResolvedValue(mockResponse);
|
|
31
|
+
|
|
32
|
+
const result = await jiraRequest("/test-endpoint", {
|
|
33
|
+
method: "post",
|
|
34
|
+
params: { key: "value" }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(httpRequest).toHaveBeenCalledWith(expect.stringContaining("/test-endpoint"), {
|
|
38
|
+
callee: "JIRA",
|
|
39
|
+
method: "post",
|
|
40
|
+
params: { key: "value" },
|
|
41
|
+
headers: expect.objectContaining({
|
|
42
|
+
Authorization: expect.stringContaining("Basic ")
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
expect(result).toEqual(mockResponse);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should throw an error if the httpRequest fails", async () => {
|
|
49
|
+
vi.mocked(httpRequest).mockRejectedValue(new Error("Request failed"));
|
|
50
|
+
|
|
51
|
+
await expect(jiraRequest("test-endpoint")).rejects.toThrow("Request failed");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jiraRequest } from "./jira-request.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Search JIRA issues using JQL (JIRA Query Language).
|
|
5
|
+
* @template Expand
|
|
6
|
+
* @param jql {string} JIRA Query Language
|
|
7
|
+
* @param opts {object} Options
|
|
8
|
+
* @returns {Promise<{issues: (import("./interfaces/JiraIIssue.d.ts").JiraIssue & Expand)[], total: number, maxResults: number}>}
|
|
9
|
+
*/
|
|
10
|
+
export function jiraSearch(jql, opts) {
|
|
11
|
+
return jiraRequest("/rest/api/3/search", {
|
|
12
|
+
params: {
|
|
13
|
+
...opts,
|
|
14
|
+
jql
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jiraRequest } from "./jira-request.js";
|
|
2
|
+
import { jiraSearch } from "./jira-search.js";
|
|
3
|
+
|
|
4
|
+
vi.mock("./jira-request.js");
|
|
5
|
+
|
|
6
|
+
describe("jiraSearch", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should call jiraRequest with the correct JQL", async () => {
|
|
12
|
+
const mockResponse = { issues: [] };
|
|
13
|
+
vi.mocked(jiraRequest).mockResolvedValue(mockResponse);
|
|
14
|
+
|
|
15
|
+
const jql = "project = TEST";
|
|
16
|
+
const result = await jiraSearch(jql);
|
|
17
|
+
|
|
18
|
+
expect(jiraRequest).toHaveBeenCalledWith("/rest/api/3/search", {
|
|
19
|
+
params: {
|
|
20
|
+
jql
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
expect(result).toEqual(mockResponse);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should call jiraRequest with the correct JQL and expend option", async () => {
|
|
27
|
+
const mockResponse = { issues: [] };
|
|
28
|
+
vi.mocked(jiraRequest).mockResolvedValue(mockResponse);
|
|
29
|
+
|
|
30
|
+
const jql = "project = TEST";
|
|
31
|
+
const result = await jiraSearch(jql, {
|
|
32
|
+
expand: "changelog"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(jiraRequest).toHaveBeenCalledWith("/rest/api/3/search", {
|
|
36
|
+
params: {
|
|
37
|
+
expand: "changelog",
|
|
38
|
+
jql
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
expect(result).toEqual(mockResponse);
|
|
42
|
+
});
|
|
43
|
+
});
|
package/src/common/get-config.js
CHANGED
|
@@ -86,7 +86,11 @@ export function getConfig() {
|
|
|
86
86
|
GIT_USER_NAME,
|
|
87
87
|
GIT_USER_EMAIL,
|
|
88
88
|
GITLAB_USER_EMAIL,
|
|
89
|
-
GITLAB_USER_NAME
|
|
89
|
+
GITLAB_USER_NAME,
|
|
90
|
+
JIRA_API,
|
|
91
|
+
JIRA_EMAIL,
|
|
92
|
+
JIRA_TOKEN,
|
|
93
|
+
HTTPS_PROXY
|
|
90
94
|
} = process.env;
|
|
91
95
|
|
|
92
96
|
const HAS_YARN = hasYarn();
|
|
@@ -129,6 +133,10 @@ export function getConfig() {
|
|
|
129
133
|
GIT_USER_EMAIL: GIT_USER_EMAIL || GITLAB_USER_EMAIL,
|
|
130
134
|
GIT_DISABLE_SSL,
|
|
131
135
|
BUMP_METHOD,
|
|
136
|
+
JIRA_API,
|
|
137
|
+
JIRA_EMAIL,
|
|
138
|
+
JIRA_TOKEN,
|
|
139
|
+
HTTPS_PROXY,
|
|
132
140
|
WORKSPACES: pkg.workspaces || [],
|
|
133
141
|
...getCI()
|
|
134
142
|
};
|