@logickernel/agileflow 0.2.1 → 0.4.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 +2 -2
- package/src/index.js +68 -23
- package/src/utils.js +78 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logickernel/agileflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Automatic semantic versioning and changelog generation based on conventional commits",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"type": "git",
|
|
35
35
|
"url": "git@code.logickernel.com:kernel/agileflow.git"
|
|
36
36
|
},
|
|
37
|
-
"author": "",
|
|
37
|
+
"author": "Víctor H. Valle <victor.valle@logickernel.com>",
|
|
38
38
|
"license": "ISC"
|
|
39
39
|
}
|
package/src/index.js
CHANGED
|
@@ -12,9 +12,9 @@ Usage:
|
|
|
12
12
|
|
|
13
13
|
Commands:
|
|
14
14
|
<none> Prints the current version, next version, commits, and changelog
|
|
15
|
-
push Push a semantic version tag to the remote repository
|
|
16
|
-
gitlab
|
|
17
|
-
github
|
|
15
|
+
push Push a semantic version tag to the remote repository
|
|
16
|
+
gitlab Create a semantic version tag via GitLab API (for GitLab CI)
|
|
17
|
+
github Create a semantic version tag via GitHub API (for GitHub Actions)
|
|
18
18
|
|
|
19
19
|
Options:
|
|
20
20
|
--quiet Only output the next version (or empty if no bump)
|
|
@@ -26,11 +26,32 @@ For more information, visit: https://code.logickernel.com/tools/agileflow
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Valid options that can be passed to commands.
|
|
30
|
+
*/
|
|
31
|
+
const VALID_OPTIONS = ['--quiet', '--help', '-h', '--version', '-v'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Valid commands.
|
|
35
|
+
*/
|
|
36
|
+
const VALID_COMMANDS = ['push', 'gitlab', 'github'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parses command line arguments and validates them.
|
|
30
40
|
* @param {Array<string>} args - Command line arguments
|
|
31
41
|
* @returns {{quiet: boolean}}
|
|
42
|
+
* @throws {Error} If invalid options are found
|
|
32
43
|
*/
|
|
33
44
|
function parseArgs(args) {
|
|
45
|
+
// Check for invalid options
|
|
46
|
+
for (const arg of args) {
|
|
47
|
+
if (arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
|
|
48
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith('-') && !arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
|
|
51
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
return {
|
|
35
56
|
quiet: args.includes('--quiet'),
|
|
36
57
|
};
|
|
@@ -38,24 +59,32 @@ function parseArgs(args) {
|
|
|
38
59
|
|
|
39
60
|
/**
|
|
40
61
|
* Displays version info to the console.
|
|
41
|
-
* @param {{currentVersion: string|null,
|
|
42
|
-
* @param {boolean} quiet - Only output the
|
|
62
|
+
* @param {{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}} info
|
|
63
|
+
* @param {boolean} quiet - Only output the new version
|
|
43
64
|
*/
|
|
44
65
|
function displayVersionInfo(info, quiet) {
|
|
45
|
-
const { currentVersion,
|
|
66
|
+
const { currentVersion, newVersion, commits, changelog } = info;
|
|
46
67
|
|
|
47
68
|
if (quiet) {
|
|
48
|
-
if (
|
|
49
|
-
console.log(
|
|
69
|
+
if (newVersion) {
|
|
70
|
+
console.log(newVersion);
|
|
50
71
|
}
|
|
51
72
|
return;
|
|
52
73
|
}
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log(`Commits since current version
|
|
75
|
+
|
|
76
|
+
// List commits
|
|
77
|
+
console.log(`Commits since current version (${commits.length}):`);
|
|
78
|
+
for (const commit of commits) {
|
|
79
|
+
const subject = commit.message.split('\n')[0].trim();
|
|
80
|
+
const shortHash = commit.hash.substring(0, 7);
|
|
81
|
+
console.log(` ${shortHash} ${subject}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\nCurrent version: ${currentVersion || 'none'}`);
|
|
85
|
+
console.log(`New version: ${newVersion || 'no bump needed'}`);
|
|
57
86
|
if (changelog) {
|
|
58
|
-
console.log(`\nChangelog:\n${changelog}`);
|
|
87
|
+
console.log(`\nChangelog:\n\n${changelog}`);
|
|
59
88
|
}
|
|
60
89
|
}
|
|
61
90
|
|
|
@@ -71,10 +100,7 @@ async function handlePushCommand(pushType, options) {
|
|
|
71
100
|
displayVersionInfo(info, options.quiet);
|
|
72
101
|
|
|
73
102
|
// Skip push if no version bump needed
|
|
74
|
-
if (!info.
|
|
75
|
-
if (!options.quiet) {
|
|
76
|
-
console.log('\nNo version bump needed. Skipping tag creation.');
|
|
77
|
-
}
|
|
103
|
+
if (!info.newVersion) {
|
|
78
104
|
return;
|
|
79
105
|
}
|
|
80
106
|
|
|
@@ -93,22 +119,32 @@ async function handlePushCommand(pushType, options) {
|
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
// Create tag message from changelog
|
|
96
|
-
const tagMessage = info.changelog || info.
|
|
122
|
+
const tagMessage = info.changelog || info.newVersion;
|
|
97
123
|
|
|
98
124
|
if (!options.quiet) {
|
|
99
|
-
console.log(`\nCreating tag ${info.
|
|
125
|
+
console.log(`\nCreating tag ${info.newVersion}...`);
|
|
100
126
|
}
|
|
101
127
|
|
|
102
|
-
await pushModule.pushTag(info.
|
|
128
|
+
await pushModule.pushTag(info.newVersion, tagMessage);
|
|
103
129
|
|
|
104
130
|
if (!options.quiet) {
|
|
105
|
-
console.log(`Tag ${info.
|
|
131
|
+
console.log(`Tag ${info.newVersion} created and pushed successfully.`);
|
|
106
132
|
}
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
async function main() {
|
|
110
136
|
const [, , cmd, ...rest] = process.argv;
|
|
111
|
-
|
|
137
|
+
|
|
138
|
+
let options;
|
|
139
|
+
try {
|
|
140
|
+
options = parseArgs(cmd ? [cmd, ...rest] : rest);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(`Error: ${err.message}`);
|
|
143
|
+
console.error();
|
|
144
|
+
printHelp();
|
|
145
|
+
process.exit(1);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
112
148
|
|
|
113
149
|
// Handle help
|
|
114
150
|
if (cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
@@ -129,13 +165,22 @@ async function main() {
|
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
// Unknown command (not an option)
|
|
132
|
-
if (cmd && !cmd.startsWith('--')) {
|
|
168
|
+
if (cmd && !cmd.startsWith('--') && !cmd.startsWith('-')) {
|
|
133
169
|
console.error(`Error: Unknown command "${cmd}"`);
|
|
134
170
|
console.error();
|
|
135
171
|
printHelp();
|
|
136
172
|
process.exit(1);
|
|
137
173
|
}
|
|
138
174
|
|
|
175
|
+
// Invalid option (starts with -- but not valid)
|
|
176
|
+
if (cmd && cmd.startsWith('--') && !VALID_OPTIONS.includes(cmd)) {
|
|
177
|
+
console.error(`Error: Unknown option "${cmd}"`);
|
|
178
|
+
console.error();
|
|
179
|
+
printHelp();
|
|
180
|
+
process.exit(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
139
184
|
// Default: show version info
|
|
140
185
|
const info = await processVersionInfo();
|
|
141
186
|
displayVersionInfo(info, options.quiet);
|
package/src/utils.js
CHANGED
|
@@ -67,6 +67,21 @@ const TYPE_ORDER = ['feat', 'fix', 'perf', 'refactor', 'style', 'test', 'docs',
|
|
|
67
67
|
const PATCH_TYPES = ['fix', 'perf', 'refactor', 'test', 'build', 'ci', 'revert'];
|
|
68
68
|
const SEMVER_PATTERN = /^v(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9.-]+)?$/;
|
|
69
69
|
|
|
70
|
+
// Friendly header names for changelog
|
|
71
|
+
const TYPE_HEADERS = {
|
|
72
|
+
feat: 'Features:',
|
|
73
|
+
fix: 'Fixes:',
|
|
74
|
+
perf: 'Performance:',
|
|
75
|
+
refactor: 'Refactors:',
|
|
76
|
+
style: 'Style:',
|
|
77
|
+
test: 'Tests:',
|
|
78
|
+
docs: 'Documentation:',
|
|
79
|
+
build: 'Build:',
|
|
80
|
+
ci: 'CI:',
|
|
81
|
+
chore: 'Chores:',
|
|
82
|
+
revert: 'Reverts:',
|
|
83
|
+
};
|
|
84
|
+
|
|
70
85
|
/**
|
|
71
86
|
* Fetches tags from remote (non-destructive) if a remote is configured.
|
|
72
87
|
* @returns {boolean} True if tags were fetched, false if using local tags only
|
|
@@ -155,13 +170,16 @@ function extractIssueReference(message) {
|
|
|
155
170
|
* @param {string} subject - First line of commit message
|
|
156
171
|
* @param {Object} parsed - Parsed conventional commit info
|
|
157
172
|
* @param {string} fullMessage - Full commit message
|
|
173
|
+
* @param {boolean} isBreakingSection - Whether this is for the breaking changes section
|
|
158
174
|
* @returns {string} Formatted description
|
|
159
175
|
*/
|
|
160
|
-
function formatChangelogDescription(subject, parsed, fullMessage) {
|
|
176
|
+
function formatChangelogDescription(subject, parsed, fullMessage, isBreakingSection = false) {
|
|
161
177
|
if (!parsed) return subject;
|
|
162
178
|
let description = parsed.description;
|
|
163
179
|
const isBreaking = parsed.breaking || /BREAKING CHANGE:/i.test(fullMessage);
|
|
164
|
-
|
|
180
|
+
|
|
181
|
+
// Only add BREAKING prefix if not in breaking changes section
|
|
182
|
+
if (isBreaking && !isBreakingSection) {
|
|
165
183
|
description = `BREAKING: ${description}`;
|
|
166
184
|
}
|
|
167
185
|
return description;
|
|
@@ -214,13 +232,25 @@ function applyVersionBump(current, bump) {
|
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Checks if a commit is a breaking change.
|
|
237
|
+
* @param {Object} commit - Commit object
|
|
238
|
+
* @param {Object} parsed - Parsed conventional commit info
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
function isBreakingChange(commit, parsed) {
|
|
242
|
+
if (!parsed) return false;
|
|
243
|
+
return parsed.breaking || /BREAKING CHANGE:/i.test(commit.message);
|
|
244
|
+
}
|
|
245
|
+
|
|
217
246
|
/**
|
|
218
247
|
* Analyzes commits to determine version bump requirements.
|
|
219
248
|
* @param {Array} commits - Array of commit objects
|
|
220
|
-
* @returns {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean, commitsByType: Object}}
|
|
249
|
+
* @returns {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean, commitsByType: Object, breakingCommits: Array}}
|
|
221
250
|
*/
|
|
222
251
|
function analyzeCommitsForVersioning(commits) {
|
|
223
252
|
const commitsByType = Object.fromEntries(TYPE_ORDER.map(t => [t, []]));
|
|
253
|
+
const breakingCommits = [];
|
|
224
254
|
let hasBreaking = false, hasFeat = false, hasPatchTypes = false;
|
|
225
255
|
|
|
226
256
|
for (const commit of commits) {
|
|
@@ -228,26 +258,42 @@ function analyzeCommitsForVersioning(commits) {
|
|
|
228
258
|
if (!parsed) continue;
|
|
229
259
|
|
|
230
260
|
const { type, breaking } = parsed;
|
|
231
|
-
const isBreaking =
|
|
232
|
-
|
|
233
|
-
if (isBreaking) hasBreaking = true;
|
|
234
|
-
if (type === 'feat') hasFeat = true;
|
|
235
|
-
else if (PATCH_TYPES.includes(type)) hasPatchTypes = true;
|
|
261
|
+
const isBreaking = isBreakingChange(commit, parsed);
|
|
236
262
|
|
|
237
|
-
if (
|
|
238
|
-
|
|
263
|
+
if (isBreaking) {
|
|
264
|
+
hasBreaking = true;
|
|
265
|
+
breakingCommits.push(commit);
|
|
266
|
+
} else {
|
|
267
|
+
// Only add to type sections if not breaking
|
|
268
|
+
if (type === 'feat') hasFeat = true;
|
|
269
|
+
else if (PATCH_TYPES.includes(type)) hasPatchTypes = true;
|
|
270
|
+
|
|
271
|
+
if (commitsByType[type]) {
|
|
272
|
+
commitsByType[type].push(commit);
|
|
273
|
+
}
|
|
239
274
|
}
|
|
240
275
|
}
|
|
241
276
|
|
|
242
|
-
return { hasBreaking, hasFeat, hasPatchTypes, commitsByType };
|
|
277
|
+
return { hasBreaking, hasFeat, hasPatchTypes, commitsByType, breakingCommits };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Capitalizes the first letter of a string.
|
|
282
|
+
* @param {string} str - The string to capitalize
|
|
283
|
+
* @returns {string} Capitalized string
|
|
284
|
+
*/
|
|
285
|
+
function capitalize(str) {
|
|
286
|
+
if (!str) return str;
|
|
287
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
243
288
|
}
|
|
244
289
|
|
|
245
290
|
/**
|
|
246
291
|
* Generates changelog entries for a commit type section.
|
|
247
292
|
* @param {Array} commits - Commits of this type
|
|
293
|
+
* @param {boolean} isBreakingSection - Whether this is for the breaking changes section
|
|
248
294
|
* @returns {Array<string>} Changelog lines
|
|
249
295
|
*/
|
|
250
|
-
function generateTypeChangelog(commits) {
|
|
296
|
+
function generateTypeChangelog(commits, isBreakingSection = false) {
|
|
251
297
|
const byScope = {};
|
|
252
298
|
const noScope = [];
|
|
253
299
|
|
|
@@ -258,7 +304,7 @@ function generateTypeChangelog(commits) {
|
|
|
258
304
|
const subject = commit.message.split('\n')[0].trim();
|
|
259
305
|
const entry = {
|
|
260
306
|
scope: parsed.scope,
|
|
261
|
-
description: formatChangelogDescription(subject, parsed, commit.message),
|
|
307
|
+
description: formatChangelogDescription(subject, parsed, commit.message, isBreakingSection),
|
|
262
308
|
issueRef: extractIssueReference(commit.message) || '',
|
|
263
309
|
};
|
|
264
310
|
|
|
@@ -272,21 +318,21 @@ function generateTypeChangelog(commits) {
|
|
|
272
318
|
const lines = [];
|
|
273
319
|
for (const entry of noScope) {
|
|
274
320
|
const ref = entry.issueRef ? ` ${entry.issueRef}` : '';
|
|
275
|
-
lines.push(`- ${entry.description}${ref}`);
|
|
321
|
+
lines.push(`- ${capitalize(entry.description)}${ref}`);
|
|
276
322
|
}
|
|
277
323
|
for (const scope of Object.keys(byScope).sort()) {
|
|
278
324
|
for (const entry of byScope[scope]) {
|
|
279
325
|
const ref = entry.issueRef ? ` ${entry.issueRef}` : '';
|
|
280
|
-
lines.push(`-
|
|
326
|
+
lines.push(`- ${scope}: ${capitalize(entry.description)}${ref}`);
|
|
281
327
|
}
|
|
282
328
|
}
|
|
283
329
|
return lines;
|
|
284
330
|
}
|
|
285
331
|
|
|
286
332
|
/**
|
|
287
|
-
* Calculates the
|
|
333
|
+
* Calculates the new version and generates a changelog.
|
|
288
334
|
* @param {{latestVersion: string|null, commits: Array}} expandedInfo
|
|
289
|
-
* @returns {{
|
|
335
|
+
* @returns {{newVersion: string|null, changelog: string}}
|
|
290
336
|
*/
|
|
291
337
|
function calculateNextVersionAndChangelog(expandedInfo) {
|
|
292
338
|
const { latestVersion, commits } = expandedInfo;
|
|
@@ -294,15 +340,24 @@ function calculateNextVersionAndChangelog(expandedInfo) {
|
|
|
294
340
|
const analysis = analyzeCommitsForVersioning(commits);
|
|
295
341
|
|
|
296
342
|
const bump = determineVersionBumpType(analysis, current.major === 0);
|
|
297
|
-
const
|
|
343
|
+
const newVersion = applyVersionBump(current, bump);
|
|
298
344
|
|
|
299
345
|
// Generate changelog
|
|
300
346
|
const changelogLines = [];
|
|
347
|
+
|
|
348
|
+
// Add breaking changes section first if any
|
|
349
|
+
if (analysis.breakingCommits.length > 0) {
|
|
350
|
+
changelogLines.push('BREAKING CHANGES:');
|
|
351
|
+
changelogLines.push(...generateTypeChangelog(analysis.breakingCommits, true));
|
|
352
|
+
changelogLines.push('');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add regular type sections
|
|
301
356
|
for (const type of TYPE_ORDER) {
|
|
302
357
|
const typeCommits = analysis.commitsByType[type];
|
|
303
358
|
if (!typeCommits?.length) continue;
|
|
304
359
|
|
|
305
|
-
changelogLines.push(
|
|
360
|
+
changelogLines.push(TYPE_HEADERS[type] || `${capitalize(type)}:`);
|
|
306
361
|
changelogLines.push(...generateTypeChangelog(typeCommits));
|
|
307
362
|
changelogLines.push('');
|
|
308
363
|
}
|
|
@@ -311,7 +366,7 @@ function calculateNextVersionAndChangelog(expandedInfo) {
|
|
|
311
366
|
changelogLines.pop();
|
|
312
367
|
}
|
|
313
368
|
|
|
314
|
-
return {
|
|
369
|
+
return { newVersion, changelog: changelogLines.join('\n') };
|
|
315
370
|
}
|
|
316
371
|
|
|
317
372
|
/**
|
|
@@ -357,7 +412,7 @@ function getAllBranchCommits(branch) {
|
|
|
357
412
|
|
|
358
413
|
/**
|
|
359
414
|
* Processes version information for the current branch.
|
|
360
|
-
* @returns {Promise<{currentVersion: string|null,
|
|
415
|
+
* @returns {Promise<{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}>}
|
|
361
416
|
*/
|
|
362
417
|
async function processVersionInfo() {
|
|
363
418
|
ensureGitRepo();
|
|
@@ -367,11 +422,11 @@ async function processVersionInfo() {
|
|
|
367
422
|
const allCommits = getAllBranchCommits(branch);
|
|
368
423
|
const expandedInfo = expandCommitInfo(allCommits);
|
|
369
424
|
const { latestVersion, commits } = expandedInfo;
|
|
370
|
-
const {
|
|
425
|
+
const { newVersion, changelog } = calculateNextVersionAndChangelog(expandedInfo);
|
|
371
426
|
|
|
372
427
|
return {
|
|
373
428
|
currentVersion: latestVersion,
|
|
374
|
-
|
|
429
|
+
newVersion,
|
|
375
430
|
commits,
|
|
376
431
|
changelog,
|
|
377
432
|
};
|