@cmflow/cli 2.0.0-alpha.9 → 2.0.0-rc.2

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.
Binary file
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <h1 style="text-align: center" align="center">CMFLOW</h1>
1
+ <h1 style="text-align: center" align="center">@clubmed/cli aka ClubMed Flow </h1>
2
2
 
3
3
  <div align="center">
4
4
 
@@ -47,64 +47,63 @@ Then create `release.config.js` with this configuration:
47
47
  import { defineConfig } from '@cmflow/cli'
48
48
 
49
49
  export default defineConfig({
50
- verifyConditions: ['@cmflow/cli/semantic/verify-conditions'],
51
- analyzeCommits: ['@cmflow/cli/semantic/analyze-commits'],
52
- verifyRelease: ['@cmflow/cli/semantic/verify-release'],
50
+ verifyConditions: ['@cmflow/cli/semantic/core/verify-conditions'],
51
+ analyzeCommits: ['@cmflow/cli/semantic/core/analyze-commits'],
52
+ verifyRelease: ['@cmflow/cli/semantic/core/verify-release'],
53
53
  generateNotes: [
54
54
  [
55
55
  "@cmflow/cli/semantic/conditional", // add this task to trigger build npm task
56
56
  {
57
57
  // when: (context) => context.branch.type === "release" // default condition to run the task
58
- run: ["@semantic-release/release-notes-generator"]
58
+ run: ["@semantic-release/core/release-notes-generator"]
59
59
  }
60
60
  ]
61
61
  ],
62
62
  prepare: [
63
- '@cmflow/cli/semantic/prepare/bump-version',
63
+ '@cmflow/cli/semantic/core/prepare/bump-version',
64
64
  [
65
- '@cmflow/cli/semantic/prepare/run', // add this task to trigger build npm task
65
+ '@cmflow/cli/semantic/core/prepare/run', // add this task to trigger build npm task
66
66
  {
67
67
  command: 'build'
68
68
  }
69
69
  ],
70
70
  [
71
- '@cmflow/cli/semantic/prepare/run', // add this task to trigger build npm task
71
+ '@cmflow/cli/semantic/core/prepare/run', // add this task to trigger build npm task
72
72
  {
73
73
  command: 'test_e2e'
74
74
  }
75
75
  ],
76
76
  [
77
- '@cmflow/cli/semantic/conditional', // add this task to trigger build npm task
77
+ '@cmflow/cli/semantic/core/conditional', // add this task to trigger build npm task
78
78
  {
79
79
  // when: (context) => context.branch.type === "release" // default condition to run the task
80
80
  run: [
81
- '@cmflow/semantic/core/prepare/commit'
81
+ '@cmflow/cli/semantic/core/prepare/commit'
82
82
  ]
83
83
  }
84
84
  ]
85
85
  ],
86
86
  publish: [
87
87
  [
88
- '@cmflow/cli/semantic/conditional',
88
+ '@cmflow/cli/semantic/core/conditional',
89
89
  {
90
90
  // when: (context) => context.branch.type === "release" // default condition to run the task
91
91
  run: [
92
- '@cmflow/cli/semantic/publish/sync-repository',
92
+ '@cmflow/cli/semantic/core/sync-repository',
93
93
  '@semantic-release/github'
94
94
  ] // only run if the conditional rule is true
95
95
  }
96
96
  ],
97
97
  [
98
- '@cmflow/cli/semantic/conditional', // run process.exit(0) if the branch is not a release branch - legacy: maybe not necessary now all task are conditional
98
+ '@cmflow/cli/semantic/core/exit', // run process.exit(0) if the branch is not a release branch - legacy: maybe not necessary now all task are conditional
99
99
  {
100
- when: (context) => context.branch.type !== 'release',
101
- run: '@cmflow/cli/semantic/publish/exit.js'
100
+ when: (context) => context.nextRelease.channel === 'prerelease'
102
101
  }
103
102
  ]
104
103
  ],
105
104
  success: [
106
105
  [
107
- '@cmflow/cli/semantic/conditional',
106
+ '@cmflow/cli/semantic/core/conditional',
108
107
  {
109
108
  // when: (context) => context.branch.type === "release" // default condition to run the task
110
109
  run: '@semantic-release/github' // only run if the conditional rule is true
@@ -113,7 +112,7 @@ export default defineConfig({
113
112
  ],
114
113
  fail: [
115
114
  [
116
- "@cmflow/cli/semantic/conditional",
115
+ "@cmflow/cli/semantic/core/conditional",
117
116
  {
118
117
  // when: (context) => context.branch.type === "release" // default condition to run the task
119
118
  run: '@semantic-release/github' // only run if the conditional rule is true
@@ -139,7 +138,7 @@ Now, CmFlow and semantic release are correctly installed on your project.
139
138
 
140
139
  ### Condition step (publish, success, fail)
141
140
 
142
- CMFlow provide a `@cmflow/cli/semantic/conditional` task to run one task or many task only if a condition is true. The condition is a
141
+ CMFlow provide a `@cmflow/cli/semantic/core/conditional` task to run one task or many task only if a condition is true. The condition is a
143
142
  function that takes the semantic context as parameter and return a boolean.
144
143
 
145
144
  You can customize the condition to run the task by adding a `when` property to the configuration object.
@@ -147,7 +146,7 @@ You can customize the condition to run the task by adding a `when` property to t
147
146
  ```javascript
148
147
  export default defineConfig({
149
148
  publish: [
150
- '@cmflow/cli/semantic/conditional',
149
+ '@cmflow/cli/semantic/core/conditional',
151
150
  {
152
151
  when: (context) => context.branch.type === 'release', // default condition to run the task
153
152
  run: ['@semantic-release/github', '@semantic-release/github'] // only run if the conditional rule is true
@@ -172,6 +171,26 @@ Example:
172
171
  }
173
172
  }
174
173
  ```
174
+ Then:
175
+
176
+ ```javascript
177
+ export default defineConfig({
178
+ prepare: [
179
+ [
180
+ '@cmflow/cli/semantic/core/run',
181
+ {
182
+ command: 'build'
183
+ }
184
+ ],
185
+ [
186
+ '@cmflow/cli/semantic/core/run',
187
+ {
188
+ command: 'test_e2e'
189
+ }
190
+ ]
191
+ ]
192
+ })
193
+ ```
175
194
 
176
195
  ### Generate release.info file
177
196
 
@@ -202,33 +221,21 @@ import { defineConfig } from '@cmflow/cli'
202
221
 
203
222
  export default defineConfig({
204
223
  publish: [
205
- [
206
- '@cmflow/cli/semantic/conditional',
207
- {
208
- run: '@semantic-release/github'
209
- }
210
- ],
211
224
  '@cmflow/cli/semantic/docker/publish'
212
225
  ]
213
226
  });
214
227
  ```
215
228
 
216
- ### Clean docker tags
229
+ ### Publish & Clean docker tags
217
230
 
218
231
  ```js
219
232
  import { defineConfig } from '@cmflow/cli'
220
233
 
221
234
  export default defineConfig({
222
235
  success: [
223
- [
224
- '@cmflow/cli/semantic/success',
225
- {
226
- runAfter: '@semantic-release/github' // only run if the conditional rule is true
227
- }
228
- ],
229
- '@cmflow/cli/semantic/docker/publish'
236
+ '@cmflow/cli/semantic/docker/success'
230
237
  ]
231
- });
238
+ })
232
239
  ```
233
240
 
234
241
  ### Configure CI
@@ -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-alpha.9",
3
+ "version": "2.0.0-rc.2",
4
4
  "description": "An awesome Git Flow",
5
5
  "author": "camfou",
6
6
  "license": "MIT",
@@ -26,9 +26,7 @@
26
26
  },
27
27
  "exports": {
28
28
  ".": "./src/index.js",
29
- "./semantic/prepare/*": "./src/semantic/core/prepare/*.js",
30
- "./semantic/*": "./src/semantic/core/*.js",
31
- "./semantic/docker/*": "./src/semantic/docker/*.js"
29
+ "./semantic/*": "./src/semantic/*.js"
32
30
  },
33
31
  "dependencies": {
34
32
  "any-observable": "0.5.1",
@@ -40,9 +38,12 @@
40
38
  "fs-extra": "^10.1.0",
41
39
  "globby": "11.1.0",
42
40
  "has-yarn": "^2.1.0",
41
+ "https-proxy-agent": "7.0.6",
43
42
  "inquirer": "9.2.19",
44
43
  "listr": "0.14.3",
45
44
  "lodash": "4.17.21",
45
+ "moment": "^2.30.1",
46
+ "node-fetch": "3.3.2",
46
47
  "rxjs": "7.8.1",
47
48
  "semantic-release": ">=24.2.3",
48
49
  "semver": "7.7.1",
@@ -85,7 +86,8 @@
85
86
  "lint_ci": "eslint --format junit --output-file ./reports/eslint.xml",
86
87
  "lint_fix": "eslint --fix",
87
88
  "release": "node bin/cmrelease.js",
88
- "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"
89
91
  },
90
92
  "husky": {
91
93
  "hooks": {
@@ -31,7 +31,7 @@ export default defineConfig({
31
31
  ]
32
32
  ],
33
33
  prepare: ["./src/semantic/core/prepare/bump-version.js", "./src/semantic/core/prepare/commit.js"],
34
- publish: ["./src/semantic/core/publish/sync-repository.js", "@semantic-release/github"],
34
+ publish: ["./src/semantic/core/sync-repository.js", "@semantic-release/github"],
35
35
  success: ["@semantic-release/github"],
36
36
  fail: ["@semantic-release/github"],
37
37
  npmPublish: false
@@ -2,6 +2,7 @@ export * from "./clean.js";
2
2
  export * from "./deploy.js";
3
3
  export * from "./fetch.js";
4
4
  export * from "./finish.js";
5
+ export * from "./jira-releases.js";
5
6
  export * from "./merge.js";
6
7
  export * from "./push.js";
7
8
  export * from "./rebase.js";
@@ -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
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
+ });
@@ -3,7 +3,7 @@ import { git } from "./cli/git.js";
3
3
  export function configureGitWorkspace(context) {
4
4
  const {
5
5
  logger,
6
- config: { SCM_TOKEN, REPOSITORY_URL, ORIGIN, GIT_USER_NAME, GIT_USER_EMAIL }
6
+ config: { SCM_TOKEN, REPOSITORY_URL, ORIGIN, GIT_USER_NAME, GIT_USER_EMAIL, GIT_DISABLE_SSL }
7
7
  } = context;
8
8
 
9
9
  if (GIT_USER_NAME && GIT_USER_EMAIL) {
@@ -11,6 +11,11 @@ export function configureGitWorkspace(context) {
11
11
  git.config("--global", "user.email", `"${GIT_USER_EMAIL}"`);
12
12
  }
13
13
 
14
+ if (GIT_DISABLE_SSL) {
15
+ logger.info("Disable SSL verification");
16
+ git.config("--global", "http.sslVerify", "false");
17
+ }
18
+
14
19
  if (SCM_TOKEN) {
15
20
  const repository = REPOSITORY_URL.replace("https://", "");
16
21
 
@@ -74,8 +74,24 @@ export function getCI() {
74
74
  export function getConfig() {
75
75
  const pkg = getPackageJson();
76
76
  const [owner, projectName] = pkg.name.replace(/^@/, "").split("/");
77
- const { PROJECT_NAME, DOCKER_HUB_ID, DOCKER_HUB_PWD, REF_COMMIT, GH_TOKEN, SCM_TOKEN, REPOSITORY_URL, GIT_USER_NAME, GIT_USER_EMAIL } =
78
- process.env;
77
+ const {
78
+ PROJECT_NAME,
79
+ DOCKER_HUB_ID,
80
+ DOCKER_HUB_PWD,
81
+ REF_COMMIT,
82
+ GH_TOKEN,
83
+ SCM_TOKEN,
84
+ REPOSITORY_URL,
85
+ GIT_DISABLE_SSL,
86
+ GIT_USER_NAME,
87
+ GIT_USER_EMAIL,
88
+ GITLAB_USER_EMAIL,
89
+ GITLAB_USER_NAME,
90
+ JIRA_API,
91
+ JIRA_EMAIL,
92
+ JIRA_TOKEN,
93
+ HTTPS_PROXY
94
+ } = process.env;
79
95
 
80
96
  const HAS_YARN = hasYarn();
81
97
  const IS_YARN_BERRY = HAS_YARN ? fs.existsSync(join(process.cwd(), ".yamlrc.yml")) : false;
@@ -113,9 +129,14 @@ export function getConfig() {
113
129
  GH_TOKEN,
114
130
  SCM_TOKEN: GH_TOKEN || SCM_TOKEN,
115
131
  REPOSITORY_URL: get(pkg, "repository.url", get(pkg, "repository", REPOSITORY_URL)),
116
- GIT_USER_NAME,
117
- GIT_USER_EMAIL,
132
+ GIT_USER_NAME: GIT_USER_NAME || GITLAB_USER_NAME,
133
+ GIT_USER_EMAIL: GIT_USER_EMAIL || GITLAB_USER_EMAIL,
134
+ GIT_DISABLE_SSL,
118
135
  BUMP_METHOD,
136
+ JIRA_API,
137
+ JIRA_EMAIL,
138
+ JIRA_TOKEN,
139
+ HTTPS_PROXY,
119
140
  WORKSPACES: pkg.workspaces || [],
120
141
  ...getCI()
121
142
  };
@@ -23,9 +23,14 @@ export function fail(pluginConfig, context) {
23
23
  async function runAll(step, pluginConfig, context, defaultCondition) {
24
24
  context.config = getConfig();
25
25
 
26
- const condition = pluginConfig.when !== undefined ? pluginConfig.if(context) : defaultCondition;
26
+ const condition = pluginConfig.when !== undefined ? pluginConfig.when(context) : defaultCondition;
27
27
 
28
- if (condition && pluginConfig.run) {
28
+ if (!condition) {
29
+ context.logger.info(`Skip step "${step}" of plugin(s) "${pluginConfig.run}"`);
30
+ return;
31
+ }
32
+
33
+ if (pluginConfig.run) {
29
34
  const tasks = [].concat(pluginConfig.run);
30
35
 
31
36
  for (const task of tasks) {
@@ -34,7 +39,15 @@ async function runAll(step, pluginConfig, context, defaultCondition) {
34
39
  const taskFunction = mod[step];
35
40
 
36
41
  if (taskFunction) {
37
- await taskFunction(pluginConfig, context);
42
+ context.logger.log(` Start conditional step "${step}" of plugin "${task}"`);
43
+
44
+ try {
45
+ await taskFunction(pluginConfig, context);
46
+ context.logger.success(` Completed conditional step "${step}" of plugin "${task}"`);
47
+ } catch (er) {
48
+ context.logger.fail(` Fail conditional step "${step}" of plugin "${task}"`);
49
+ throw er;
50
+ }
38
51
  }
39
52
  }
40
53
  }
@@ -0,0 +1,14 @@
1
+ import { getConfig } from "../../common/index.js";
2
+
3
+ export default async function exit(pluginConfig, context) {
4
+ context.config = getConfig();
5
+ const condition = pluginConfig.when !== undefined ? pluginConfig.when(context) : true;
6
+
7
+ if (condition) {
8
+ // avoid next semantic-release steps when is a prerelease or release branch with SKIP_GITHUB_PUBLISH
9
+ context.logger.success("Skip next semantic-release steps");
10
+ process.exit(0);
11
+ } else {
12
+ context.logger.success("Continue next semantic-release steps");
13
+ }
14
+ }
@@ -1,11 +1,11 @@
1
- import { getConfig, npm, yarn } from "../../../common/index.js";
1
+ import { getConfig, npm, yarn } from "../../common/index.js";
2
2
 
3
3
  /**
4
4
  * @param pluginConfig {{command: string}}
5
5
  * @param context
6
6
  * @return {Promise<void>}
7
7
  */
8
- export async function prepare(pluginConfig, context) {
8
+ export default async function runCommand(pluginConfig, context) {
9
9
  context.config = getConfig();
10
10
  const {
11
11
  config: { HAS_YARN },
@@ -1,6 +1,6 @@
1
- import { getConfig, git } from "../../../common/index.js";
1
+ import { getConfig, git } from "../../common/index.js";
2
2
 
3
- export async function publish(pluginConfig, context) {
3
+ export default async function syncRepository(pluginConfig, context) {
4
4
  context.config = getConfig();
5
5
 
6
6
  const {
@@ -30,34 +30,8 @@ export function defineConfig(opts = {}) {
30
30
  generateNotes: ["@cmflow/cli/semantic/generate-notes"],
31
31
  prepare: [
32
32
  "@cmflow/cli/semantic/prepare/bump-version",
33
- // '@cmflow/cli/semantic/prepare/build',
34
- // '@cmflow/cli/semantic/prepare/e2e',
35
33
  "@cmflow/cli/semantif/prepare/post" // need to avoid semantic next step
36
34
  ],
37
- publish: [
38
- [
39
- "@cmflow/cli/semantic/publish",
40
- {
41
- runAfter: opts.runAfter
42
- }
43
- ]
44
- ],
45
- success: [
46
- [
47
- "@cmflow/cli/semantic/success",
48
- {
49
- runAfter: opts.runAfter
50
- }
51
- ]
52
- ],
53
- fail: [
54
- [
55
- "@cmflow/cli/semantic/fail",
56
- {
57
- runAfter: opts.runAfter
58
- }
59
- ]
60
- ],
61
35
  releaseRules,
62
36
  parserOpts: {
63
37
  noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"]
@@ -1,7 +0,0 @@
1
- import { getConfig } from "../../../common/index.js";
2
-
3
- export async function prepare(pluginConfig, context) {
4
- context.config = getConfig();
5
- // avoid next semantic-release steps when is a prerelease or release branch with SKIP_GITHUB_PUBLISH
6
- process.exit(0);
7
- }