@logickernel/agileflow 0.14.1 → 0.15.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 +9 -3
- package/src/git-push.js +2 -1
- package/src/github-push.js +6 -1
- package/src/gitlab-push.js +6 -1
- package/src/utils.js +46 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logickernel/agileflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Automatic semantic versioning and changelog generation based on conventional commits",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "
|
|
17
|
+
"test": "jest",
|
|
18
18
|
"prepack": "chmod +x bin/agileflow"
|
|
19
19
|
},
|
|
20
|
+
"jest": {
|
|
21
|
+
"testEnvironment": "node"
|
|
22
|
+
},
|
|
20
23
|
"keywords": [
|
|
21
24
|
"semantic-versioning",
|
|
22
25
|
"conventional-commits",
|
|
@@ -37,5 +40,8 @@
|
|
|
37
40
|
"url": "git@code.logickernel.com:kernel/agileflow.git"
|
|
38
41
|
},
|
|
39
42
|
"author": "Víctor Valle <victor.valle@logickernel.com>",
|
|
40
|
-
"license": "ISC"
|
|
43
|
+
"license": "ISC",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"jest": "^30.2.0"
|
|
46
|
+
}
|
|
41
47
|
}
|
package/src/git-push.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const os = require('os');
|
|
@@ -17,7 +18,7 @@ async function pushTag(tagName, message, quiet = false) {
|
|
|
17
18
|
const safeTag = String(tagName).replace(/"/g, '\\"');
|
|
18
19
|
|
|
19
20
|
// Write message to a temp file to avoid shell escaping issues with special characters
|
|
20
|
-
const tempFile = path.join(os.tmpdir(), `agileflow-tag-${
|
|
21
|
+
const tempFile = path.join(os.tmpdir(), `agileflow-tag-${crypto.randomBytes(8).toString('hex')}.txt`);
|
|
21
22
|
try {
|
|
22
23
|
fs.writeFileSync(tempFile, message, 'utf8');
|
|
23
24
|
|
package/src/github-push.js
CHANGED
|
@@ -64,10 +64,15 @@ function makeRequest({ method, path, accessToken, body }) {
|
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MB
|
|
67
68
|
const req = https.request(options, (res) => {
|
|
68
69
|
let data = '';
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
res.on('data', (chunk) => {
|
|
72
|
+
if (data.length + chunk.length > MAX_RESPONSE_BYTES) {
|
|
73
|
+
req.destroy(new Error('GitHub API response exceeded size limit'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
71
76
|
data += chunk;
|
|
72
77
|
});
|
|
73
78
|
|
package/src/gitlab-push.js
CHANGED
|
@@ -34,10 +34,15 @@ function createTagViaAPI(tagName, message, projectPath, serverHost, accessToken,
|
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MB
|
|
37
38
|
const req = https.request(options, (res) => {
|
|
38
39
|
let data = '';
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
res.on('data', (chunk) => {
|
|
42
|
+
if (data.length + chunk.length > MAX_RESPONSE_BYTES) {
|
|
43
|
+
req.destroy(new Error('GitLab API response exceeded size limit'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
41
46
|
data += chunk;
|
|
42
47
|
});
|
|
43
48
|
|
package/src/utils.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
-
const fs = require('fs');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Executes a shell command and returns the output.
|
|
@@ -44,8 +43,10 @@ function run(command, options = {}) {
|
|
|
44
43
|
* @throws {Error} If the current directory is not a git repository
|
|
45
44
|
*/
|
|
46
45
|
function ensureGitRepo() {
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
try {
|
|
47
|
+
runWithOutput('git rev-parse --is-inside-work-tree');
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error('Current directory is not a git repository.');
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -105,16 +106,28 @@ function fetchTags() {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* @returns {
|
|
109
|
+
* Builds a map of commit SHA → tag names for all tags in the repository.
|
|
110
|
+
* Uses a single git call instead of one per commit.
|
|
111
|
+
* @returns {Map<string, string[]>}
|
|
111
112
|
*/
|
|
112
|
-
function
|
|
113
|
+
function buildTagMap() {
|
|
113
114
|
try {
|
|
114
|
-
const output = runWithOutput(
|
|
115
|
-
|
|
115
|
+
const output = runWithOutput('git tag --format=%(refname:short)|%(*objectname)|%(objectname)').trim();
|
|
116
|
+
if (!output) return new Map();
|
|
117
|
+
const map = new Map();
|
|
118
|
+
for (const line of output.split('\n')) {
|
|
119
|
+
const [name, deref, obj] = line.split('|');
|
|
120
|
+
// Annotated tags dereference to the commit via %(*objectname);
|
|
121
|
+
// lightweight tags point directly via %(objectname).
|
|
122
|
+
const sha = (deref || obj || '').trim();
|
|
123
|
+
const tagName = (name || '').trim();
|
|
124
|
+
if (!sha || !tagName) continue;
|
|
125
|
+
if (!map.has(sha)) map.set(sha, []);
|
|
126
|
+
map.get(sha).push(tagName);
|
|
127
|
+
}
|
|
128
|
+
return map;
|
|
116
129
|
} catch {
|
|
117
|
-
return
|
|
130
|
+
return new Map();
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
@@ -126,12 +139,12 @@ function getTagsForCommit(commitSha) {
|
|
|
126
139
|
function parseConventionalCommit(message) {
|
|
127
140
|
if (!message) return null;
|
|
128
141
|
const subject = message.split('\n')[0].trim();
|
|
129
|
-
const match = subject.match(/^(\w+)(
|
|
142
|
+
const match = subject.match(/^(\w+)(?:\(([^)]+)\))?(!)?:\s+(.+)$/);
|
|
130
143
|
if (!match) return null;
|
|
131
144
|
return {
|
|
132
145
|
type: match[1].toLowerCase(),
|
|
133
|
-
breaking: Boolean(match[
|
|
134
|
-
scope: match[
|
|
146
|
+
breaking: Boolean(match[3]),
|
|
147
|
+
scope: match[2] ? String(match[2]).trim() : '',
|
|
135
148
|
description: String(match[4]).trim(),
|
|
136
149
|
};
|
|
137
150
|
}
|
|
@@ -154,7 +167,15 @@ function expandCommitInfo(commits) {
|
|
|
154
167
|
return { latestVersion: null, commits };
|
|
155
168
|
}
|
|
156
169
|
|
|
157
|
-
const latestVersion = commits[taggedIndex].tags
|
|
170
|
+
const latestVersion = commits[taggedIndex].tags
|
|
171
|
+
.filter(tag => SEMVER_PATTERN.test(tag))
|
|
172
|
+
.sort((a, b) => {
|
|
173
|
+
const pa = parseVersion(a);
|
|
174
|
+
const pb = parseVersion(b);
|
|
175
|
+
if (pb.major !== pa.major) return pb.major - pa.major;
|
|
176
|
+
if (pb.minor !== pa.minor) return pb.minor - pa.minor;
|
|
177
|
+
return pb.patch - pa.patch;
|
|
178
|
+
})[0];
|
|
158
179
|
// Exclude the tagged commit itself - only return commits since the tag
|
|
159
180
|
return {
|
|
160
181
|
latestVersion,
|
|
@@ -382,28 +403,29 @@ function calculateNextVersionAndChangelog(expandedInfo) {
|
|
|
382
403
|
* @returns {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>}
|
|
383
404
|
*/
|
|
384
405
|
function getAllBranchCommits(branch) {
|
|
385
|
-
//
|
|
386
|
-
|
|
406
|
+
// Resolve the branch to a SHA to avoid shell injection when the branch
|
|
407
|
+
// name originates from a CI environment variable.
|
|
408
|
+
let resolvedSha;
|
|
387
409
|
try {
|
|
388
|
-
runWithOutput(`git rev-parse --verify ${branch}`);
|
|
410
|
+
resolvedSha = runWithOutput(`git rev-parse --verify -- ${branch}`).trim();
|
|
389
411
|
} catch {
|
|
390
412
|
// Try with origin/ prefix (common in CI environments where local branch doesn't exist)
|
|
391
413
|
try {
|
|
392
|
-
runWithOutput(`git rev-parse --verify origin/${branch}`);
|
|
393
|
-
branchRef = `origin/${branch}`;
|
|
414
|
+
resolvedSha = runWithOutput(`git rev-parse --verify -- origin/${branch}`).trim();
|
|
394
415
|
} catch {
|
|
395
416
|
return [];
|
|
396
417
|
}
|
|
397
418
|
}
|
|
398
|
-
|
|
419
|
+
|
|
420
|
+
const tagMap = buildTagMap();
|
|
399
421
|
const RS = '\x1E';
|
|
400
422
|
const COMMIT_SEP = `${RS}${RS}`;
|
|
401
|
-
|
|
423
|
+
|
|
402
424
|
try {
|
|
403
|
-
const logCmd = `git log --format=%H${RS}%ai${RS}%an${RS}%B${COMMIT_SEP} ${
|
|
425
|
+
const logCmd = `git log --format=%H${RS}%ai${RS}%an${RS}%B${COMMIT_SEP} ${resolvedSha}`;
|
|
404
426
|
const output = runWithOutput(logCmd).trim();
|
|
405
427
|
if (!output) return [];
|
|
406
|
-
|
|
428
|
+
|
|
407
429
|
return output
|
|
408
430
|
.split(COMMIT_SEP)
|
|
409
431
|
.filter(block => block.trim())
|
|
@@ -416,7 +438,7 @@ function getAllBranchCommits(branch) {
|
|
|
416
438
|
datetime: parts[1].trim(),
|
|
417
439
|
author: parts[2].trim(),
|
|
418
440
|
message: parts.slice(3).join(RS).trim(),
|
|
419
|
-
tags:
|
|
441
|
+
tags: tagMap.get(hash) || [],
|
|
420
442
|
};
|
|
421
443
|
})
|
|
422
444
|
.filter(Boolean);
|