@logickernel/agileflow 0.14.0 → 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/README.md +7 -6
- package/docs/configuration.md +8 -10
- package/docs/conventional-commits.md +14 -4
- package/package.json +10 -3
- package/src/git-push.js +8 -2
- package/src/github-push.js +12 -2
- package/src/gitlab-push.js +6 -1
- package/src/index.js +1 -5
- package/src/utils.js +46 -24
package/README.md
CHANGED
|
@@ -165,12 +165,13 @@ After 1.0.0, AgileFlow continues automatic versioning with standard semantic ver
|
|
|
165
165
|
|
|
166
166
|
AgileFlow analyzes commits since the last version tag to determine the appropriate version bump:
|
|
167
167
|
|
|
168
|
-
| Commit Type | Example | 0.x.x | 1.0.0+ |
|
|
169
|
-
|
|
170
|
-
| Breaking change | `feat!: redesign API` | **Minor** (0.1.0 → 0.2.0) | **Major** (1.0.0 → 2.0.0) |
|
|
171
|
-
| Feature | `feat: add login` | **Minor** | **Minor** |
|
|
172
|
-
| Fix | `fix: resolve crash` | **Patch** | **Patch** |
|
|
173
|
-
|
|
|
168
|
+
| Commit Type | Example | Changelog | 0.x.x | 1.0.0+ |
|
|
169
|
+
|-------------|---------|-----------|-------|--------|
|
|
170
|
+
| Breaking change | `feat!: redesign API` | Add entry | **Minor** (0.1.0 → 0.2.0) | **Major** (1.0.0 → 2.0.0) |
|
|
171
|
+
| Feature | `feat: add login` | Add entry | **Minor** | **Minor** |
|
|
172
|
+
| Fix | `fix: resolve crash` | Add entry | **Patch** | **Patch** |
|
|
173
|
+
| Chore | `chore: update dependencies` | **No** entry | No bump | No bump |
|
|
174
|
+
| Everything else | `docs: update README` | Add entry | No bump | No bump |
|
|
174
175
|
|
|
175
176
|
---
|
|
176
177
|
|
package/docs/configuration.md
CHANGED
|
@@ -74,23 +74,21 @@ jobs:
|
|
|
74
74
|
- name: Create version tag
|
|
75
75
|
env:
|
|
76
76
|
AGILEFLOW_TOKEN: ${{ secrets.AGILEFLOW_TOKEN }}
|
|
77
|
-
run:
|
|
77
|
+
run: agileflow github
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### GitLab CI
|
|
81
81
|
|
|
82
82
|
```yaml
|
|
83
83
|
agileflow:
|
|
84
|
-
|
|
85
|
-
image: node:20-alpine
|
|
84
|
+
image: node:20
|
|
86
85
|
script:
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- main
|
|
86
|
+
- npm install -g @logickernel/agileflow
|
|
87
|
+
- agileflow gitlab
|
|
88
|
+
rules:
|
|
89
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
90
|
+
tags:
|
|
91
|
+
- agileflow
|
|
94
92
|
```
|
|
95
93
|
|
|
96
94
|
---
|
|
@@ -39,10 +39,10 @@ flowchart TD
|
|
|
39
39
|
A -- "no" --> B{Does it fix functionality?}
|
|
40
40
|
|
|
41
41
|
B -- "yes" --> X[fix:]
|
|
42
|
-
B -- "no" --> C{Is it
|
|
42
|
+
B -- "no" --> C{Is it worth an entry in the changelog?}
|
|
43
43
|
|
|
44
|
-
C -- "yes" --> W[
|
|
45
|
-
C -- "no" --> D[
|
|
44
|
+
C -- "yes" --> W[Choose best: docs, ci, style, etc.]
|
|
45
|
+
C -- "no" --> D[chore:]
|
|
46
46
|
|
|
47
47
|
W --> E{Is it a breaking change?}
|
|
48
48
|
F --> E
|
|
@@ -221,7 +221,17 @@ feat: add user authentication
|
|
|
221
221
|
feat: added user authentication
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
5.
|
|
224
|
+
5. Use Chore for *work in progress*
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# ✅ Use chore so an entry is not added to the changelog
|
|
228
|
+
chore: add framework for form validation
|
|
229
|
+
|
|
230
|
+
# ❌ Used something else that will add a meaningless entry to the changelog
|
|
231
|
+
refactor: add framework for form validation
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
6. Add Meaningful Scopes when applicable
|
|
225
235
|
|
|
226
236
|
```bash
|
|
227
237
|
# ✅ Helpful scope
|
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",
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
"versioning",
|
|
25
28
|
"git",
|
|
26
29
|
"ci-cd",
|
|
30
|
+
"github",
|
|
27
31
|
"gitlab",
|
|
28
32
|
"automation",
|
|
29
33
|
"release-management"
|
|
@@ -36,5 +40,8 @@
|
|
|
36
40
|
"url": "git@code.logickernel.com:kernel/agileflow.git"
|
|
37
41
|
},
|
|
38
42
|
"author": "Víctor Valle <victor.valle@logickernel.com>",
|
|
39
|
-
"license": "ISC"
|
|
43
|
+
"license": "ISC",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"jest": "^30.2.0"
|
|
46
|
+
}
|
|
40
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');
|
|
@@ -10,13 +11,14 @@ const os = require('os');
|
|
|
10
11
|
* Uses native git commands - requires git credentials to be configured.
|
|
11
12
|
* @param {string} tagName - The tag name (e.g., "v1.2.3")
|
|
12
13
|
* @param {string} message - The tag message (changelog)
|
|
14
|
+
* @param {boolean} quiet - If true, suppress success message
|
|
13
15
|
* @returns {Promise<void>}
|
|
14
16
|
*/
|
|
15
|
-
async function pushTag(tagName, message) {
|
|
17
|
+
async function pushTag(tagName, message, quiet = false) {
|
|
16
18
|
const safeTag = String(tagName).replace(/"/g, '\\"');
|
|
17
19
|
|
|
18
20
|
// Write message to a temp file to avoid shell escaping issues with special characters
|
|
19
|
-
const tempFile = path.join(os.tmpdir(), `agileflow-tag-${
|
|
21
|
+
const tempFile = path.join(os.tmpdir(), `agileflow-tag-${crypto.randomBytes(8).toString('hex')}.txt`);
|
|
20
22
|
try {
|
|
21
23
|
fs.writeFileSync(tempFile, message, 'utf8');
|
|
22
24
|
|
|
@@ -25,6 +27,10 @@ async function pushTag(tagName, message) {
|
|
|
25
27
|
|
|
26
28
|
// Push to origin
|
|
27
29
|
execSync(`git push origin "${safeTag}"`, { stdio: 'pipe' });
|
|
30
|
+
|
|
31
|
+
if (!quiet) {
|
|
32
|
+
console.log(`Tag ${tagName} created and pushed successfully.`);
|
|
33
|
+
}
|
|
28
34
|
} finally {
|
|
29
35
|
// Clean up temp file
|
|
30
36
|
try {
|
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
|
|
|
@@ -122,9 +127,10 @@ function makeRequest({ method, path, accessToken, body }) {
|
|
|
122
127
|
* Uses GITHUB_REPOSITORY and GITHUB_SHA from GitHub Actions environment.
|
|
123
128
|
* @param {string} tagName - The tag name
|
|
124
129
|
* @param {string} message - The tag message
|
|
130
|
+
* @param {boolean} quiet - If true, suppress success message
|
|
125
131
|
* @returns {Promise<void>}
|
|
126
132
|
*/
|
|
127
|
-
async function pushTag(tagName, message) {
|
|
133
|
+
async function pushTag(tagName, message, quiet = false) {
|
|
128
134
|
const accessToken = process.env.AGILEFLOW_TOKEN;
|
|
129
135
|
const repository = process.env.GITHUB_REPOSITORY;
|
|
130
136
|
const commitSha = process.env.GITHUB_SHA;
|
|
@@ -148,6 +154,10 @@ async function pushTag(tagName, message) {
|
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
await createTagViaAPI(tagName, message || tagName, repository, accessToken, commitSha);
|
|
157
|
+
|
|
158
|
+
if (!quiet) {
|
|
159
|
+
console.log(`Tag ${tagName} created and pushed successfully.`);
|
|
160
|
+
}
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
module.exports = {
|
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/index.js
CHANGED
|
@@ -125,11 +125,7 @@ async function handlePushCommand(pushType, options) {
|
|
|
125
125
|
console.log(`\nCreating tag ${info.newVersion}...`);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
await pushModule.pushTag(info.newVersion, tagMessage);
|
|
129
|
-
|
|
130
|
-
if (!options.quiet) {
|
|
131
|
-
console.log(`Tag ${info.newVersion} created and pushed successfully.`);
|
|
132
|
-
}
|
|
128
|
+
await pushModule.pushTag(info.newVersion, tagMessage, options.quiet);
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
async function main() {
|
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);
|