@logickernel/agileflow 0.17.0 → 0.20.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.
@@ -0,0 +1,124 @@
1
+ # CLI Reference
2
+
3
+ ## Installation
4
+
5
+ Run without installing:
6
+ ```bash
7
+ npx @logickernel/agileflow
8
+ ```
9
+
10
+ Or install globally:
11
+ ```bash
12
+ npm install -g @logickernel/agileflow
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Commands
18
+
19
+ ### `agileflow` (no command)
20
+
21
+ Analyzes the repository and prints the current version, next version, commits, and changelog. Does not create or modify anything.
22
+
23
+ ```bash
24
+ agileflow
25
+ ```
26
+
27
+ ```
28
+ Commits since current version (3):
29
+ a1b2c3d feat: add dark mode
30
+ d4e5f6a fix: resolve login timeout
31
+ 7g8h9i0 docs: update README
32
+
33
+ Current version: v1.4.2
34
+ New version: v1.5.0
35
+
36
+ Changelog:
37
+
38
+ ### Features
39
+ - add dark mode
40
+
41
+ ### Bug fixes
42
+ - resolve login timeout
43
+ ```
44
+
45
+ If no bump is needed, `New version` shows `no bump needed` and no changelog is printed.
46
+
47
+ ---
48
+
49
+ ### `agileflow push [remote]`
50
+
51
+ Creates an annotated git tag and pushes it to the specified remote. Uses standard git commands — requires git credentials to be configured.
52
+
53
+ ```bash
54
+ agileflow push # pushes to origin
55
+ agileflow push upstream # pushes to a different remote
56
+ ```
57
+
58
+ If no bump is needed, exits without creating a tag.
59
+
60
+ ---
61
+
62
+ ### `agileflow gitlab`
63
+
64
+ Creates a version tag via the GitLab API. Designed for GitLab CI pipelines.
65
+
66
+ ```bash
67
+ agileflow gitlab
68
+ ```
69
+
70
+ **Required environment variable:**
71
+ - `AGILEFLOW_TOKEN` — GitLab access token with `api` scope and `Maintainer` role
72
+
73
+ **Provided automatically by GitLab CI:**
74
+ - `CI_SERVER_HOST` — GitLab server hostname
75
+ - `CI_PROJECT_PATH` — Project path (e.g., `group/project`)
76
+ - `CI_COMMIT_SHA` — Commit to tag
77
+
78
+ ---
79
+
80
+ ### `agileflow github`
81
+
82
+ Creates a version tag via the GitHub API. Designed for GitHub Actions workflows.
83
+
84
+ ```bash
85
+ agileflow github
86
+ ```
87
+
88
+ **Required environment variable:**
89
+ - `AGILEFLOW_TOKEN` — GitHub Personal Access Token with `Contents: Read and write` permission
90
+
91
+ **Provided automatically by GitHub Actions:**
92
+ - `GITHUB_REPOSITORY` — Repository (e.g., `owner/repo`)
93
+ - `GITHUB_SHA` — Commit to tag
94
+
95
+ ---
96
+
97
+ ### `agileflow version`
98
+
99
+ Prints the AgileFlow tool version.
100
+
101
+ ```bash
102
+ agileflow version
103
+ # 0.17.0
104
+ ```
105
+
106
+ ---
107
+
108
+ ### `agileflow --help`
109
+
110
+ Prints usage information.
111
+
112
+ ```bash
113
+ agileflow --help
114
+ agileflow -h
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Exit codes
120
+
121
+ | Code | Meaning |
122
+ |------|---------|
123
+ | `0` | Success (including "no bump needed") |
124
+ | `1` | Error (authentication failure, git error, API error, unknown command) |
@@ -0,0 +1,133 @@
1
+ # Conventional Commits
2
+
3
+ AgileFlow uses [Conventional Commits](https://www.conventionalcommits.org/) to determine the next semantic version and generate changelogs.
4
+
5
+ ## Format
6
+
7
+ ```
8
+ <type>[optional scope]: <description>
9
+
10
+ [optional body]
11
+
12
+ [optional footer(s)]
13
+ ```
14
+
15
+ Examples:
16
+ ```
17
+ feat: add user authentication
18
+ fix(api): handle timeout errors
19
+ feat!: remove deprecated endpoints
20
+ docs: update README
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Version impact
26
+
27
+ | Commit | Example | Before v1.0.0 | v1.0.0 and after |
28
+ |--------|---------|---------------|------------------|
29
+ | Breaking change | `feat!: redesign API` | minor bump | major bump |
30
+ | `feat` | `feat: add login` | minor bump | minor bump |
31
+ | `fix` | `fix: resolve crash` | patch bump | patch bump |
32
+ | Everything else | `docs: update README` | no bump | no bump |
33
+
34
+ When multiple commits exist since the last tag, the highest-priority bump wins.
35
+
36
+ ### Marking breaking changes
37
+
38
+ ```bash
39
+ # Using ! after the type
40
+ feat!: remove deprecated API endpoints
41
+
42
+ # Using a BREAKING CHANGE footer
43
+ feat: change response format
44
+
45
+ BREAKING CHANGE: Response now uses camelCase instead of snake_case
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Commit types
51
+
52
+ | Type | Use for | Changelog |
53
+ |------|---------|-----------|
54
+ | `feat` | New functionality | Yes — Features |
55
+ | `fix` | Bug fixes | Yes — Bug fixes |
56
+ | `perf` | Performance improvements | Yes — Performance improvements |
57
+ | `refactor` | Code restructuring (no behavior change) | Yes — Other changes |
58
+ | `docs` | Documentation only | Yes — Documentation |
59
+ | `ci` | CI/CD configuration | Yes — Other changes |
60
+ | `test` | Tests only | No |
61
+ | `style` | Formatting, whitespace | No |
62
+ | `chore` | Maintenance tasks, work in progress | No |
63
+ | `build` | Build system changes | No |
64
+ | `revert` | Revert a previous commit | No |
65
+
66
+ Types not in this table appear under "Other changes" in the changelog.
67
+
68
+ ---
69
+
70
+ ## Choosing the right type
71
+
72
+ 1. **Adds new functionality users can use?** → `feat`
73
+ 2. **Fixes broken functionality?** → `fix`
74
+ 3. **Work in progress or maintenance with no user impact?** → `chore` (excluded from changelog)
75
+ 4. **Performance improvement?** → `perf`
76
+ 5. **Refactoring internal code?** → `refactor`
77
+ 6. **Breaking any existing behavior?** → add `!` after the type (e.g., `feat!:`, `fix!:`)
78
+
79
+ ---
80
+
81
+ ## Best practices
82
+
83
+ Use present tense: `feat: add login` not `feat: added login`
84
+
85
+ Be specific: `fix: prevent timeout on uploads larger than 100MB` not `fix: fix timeout`
86
+
87
+ Use `chore:` for work in progress so it doesn't add noise to the changelog:
88
+ ```bash
89
+ # ✅ Won't appear in changelog
90
+ chore: scaffold form validation module
91
+
92
+ # ❌ Will appear as "Other changes" — misleading
93
+ refactor: scaffold form validation module
94
+ ```
95
+
96
+ Use scopes to clarify context when helpful:
97
+ ```bash
98
+ feat(auth): add OAuth2 support
99
+ fix(api): handle empty response body
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Changelog format
105
+
106
+ AgileFlow groups commits by type:
107
+
108
+ ```
109
+ v1.5.0
110
+
111
+ ### Features
112
+ - add dark mode
113
+ - add keyboard shortcuts
114
+
115
+ ### Bug fixes
116
+ - resolve login timeout
117
+ - fix pagination on mobile
118
+
119
+ ### Performance improvements
120
+ - optimize image loading
121
+ ```
122
+
123
+ Breaking changes are highlighted:
124
+
125
+ ```
126
+ v2.0.0
127
+
128
+ ### Features
129
+ - BREAKING: remove deprecated v1 API endpoints
130
+ - add new v2 API
131
+ ```
132
+
133
+ Non-conventional commits (no `type:` prefix) appear under "Other changes" and trigger no version bump.
@@ -0,0 +1,83 @@
1
+ # Getting Started
2
+
3
+ ## Run it now
4
+
5
+ No installation needed. Run AgileFlow in any git repository:
6
+
7
+ ```bash
8
+ npx @logickernel/agileflow
9
+ ```
10
+
11
+ This is a read-only preview — it never creates tags or modifies anything.
12
+
13
+ ### Understanding the output
14
+
15
+ ```
16
+ Commits since current version (3):
17
+ a1b2c3d feat: add dark mode
18
+ d4e5f6a fix: resolve login timeout
19
+ 7g8h9i0 docs: update README
20
+
21
+ Current version: v1.4.2
22
+ New version: v1.5.0
23
+
24
+ Changelog:
25
+
26
+ ### Features
27
+ - add dark mode
28
+
29
+ ### Bug fixes
30
+ - resolve login timeout
31
+ ```
32
+
33
+ - **Current version** — the highest semver tag found in the commit history
34
+ - **New version** — calculated from the commit types since that tag
35
+ - **Changelog** — grouped by commit type; `docs`, `chore`, and `style` commits are omitted from the changelog but still analyzed for versioning
36
+
37
+ If all commits since the last tag are `docs`, `chore`, `style`, or other non-bumping types, AgileFlow prints `no bump needed` and skips tag creation.
38
+
39
+ ### Starting from scratch
40
+
41
+ No version tags yet? AgileFlow starts from `v0.0.0`:
42
+
43
+ ```bash
44
+ git commit -m "feat: initial project setup"
45
+ npx @logickernel/agileflow
46
+ # New version: v0.1.0
47
+ ```
48
+
49
+ ### Creating v1.0.0
50
+
51
+ `v1.0.0` signals your first stable release. Create it manually when you're ready:
52
+
53
+ ```bash
54
+ git tag -a v1.0.0 -m "First stable release"
55
+ git push origin v1.0.0
56
+ ```
57
+
58
+ After `v1.0.0`, breaking changes bump the major version (e.g., `v2.0.0`). Before it, they bump minor (e.g., `v0.2.0`).
59
+
60
+ ---
61
+
62
+ ## Set up CI/CD
63
+
64
+ Pick your platform:
65
+
66
+ - [GitHub Actions](../guides/github-actions.md)
67
+ - [GitLab CI](../guides/gitlab-ci.md)
68
+ - [Other CI/CD](../guides/other-ci.md)
69
+
70
+ ---
71
+
72
+ ## How version bumps are calculated
73
+
74
+ AgileFlow picks the highest-priority bump across all commits since the last tag:
75
+
76
+ | Commit | Example | Before v1.0.0 | v1.0.0 and after |
77
+ |--------|---------|---------------|------------------|
78
+ | Breaking change | `feat!: redesign API` | minor bump | major bump |
79
+ | New feature | `feat: add login` | minor bump | minor bump |
80
+ | Bug fix | `fix: resolve crash` | patch bump | patch bump |
81
+ | Everything else | `docs: update README` | no bump | no bump |
82
+
83
+ See [Conventional Commits](../reference/conventional-commits.md) for the full format reference.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logickernel/agileflow",
3
- "version": "0.17.0",
3
+ "version": "0.20.0",
4
4
  "description": "Automatic semantic versioning and changelog generation based on conventional commits",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/git-push.js CHANGED
@@ -14,7 +14,7 @@ const os = require('os');
14
14
  * @param {boolean} quiet - If true, suppress success message
15
15
  * @returns {Promise<void>}
16
16
  */
17
- async function pushTag(tagName, message, quiet = false, remote = 'origin') {
17
+ async function pushTag(tagName, message, remote = 'origin') {
18
18
  const safeTag = String(tagName).replace(/"/g, '\\"');
19
19
  const safeRemote = String(remote).replace(/"/g, '\\"');
20
20
 
@@ -29,9 +29,7 @@ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
29
29
  // Push to remote
30
30
  execSync(`git push "${safeRemote}" "${safeTag}"`, { stdio: 'pipe' });
31
31
 
32
- if (!quiet) {
33
- console.log(`Tag ${tagName} created and pushed successfully.`);
34
- }
32
+ console.log(`Tag ${tagName} created and pushed successfully.`);
35
33
  } finally {
36
34
  // Clean up temp file
37
35
  try {
@@ -130,7 +130,7 @@ function makeRequest({ method, path, accessToken, body }) {
130
130
  * @param {boolean} quiet - If true, suppress success message
131
131
  * @returns {Promise<void>}
132
132
  */
133
- async function pushTag(tagName, message, quiet = false, remote = 'origin') {
133
+ async function pushTag(tagName, message, remote = 'origin') {
134
134
  const accessToken = process.env.AGILEFLOW_TOKEN;
135
135
  const repository = process.env.GITHUB_REPOSITORY;
136
136
  const commitSha = process.env.GITHUB_SHA;
@@ -155,9 +155,7 @@ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
155
155
 
156
156
  await createTagViaAPI(tagName, message || tagName, repository, accessToken, commitSha);
157
157
 
158
- if (!quiet) {
159
- console.log(`Tag ${tagName} created and pushed successfully.`);
160
- }
158
+ console.log(`Tag ${tagName} created and pushed successfully.`);
161
159
  }
162
160
 
163
161
  module.exports = {
@@ -96,7 +96,7 @@ function createTagViaAPI(tagName, message, projectPath, serverHost, accessToken,
96
96
  * @param {string} message - The tag message
97
97
  * @returns {Promise<void>}
98
98
  */
99
- async function pushTag(tagName, message, quiet = false, remote = 'origin') {
99
+ async function pushTag(tagName, message, remote = 'origin') {
100
100
  const accessToken = process.env.AGILEFLOW_TOKEN;
101
101
  const serverHost = process.env.CI_SERVER_HOST;
102
102
  const projectPath = process.env.CI_PROJECT_PATH;
@@ -134,10 +134,8 @@ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
134
134
 
135
135
  await createTagViaAPI(tagName, message || tagName, projectPath, serverHost, accessToken, commitSha);
136
136
 
137
- if (!quiet) {
138
- const commitUrl = `https://${process.env.CI_SERVER_HOST}/${process.env.CI_PROJECT_PATH}/-/commit/${process.env.CI_COMMIT_SHA}/pipelines`;
139
- console.log(`Tag ${tagName} created and pushed successfully.\nView the build pipelines at: ${commitUrl}`);
140
- }
137
+ const commitUrl = `https://${process.env.CI_SERVER_HOST}/${process.env.CI_PROJECT_PATH}/-/commit/${process.env.CI_COMMIT_SHA}/pipelines`;
138
+ console.log(`Tag ${tagName} created and pushed successfully.\nView the build pipelines at: ${commitUrl}`);
141
139
  }
142
140
 
143
141
  module.exports = {
package/src/index.js CHANGED
@@ -18,9 +18,7 @@ Commands:
18
18
  version Print the agileflow tool version
19
19
 
20
20
  Options:
21
- --quiet Only output the next version (or empty if no bump)
22
21
  -h, --help Show this help message
23
- -v, --version Show version number
24
22
 
25
23
  For more information, visit: https://code.logickernel.com/tools/agileflow
26
24
  `);
@@ -29,7 +27,7 @@ For more information, visit: https://code.logickernel.com/tools/agileflow
29
27
  /**
30
28
  * Valid options that can be passed to commands.
31
29
  */
32
- const VALID_OPTIONS = ['--quiet', '--help', '-h', '--version', '-v'];
30
+ const VALID_OPTIONS = ['--help', '-h'];
33
31
 
34
32
  /**
35
33
  * Valid commands.
@@ -39,11 +37,9 @@ const VALID_COMMANDS = ['push', 'gitlab', 'github', 'version'];
39
37
  /**
40
38
  * Parses command line arguments and validates them.
41
39
  * @param {Array<string>} args - Command line arguments
42
- * @returns {{quiet: boolean}}
43
40
  * @throws {Error} If invalid options are found
44
41
  */
45
42
  function parseArgs(args) {
46
- // Check for invalid options
47
43
  for (const arg of args) {
48
44
  if (arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
49
45
  throw new Error(`Unknown option: ${arg}`);
@@ -52,28 +48,15 @@ function parseArgs(args) {
52
48
  throw new Error(`Unknown option: ${arg}`);
53
49
  }
54
50
  }
55
-
56
- return {
57
- quiet: args.includes('--quiet'),
58
- };
59
51
  }
60
52
 
61
53
  /**
62
54
  * Displays version info to the console.
63
55
  * @param {{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}} info
64
- * @param {boolean} quiet - Only output the new version
65
56
  */
66
- function displayVersionInfo(info, quiet) {
57
+ function displayVersionInfo(info) {
67
58
  const { currentVersion, newVersion, commits, changelog } = info;
68
-
69
- if (quiet) {
70
- if (newVersion) {
71
- console.log(newVersion);
72
- }
73
- return;
74
- }
75
-
76
-
59
+
77
60
  // List commits
78
61
  console.log(`Commits since current version (${commits.length}):`);
79
62
  for (const commit of commits) {
@@ -92,20 +75,17 @@ function displayVersionInfo(info, quiet) {
92
75
  /**
93
76
  * Handles a push command.
94
77
  * @param {string} pushType - 'push', 'gitlab', or 'github'
95
- * @param {{quiet: boolean}} options
78
+ * @param {string} remote
96
79
  */
97
- async function handlePushCommand(pushType, options, remote = 'origin') {
80
+ async function handlePushCommand(pushType, remote = 'origin') {
98
81
  const info = await processVersionInfo();
99
-
100
- // Display version info
101
- displayVersionInfo(info, options.quiet);
102
-
103
- // Skip push if no version bump needed
82
+
83
+ displayVersionInfo(info);
84
+
104
85
  if (!info.newVersion) {
105
86
  return;
106
87
  }
107
-
108
- // Get the appropriate push module
88
+
109
89
  let pushModule;
110
90
  switch (pushType) {
111
91
  case 'push':
@@ -118,23 +98,19 @@ async function handlePushCommand(pushType, options, remote = 'origin') {
118
98
  pushModule = require('./github-push');
119
99
  break;
120
100
  }
121
-
122
- // Create tag message from changelog
101
+
123
102
  const tagMessage = info.changelog || info.newVersion;
124
-
125
- if (!options.quiet) {
126
- console.log(`\nCreating tag ${info.newVersion}...`);
127
- }
128
-
129
- await pushModule.pushTag(info.newVersion, tagMessage, options.quiet, remote);
103
+
104
+ console.log(`\nCreating tag ${info.newVersion}...`);
105
+
106
+ await pushModule.pushTag(info.newVersion, tagMessage, remote);
130
107
  }
131
108
 
132
109
  async function main() {
133
110
  const [, , cmd, ...rest] = process.argv;
134
111
 
135
- let options;
136
112
  try {
137
- options = parseArgs(cmd ? [cmd, ...rest] : rest);
113
+ parseArgs(cmd ? [cmd, ...rest] : rest);
138
114
  } catch (err) {
139
115
  console.error(`Error: ${err.message}`);
140
116
  console.error();
@@ -150,7 +126,7 @@ async function main() {
150
126
  }
151
127
 
152
128
  // Handle version
153
- if (cmd === '-v' || cmd === '--version' || cmd === 'version') {
129
+ if (cmd === 'version') {
154
130
  console.log(version);
155
131
  process.exit(0);
156
132
  }
@@ -158,7 +134,7 @@ async function main() {
158
134
  // Handle push commands
159
135
  if (cmd === 'push' || cmd === 'gitlab' || cmd === 'github') {
160
136
  const remote = rest.find(arg => !arg.startsWith('-')) || 'origin';
161
- await handlePushCommand(cmd, options, remote);
137
+ await handlePushCommand(cmd, remote);
162
138
  return;
163
139
  }
164
140
 
@@ -181,7 +157,7 @@ async function main() {
181
157
 
182
158
  // Default: show version info
183
159
  const info = await processVersionInfo();
184
- displayVersionInfo(info, options.quiet);
160
+ displayVersionInfo(info);
185
161
  }
186
162
 
187
163
  process.on('unhandledRejection', (err) => {
package/src/utils.js CHANGED
@@ -224,9 +224,57 @@ function parseConventionalCommit(message) {
224
224
  }
225
225
 
226
226
  /**
227
- * Expands commit information by finding the latest version and filtering commits.
228
- * @param {Array} commits - Array of commit objects (newest to oldest)
229
- * @returns {{latestVersion: string|null, commits: Array}} Filtered commits since last version
227
+ * Finds the highest semver version tag in a list of commits.
228
+ * @param {Array} commits - The commits to scan
229
+ * @returns {{latestVersion: string|null, taggedCommitHash: string|null}}
230
+ */
231
+ function findLatestVersionTag(commits) {
232
+ if (!commits?.length) {
233
+ return { latestVersion: null, taggedCommitHash: null };
234
+ }
235
+
236
+ let bestHash = null;
237
+ let bestVersion = null;
238
+
239
+ for (let i = 0; i < commits.length; i++) {
240
+ const semverTags = commits[i].tags?.filter(tag => SEMVER_PATTERN.test(tag));
241
+ if (!semverTags?.length) continue;
242
+
243
+ const highest = semverTags.sort((a, b) => {
244
+ const pa = parseVersion(a);
245
+ const pb = parseVersion(b);
246
+ if (pb.major !== pa.major) return pb.major - pa.major;
247
+ if (pb.minor !== pa.minor) return pb.minor - pa.minor;
248
+ return pb.patch - pa.patch;
249
+ })[0];
250
+
251
+ if (!bestVersion) {
252
+ bestVersion = highest;
253
+ bestHash = commits[i].hash;
254
+ } else {
255
+ const pBest = parseVersion(bestVersion);
256
+ const pCand = parseVersion(highest);
257
+ const isHigher =
258
+ pCand.major > pBest.major ||
259
+ (pCand.major === pBest.major && pCand.minor > pBest.minor) ||
260
+ (pCand.major === pBest.major && pCand.minor === pBest.minor && pCand.patch > pBest.patch);
261
+ if (isHigher) {
262
+ bestVersion = highest;
263
+ bestHash = commits[i].hash;
264
+ }
265
+ }
266
+ }
267
+
268
+ return { latestVersion: bestVersion, taggedCommitHash: bestHash };
269
+ }
270
+
271
+ /**
272
+ * Expands commit info by finding the latest version tag and returning
273
+ * only commits since that tag. Uses positional slicing from a date-sorted
274
+ * commit list — for accurate results with merge commits, prefer using
275
+ * findLatestVersionTag + getCommitsSinceTag (as processVersionInfo does).
276
+ * @param {Array} commits - All commits (date-sorted, newest first)
277
+ * @returns {{latestVersion: string|null, commits: Array}}
230
278
  */
231
279
  function expandCommitInfo(commits) {
232
280
  if (!commits?.length) {
@@ -548,6 +596,45 @@ function getAllBranchCommits(branch) {
548
596
  }
549
597
  }
550
598
 
599
+ /**
600
+ * Retrieves commits reachable from the branch but not from the given tag,
601
+ * using git's native range selection so that date ordering cannot cause
602
+ * commits from merged branches to be missed.
603
+ * @param {string} tagHash - The commit hash of the version tag
604
+ * @param {string} branchSha - The resolved SHA of the branch tip
605
+ * @returns {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>}
606
+ */
607
+ function getCommitsSinceTag(tagHash, branchSha) {
608
+ const tagMap = buildTagMap();
609
+ const RS = '\x1E';
610
+ const COMMIT_SEP = `${RS}${RS}`;
611
+
612
+ try {
613
+ const logCmd = `git log --format=%H${RS}%ai${RS}%an${RS}%B${COMMIT_SEP} ${tagHash}..${branchSha}`;
614
+ const output = runWithOutput(logCmd).trim();
615
+ if (!output) return [];
616
+
617
+ return output
618
+ .split(COMMIT_SEP)
619
+ .filter(block => block.trim())
620
+ .map(block => {
621
+ const parts = block.split(RS);
622
+ if (parts.length < 4) return null;
623
+ const hash = parts[0].trim();
624
+ return {
625
+ hash,
626
+ datetime: parts[1].trim(),
627
+ author: parts[2].trim(),
628
+ message: parts.slice(3).join(RS).trim(),
629
+ tags: tagMap.get(hash) || [],
630
+ };
631
+ })
632
+ .filter(Boolean);
633
+ } catch {
634
+ return [];
635
+ }
636
+ }
637
+
551
638
  /**
552
639
  * Processes version information for the current branch.
553
640
  * @returns {Promise<{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}>}
@@ -556,12 +643,25 @@ async function processVersionInfo() {
556
643
  ensureGitRepo();
557
644
  const branch = getCurrentBranch();
558
645
  fetchTags();
559
-
646
+
560
647
  const allCommits = getAllBranchCommits(branch);
561
- const expandedInfo = expandCommitInfo(allCommits);
562
- const { latestVersion, commits } = expandedInfo;
648
+ const { latestVersion, taggedCommitHash } = findLatestVersionTag(allCommits);
649
+
650
+ let commits;
651
+ if (taggedCommitHash) {
652
+ // Use git's range selection (tag..HEAD) to correctly identify all commits
653
+ // since the tag, regardless of commit date ordering. This fixes an issue
654
+ // where commits from merged feature branches could be missed because their
655
+ // dates are older than the tagged commit.
656
+ const branchSha = allCommits[0]?.hash;
657
+ commits = branchSha ? getCommitsSinceTag(taggedCommitHash, branchSha) : [];
658
+ } else {
659
+ commits = allCommits;
660
+ }
661
+
662
+ const expandedInfo = { latestVersion, commits };
563
663
  const { newVersion, changelog } = calculateNextVersionAndChangelog(expandedInfo);
564
-
664
+
565
665
  return {
566
666
  currentVersion: latestVersion,
567
667
  newVersion,
@@ -577,6 +677,8 @@ module.exports = {
577
677
  getCurrentBranch,
578
678
  fetchTags,
579
679
  getAllBranchCommits,
680
+ findLatestVersionTag,
681
+ getCommitsSinceTag,
580
682
  expandCommitInfo,
581
683
  calculateNextVersionAndChangelog,
582
684
  processVersionInfo,