@logickernel/agileflow 0.1.0 → 0.2.1

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,136 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+
5
+ /**
6
+ * Creates a tag via the GitLab API.
7
+ * @param {string} tagName - The tag name
8
+ * @param {string} message - The tag message
9
+ * @param {string} projectPath - GitLab project path (e.g., "group/project")
10
+ * @param {string} serverHost - GitLab server hostname
11
+ * @param {string} accessToken - GitLab access token
12
+ * @param {string} ref - Git ref to tag (branch name or commit SHA)
13
+ * @returns {Promise<Object>} API response
14
+ */
15
+ function createTagViaAPI(tagName, message, projectPath, serverHost, accessToken, ref) {
16
+ return new Promise((resolve, reject) => {
17
+ const projectId = encodeURIComponent(projectPath);
18
+
19
+ const postData = JSON.stringify({
20
+ tag_name: tagName,
21
+ ref: ref,
22
+ message: message,
23
+ });
24
+
25
+ const options = {
26
+ hostname: serverHost,
27
+ port: 443,
28
+ path: `/api/v4/projects/${projectId}/repository/tags`,
29
+ method: 'POST',
30
+ headers: {
31
+ 'PRIVATE-TOKEN': accessToken,
32
+ 'Content-Type': 'application/json',
33
+ 'Content-Length': Buffer.byteLength(postData),
34
+ },
35
+ };
36
+
37
+ const req = https.request(options, (res) => {
38
+ let data = '';
39
+
40
+ res.on('data', (chunk) => {
41
+ data += chunk;
42
+ });
43
+
44
+ res.on('end', () => {
45
+ if (res.statusCode >= 200 && res.statusCode < 300) {
46
+ resolve(JSON.parse(data));
47
+ } else {
48
+ let errorMessage = `GitLab API request failed with status ${res.statusCode}`;
49
+ try {
50
+ const errorData = JSON.parse(data);
51
+ if (errorData.message) {
52
+ errorMessage += `: ${JSON.stringify(errorData.message)}`;
53
+ }
54
+ if (res.statusCode === 401) {
55
+ errorMessage += '\n\nAuthentication failed. The AGILEFLOW_TOKEN is invalid or expired.';
56
+ errorMessage += '\nTo fix this:';
57
+ errorMessage += '\n1. Go to your project Settings > Access Tokens';
58
+ errorMessage += '\n2. Check the expiration date of your AgileFlow Bot token';
59
+ errorMessage += '\n3. If expired, create a new token or extend the existing one';
60
+ } else if (res.statusCode === 403) {
61
+ errorMessage += '\n\nPermission denied. The AGILEFLOW_TOKEN needs "api" scope and maintainer role.';
62
+ errorMessage += '\nTo fix this:';
63
+ errorMessage += '\n1. Go to your project Settings > Access Tokens';
64
+ errorMessage += '\n2. Ensure your AgileFlow Bot token has "api" scope and maintainer role';
65
+ errorMessage += '\n3. If permissions are insufficient, create a new token with proper permissions';
66
+ }
67
+ } catch {
68
+ if (data) {
69
+ errorMessage += `\nResponse: ${data}`;
70
+ }
71
+ }
72
+ reject(new Error(errorMessage));
73
+ }
74
+ });
75
+ });
76
+
77
+ req.on('error', (error) => {
78
+ reject(new Error(`Network error: ${error.message}`));
79
+ });
80
+
81
+ req.write(postData);
82
+ req.end();
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Pushes a tag to GitLab via the API.
88
+ * Requires AGILEFLOW_TOKEN environment variable.
89
+ * Uses CI_SERVER_HOST and CI_PROJECT_PATH from GitLab CI environment.
90
+ * @param {string} tagName - The tag name
91
+ * @param {string} message - The tag message
92
+ * @returns {Promise<void>}
93
+ */
94
+ async function pushTag(tagName, message) {
95
+ const accessToken = process.env.AGILEFLOW_TOKEN;
96
+ const serverHost = process.env.CI_SERVER_HOST;
97
+ const projectPath = process.env.CI_PROJECT_PATH;
98
+ const commitSha = process.env.CI_COMMIT_SHA;
99
+
100
+ if (!accessToken) {
101
+ const projectUrl = serverHost && projectPath ? `https://${serverHost}/${projectPath}` : 'your-project';
102
+ const projectTokenUrl = `${projectUrl}/-/settings/access_tokens`;
103
+ const cicdUrl = `${projectUrl}/-/settings/ci_cd#js-cicd-variables-settings`;
104
+
105
+ throw new Error(
106
+ `AGILEFLOW_TOKEN environment variable is required but not set.\n\n` +
107
+ `To fix this:\n` +
108
+ `1. Create a project access token: ${projectTokenUrl}\n` +
109
+ ` - Name: AgileFlow Bot\n` +
110
+ ` - Role: maintainer\n` +
111
+ ` - Scopes: api\n` +
112
+ `2. Add it as a CI/CD variable: ${cicdUrl}\n` +
113
+ ` - Variable key: AGILEFLOW_TOKEN\n` +
114
+ ` - Protect variable: Yes (recommended)`
115
+ );
116
+ }
117
+
118
+ if (!serverHost) {
119
+ throw new Error('CI_SERVER_HOST environment variable is not set. Are you running inside GitLab CI?');
120
+ }
121
+
122
+ if (!projectPath) {
123
+ throw new Error('CI_PROJECT_PATH environment variable is not set. Are you running inside GitLab CI?');
124
+ }
125
+
126
+ if (!commitSha) {
127
+ throw new Error('CI_COMMIT_SHA environment variable is not set. Are you running inside GitLab CI?');
128
+ }
129
+
130
+ await createTagViaAPI(tagName, message || tagName, projectPath, serverHost, accessToken, commitSha);
131
+ }
132
+
133
+ module.exports = {
134
+ pushTag,
135
+ };
136
+
package/src/index.js CHANGED
@@ -1,50 +1,114 @@
1
1
  'use strict';
2
2
 
3
3
  const { version } = require('../package.json');
4
+ const { processVersionInfo } = require('./utils');
4
5
 
5
6
  function printHelp() {
6
- console.log(`agileflow - Automatic semantic versioning and changelog generation
7
+ console.log(`agileflow - Automatic semantic versioning and changelog generation tool.
7
8
 
8
9
  Usage:
9
10
  agileflow [options]
10
11
  agileflow <command>
11
12
 
12
13
  Commands:
13
- gitlab-ci Configure git, compute semver tag, and push to GitLab (CI mode)
14
+ <none> Prints the current version, next version, commits, and changelog
15
+ push Push a semantic version tag to the remote repository (native git)
16
+ gitlab Push a semantic version tag via GitLab API (for GitLab CI)
17
+ github Push a semantic version tag via GitHub API (for GitHub Actions)
14
18
 
15
19
  Options:
16
- --branch <name> Allow running on specified branch (default: main)
17
- -h, --help Show this help message
18
- -v, --version Show version number
20
+ --quiet Only output the next version (or empty if no bump)
21
+ -h, --help Show this help message
22
+ -v, --version Show version number
19
23
 
20
- Examples:
21
- agileflow # Run on main branch
22
- agileflow --branch develop # Run on develop branch
23
- agileflow gitlab-ci
24
-
25
- For more information, visit: https://code.logickernel.com/kernel/agileflow
24
+ For more information, visit: https://code.logickernel.com/tools/agileflow
26
25
  `);
27
26
  }
28
27
 
28
+ /**
29
+ * Parses command line arguments.
30
+ * @param {Array<string>} args - Command line arguments
31
+ * @returns {{quiet: boolean}}
32
+ */
29
33
  function parseArgs(args) {
30
- const parsed = { branch: 'main' };
31
- for (let i = 0; i < args.length; i++) {
32
- if (args[i] === '--branch' && i + 1 < args.length) {
33
- parsed.branch = args[i + 1];
34
- i++;
34
+ return {
35
+ quiet: args.includes('--quiet'),
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Displays version info to the console.
41
+ * @param {{currentVersion: string|null, nextVersion: string|null, commits: Array, changelog: string}} info
42
+ * @param {boolean} quiet - Only output the next version
43
+ */
44
+ function displayVersionInfo(info, quiet) {
45
+ const { currentVersion, nextVersion, commits, changelog } = info;
46
+
47
+ if (quiet) {
48
+ if (nextVersion) {
49
+ console.log(nextVersion);
35
50
  }
51
+ return;
52
+ }
53
+
54
+ console.log(`Current version: ${currentVersion || 'none'}`);
55
+ console.log(`Next version: ${nextVersion || 'no bump needed'}`);
56
+ console.log(`Commits since current version: ${commits.length}`);
57
+ if (changelog) {
58
+ console.log(`\nChangelog:\n${changelog}`);
36
59
  }
37
- return parsed;
38
60
  }
39
61
 
40
- async function runLocal(args) {
41
- const { branch } = parseArgs(args);
42
- const localMain = require('./local.js');
43
- await localMain(branch);
62
+ /**
63
+ * Handles a push command.
64
+ * @param {string} pushType - 'push', 'gitlab', or 'github'
65
+ * @param {{quiet: boolean}} options
66
+ */
67
+ async function handlePushCommand(pushType, options) {
68
+ const info = await processVersionInfo();
69
+
70
+ // Display version info
71
+ displayVersionInfo(info, options.quiet);
72
+
73
+ // Skip push if no version bump needed
74
+ if (!info.nextVersion) {
75
+ if (!options.quiet) {
76
+ console.log('\nNo version bump needed. Skipping tag creation.');
77
+ }
78
+ return;
79
+ }
80
+
81
+ // Get the appropriate push module
82
+ let pushModule;
83
+ switch (pushType) {
84
+ case 'push':
85
+ pushModule = require('./git-push');
86
+ break;
87
+ case 'gitlab':
88
+ pushModule = require('./gitlab-push');
89
+ break;
90
+ case 'github':
91
+ pushModule = require('./github-push');
92
+ break;
93
+ }
94
+
95
+ // Create tag message from changelog
96
+ const tagMessage = info.changelog || info.nextVersion;
97
+
98
+ if (!options.quiet) {
99
+ console.log(`\nCreating tag ${info.nextVersion}...`);
100
+ }
101
+
102
+ await pushModule.pushTag(info.nextVersion, tagMessage);
103
+
104
+ if (!options.quiet) {
105
+ console.log(`Tag ${info.nextVersion} created and pushed successfully.`);
106
+ }
44
107
  }
45
108
 
46
109
  async function main() {
47
110
  const [, , cmd, ...rest] = process.argv;
111
+ const options = parseArgs(cmd ? [cmd, ...rest] : rest);
48
112
 
49
113
  // Handle help
50
114
  if (cmd === '-h' || cmd === '--help' || cmd === 'help') {
@@ -58,14 +122,13 @@ async function main() {
58
122
  process.exit(0);
59
123
  }
60
124
 
61
- // Handle gitlab-ci command
62
- if (cmd === 'gitlab-ci') {
63
- console.error('Error: gitlab-ci command is not yet implemented');
64
- console.error('This feature will be available in a future release.');
65
- process.exit(1);
125
+ // Handle push commands
126
+ if (cmd === 'push' || cmd === 'gitlab' || cmd === 'github') {
127
+ await handlePushCommand(cmd, options);
128
+ return;
66
129
  }
67
130
 
68
- // Unknown command
131
+ // Unknown command (not an option)
69
132
  if (cmd && !cmd.startsWith('--')) {
70
133
  console.error(`Error: Unknown command "${cmd}"`);
71
134
  console.error();
@@ -73,8 +136,9 @@ async function main() {
73
136
  process.exit(1);
74
137
  }
75
138
 
76
- // Default: run version calculation
77
- await runLocal(cmd ? [cmd, ...rest] : rest);
139
+ // Default: show version info
140
+ const info = await processVersionInfo();
141
+ displayVersionInfo(info, options.quiet);
78
142
  }
79
143
 
80
144
  process.on('unhandledRejection', (err) => {
package/src/utils.js CHANGED
@@ -1,6 +1,66 @@
1
1
  'use strict';
2
2
 
3
- const { runWithOutput, ensureGitRepo } = require('./git-utils');
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+
6
+ /**
7
+ * Executes a shell command and returns the output.
8
+ * @param {string} command - The command to execute
9
+ * @param {Object} options - Execution options
10
+ * @returns {string} Command output
11
+ * @throws {Error} If command fails
12
+ */
13
+ function runWithOutput(command, options = {}) {
14
+ try {
15
+ return execSync(command, { stdio: 'pipe', encoding: 'utf8', ...options });
16
+ } catch (error) {
17
+ const captured = {
18
+ stdout: error?.stdout ? String(error.stdout) : '',
19
+ stderr: error?.stderr ? String(error.stderr) : '',
20
+ message: error?.message || 'Command failed',
21
+ status: typeof error?.status === 'number' ? error.status : 1,
22
+ };
23
+ try {
24
+ Object.defineProperty(error, '_captured', { value: captured });
25
+ } catch {
26
+ error._captured = captured;
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Executes a shell command without returning output.
34
+ * @param {string} command - The command to execute
35
+ * @param {Object} options - Execution options
36
+ * @throws {Error} If command fails
37
+ */
38
+ function run(command, options = {}) {
39
+ execSync(command, { stdio: 'pipe', ...options });
40
+ }
41
+
42
+ /**
43
+ * Ensures the current directory is a git repository.
44
+ * @throws {Error} If the current directory is not a git repository
45
+ */
46
+ function ensureGitRepo() {
47
+ if (!fs.existsSync('.git')) {
48
+ throw new Error('Current directory is not a git repository (missing .git directory).');
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Gets the current branch name.
54
+ * @returns {string} Current branch name
55
+ * @throws {Error} If in detached HEAD state
56
+ */
57
+ function getCurrentBranch() {
58
+ const branch = runWithOutput('git branch --show-current').trim();
59
+ if (!branch) {
60
+ throw new Error('Repository is in a detached HEAD state. Please check out a branch and try again.');
61
+ }
62
+ return branch;
63
+ }
4
64
 
5
65
  // Conventional commit type configuration
6
66
  const TYPE_ORDER = ['feat', 'fix', 'perf', 'refactor', 'style', 'test', 'docs', 'build', 'ci', 'chore', 'revert'];
@@ -9,15 +69,12 @@ const SEMVER_PATTERN = /^v(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9.-]+)?$/;
9
69
 
10
70
  /**
11
71
  * Fetches tags from remote (non-destructive) if a remote is configured.
12
- * This is a safe operation that doesn't modify the working directory.
13
72
  * @returns {boolean} True if tags were fetched, false if using local tags only
14
73
  */
15
- function fetchTagsLocally() {
74
+ function fetchTags() {
16
75
  try {
17
76
  const remotes = runWithOutput('git remote').trim();
18
- if (!remotes) {
19
- return false;
20
- }
77
+ if (!remotes) return false;
21
78
  runWithOutput('git fetch --tags --prune --prune-tags');
22
79
  return true;
23
80
  } catch {
@@ -25,21 +82,6 @@ function fetchTagsLocally() {
25
82
  }
26
83
  }
27
84
 
28
- /**
29
- * Validates that the current branch is the expected name.
30
- * @param {string} expectedName - The expected name of the branch
31
- * @throws {Error} If the current branch is not the expected name or in detached HEAD state
32
- */
33
- function validateBranchName(expectedName) {
34
- const branch = runWithOutput('git branch --show-current').trim();
35
- if (!branch) {
36
- throw new Error('Repository is in a detached HEAD state. Please check out a branch and try again.');
37
- }
38
- if (branch !== expectedName) {
39
- throw new Error(`Current branch is "${branch}", not "${expectedName}". Switch to ${expectedName} or use --branch ${branch}.`);
40
- }
41
- }
42
-
43
85
  /**
44
86
  * Gets all tags pointing to a specific commit.
45
87
  * @param {string} commitSha - The commit SHA to check for tags
@@ -57,18 +99,13 @@ function getTagsForCommit(commitSha) {
57
99
  /**
58
100
  * Parses a conventional commit message.
59
101
  * @param {string} message - The commit message to parse
60
- * @returns {{type: string, breaking: boolean, scope: string, description: string}|null} Parsed commit or null if not conventional
102
+ * @returns {{type: string, breaking: boolean, scope: string, description: string}|null}
61
103
  */
62
104
  function parseConventionalCommit(message) {
63
105
  if (!message) return null;
64
-
65
- // Get the first line (subject) of the commit message
66
106
  const subject = message.split('\n')[0].trim();
67
-
68
- // Conventional commit pattern: type[!]?(scope)?: description
69
107
  const match = subject.match(/^(\w+)(!)?(?:\(([^)]+)\))?:\s+(.+)$/);
70
108
  if (!match) return null;
71
-
72
109
  return {
73
110
  type: match[1].toLowerCase(),
74
111
  breaking: Boolean(match[2]),
@@ -79,17 +116,14 @@ function parseConventionalCommit(message) {
79
116
 
80
117
  /**
81
118
  * Expands commit information by finding the latest version and filtering commits.
82
- * Filters out commits that are older than the latest commit with a semver tag.
83
- * Commits are expected to be ordered from newest to oldest.
84
- * @param {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>} commits - Array of commit objects
85
- * @returns {{latestVersion: string|null, commits: Array}} Object with latestVersion and filtered commits array
119
+ * @param {Array} commits - Array of commit objects (newest to oldest)
120
+ * @returns {{latestVersion: string|null, commits: Array}} Filtered commits since last version
86
121
  */
87
122
  function expandCommitInfo(commits) {
88
123
  if (!commits?.length) {
89
124
  return { latestVersion: null, commits: [] };
90
125
  }
91
126
 
92
- // Find the first commit (newest) that has a semver tag
93
127
  const taggedIndex = commits.findIndex(commit =>
94
128
  commit.tags?.some(tag => SEMVER_PATTERN.test(tag))
95
129
  );
@@ -99,16 +133,17 @@ function expandCommitInfo(commits) {
99
133
  }
100
134
 
101
135
  const latestVersion = commits[taggedIndex].tags.find(tag => SEMVER_PATTERN.test(tag));
136
+ // Exclude the tagged commit itself - only return commits since the tag
102
137
  return {
103
138
  latestVersion,
104
- commits: commits.slice(0, taggedIndex + 1),
139
+ commits: commits.slice(0, taggedIndex),
105
140
  };
106
141
  }
107
142
 
108
143
  /**
109
- * Extracts issue reference from commit message (e.g., #123 or (#123)).
144
+ * Extracts issue reference from commit message.
110
145
  * @param {string} message - The commit message
111
- * @returns {string|null} Issue reference like "(#123)" or null if not found
146
+ * @returns {string|null} Issue reference like "(#123)" or null
112
147
  */
113
148
  function extractIssueReference(message) {
114
149
  const match = message?.match(/\(?#(\d+)\)?/);
@@ -116,33 +151,26 @@ function extractIssueReference(message) {
116
151
  }
117
152
 
118
153
  /**
119
- * Formats the first line of commit message for changelog, removing the type prefix.
120
- * @param {string} subject - The first line of the commit message
154
+ * Formats commit description for changelog.
155
+ * @param {string} subject - First line of commit message
121
156
  * @param {Object} parsed - Parsed conventional commit info
122
- * @param {string} fullMessage - Full commit message to check for BREAKING CHANGE:
157
+ * @param {string} fullMessage - Full commit message
123
158
  * @returns {string} Formatted description
124
159
  */
125
160
  function formatChangelogDescription(subject, parsed, fullMessage) {
126
- if (!parsed) {
127
- // Not a conventional commit, use subject as-is
128
- return subject;
129
- }
130
-
161
+ if (!parsed) return subject;
131
162
  let description = parsed.description;
132
-
133
- // Add BREAKING prefix if it's a breaking change (check both ! and BREAKING CHANGE:)
134
163
  const isBreaking = parsed.breaking || /BREAKING CHANGE:/i.test(fullMessage);
135
164
  if (isBreaking) {
136
165
  description = `BREAKING: ${description}`;
137
166
  }
138
-
139
167
  return description;
140
168
  }
141
169
 
142
170
  /**
143
- * Parses a semver version string into its components.
144
- * @param {string|null} version - Version string like "v1.2.3" or "v1.2.3-beta"
145
- * @returns {{major: number, minor: number, patch: number}} Parsed version or {0,0,0} if invalid
171
+ * Parses a semver version string into components.
172
+ * @param {string|null} version - Version string like "v1.2.3"
173
+ * @returns {{major: number, minor: number, patch: number}}
146
174
  */
147
175
  function parseVersion(version) {
148
176
  if (!version) return { major: 0, minor: 0, patch: 0 };
@@ -152,14 +180,13 @@ function parseVersion(version) {
152
180
  }
153
181
 
154
182
  /**
155
- * Determines the version bump type based on commit analysis.
156
- * @param {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean}} analysis - Commit analysis
183
+ * Determines version bump type based on commit analysis.
184
+ * @param {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean}} analysis
157
185
  * @param {boolean} isPreOneZero - Whether current version is 0.x.x
158
- * @returns {'major'|'minor'|'patch'|'none'} The version bump type
186
+ * @returns {'major'|'minor'|'patch'|'none'}
159
187
  */
160
188
  function determineVersionBumpType(analysis, isPreOneZero) {
161
189
  const { hasBreaking, hasFeat, hasPatchTypes } = analysis;
162
-
163
190
  if (isPreOneZero) {
164
191
  if (hasBreaking || hasFeat) return 'minor';
165
192
  if (hasPatchTypes) return 'patch';
@@ -173,17 +200,17 @@ function determineVersionBumpType(analysis, isPreOneZero) {
173
200
 
174
201
  /**
175
202
  * Applies a version bump to the current version.
176
- * @param {{major: number, minor: number, patch: number}} current - Current version
177
- * @param {'major'|'minor'|'patch'|'none'} bump - Bump type
178
- * @returns {string} Next version string like "v1.2.3"
203
+ * @param {{major: number, minor: number, patch: number}} current
204
+ * @param {'major'|'minor'|'patch'|'none'} bump
205
+ * @returns {string|null} Next version string or null if no bump
179
206
  */
180
207
  function applyVersionBump(current, bump) {
181
- let { major, minor, patch } = current;
208
+ const { major, minor, patch } = current;
182
209
  switch (bump) {
183
210
  case 'major': return `v${major + 1}.0.0`;
184
211
  case 'minor': return `v${major}.${minor + 1}.0`;
185
212
  case 'patch': return `v${major}.${minor}.${patch + 1}`;
186
- default: return `v${major}.${minor}.${patch}`;
213
+ default: return null;
187
214
  }
188
215
  }
189
216
 
@@ -244,20 +271,22 @@ function generateTypeChangelog(commits) {
244
271
 
245
272
  const lines = [];
246
273
  for (const entry of noScope) {
247
- lines.push(`- ${entry.description}${entry.issueRef}`);
274
+ const ref = entry.issueRef ? ` ${entry.issueRef}` : '';
275
+ lines.push(`- ${entry.description}${ref}`);
248
276
  }
249
277
  for (const scope of Object.keys(byScope).sort()) {
250
278
  for (const entry of byScope[scope]) {
251
- lines.push(`- **${scope}**: ${entry.description}${entry.issueRef}`);
279
+ const ref = entry.issueRef ? ` ${entry.issueRef}` : '';
280
+ lines.push(`- **${scope}**: ${entry.description}${ref}`);
252
281
  }
253
282
  }
254
283
  return lines;
255
284
  }
256
285
 
257
286
  /**
258
- * Calculates the next version and generates a changelog from expanded commit info.
259
- * @param {{latestVersion: string|null, commits: Array}} expandedInfo - Output from expandCommitInfo
260
- * @returns {{nextVersion: string, changelog: string}} Object with nextVersion and changelog
287
+ * Calculates the next version and generates a changelog.
288
+ * @param {{latestVersion: string|null, commits: Array}} expandedInfo
289
+ * @returns {{nextVersion: string|null, changelog: string}}
261
290
  */
262
291
  function calculateNextVersionAndChangelog(expandedInfo) {
263
292
  const { latestVersion, commits } = expandedInfo;
@@ -278,7 +307,6 @@ function calculateNextVersionAndChangelog(expandedInfo) {
278
307
  changelogLines.push('');
279
308
  }
280
309
 
281
- // Remove trailing empty line
282
310
  if (changelogLines.at(-1) === '') {
283
311
  changelogLines.pop();
284
312
  }
@@ -287,54 +315,18 @@ function calculateNextVersionAndChangelog(expandedInfo) {
287
315
  }
288
316
 
289
317
  /**
290
- * Processes version information for a branch, returning version details and changelog.
291
- * @param {string} branch - The branch to process
292
- * @returns {Promise<{previousVersion: string|null, nextVersion: string, commits: Array, conventionalCommits: Object, changelog: string}>} Promise resolving to version info
293
- */
294
- async function processVersionInfo(branch) {
295
- ensureGitRepo();
296
- validateBranchName(branch);
297
- fetchTagsLocally();
298
-
299
- const allCommits = getAllBranchCommits(branch);
300
- const expandedInfo = expandCommitInfo(allCommits);
301
- const { latestVersion, commits } = expandedInfo;
302
-
303
- // Group commits by conventional type
304
- const conventionalCommits = Object.fromEntries(TYPE_ORDER.map(t => [t, []]));
305
- for (const commit of commits) {
306
- const parsed = parseConventionalCommit(commit.message);
307
- if (parsed && conventionalCommits[parsed.type]) {
308
- conventionalCommits[parsed.type].push({ ...commit, conventional: parsed });
309
- }
310
- }
311
-
312
- const { nextVersion, changelog } = calculateNextVersionAndChangelog(expandedInfo);
313
-
314
- return {
315
- previousVersion: latestVersion,
316
- nextVersion,
317
- commits,
318
- conventionalCommits,
319
- changelog,
320
- };
321
- }
322
-
323
- /**
324
- * Retrieves all commits in the history of the specified branch, including merged commits.
325
- * Returns a simplified array with commit hash, datetime, author, commit message, and tags.
318
+ * Retrieves all commits in the history of the specified branch.
326
319
  * @param {string} branch - The branch to get commits from
327
- * @returns {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>} Array of commit objects
320
+ * @returns {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>}
328
321
  */
329
322
  function getAllBranchCommits(branch) {
330
323
  try {
331
- // Verify the branch exists
332
324
  runWithOutput(`git rev-parse --verify ${branch}`);
333
325
  } catch {
334
326
  return [];
335
327
  }
336
328
 
337
- const RS = '\x1E'; // Record Separator
329
+ const RS = '\x1E';
338
330
  const COMMIT_SEP = `${RS}${RS}`;
339
331
 
340
332
  try {
@@ -363,14 +355,37 @@ function getAllBranchCommits(branch) {
363
355
  }
364
356
  }
365
357
 
358
+ /**
359
+ * Processes version information for the current branch.
360
+ * @returns {Promise<{currentVersion: string|null, nextVersion: string|null, commits: Array, changelog: string}>}
361
+ */
362
+ async function processVersionInfo() {
363
+ ensureGitRepo();
364
+ const branch = getCurrentBranch();
365
+ fetchTags();
366
+
367
+ const allCommits = getAllBranchCommits(branch);
368
+ const expandedInfo = expandCommitInfo(allCommits);
369
+ const { latestVersion, commits } = expandedInfo;
370
+ const { nextVersion, changelog } = calculateNextVersionAndChangelog(expandedInfo);
371
+
372
+ return {
373
+ currentVersion: latestVersion,
374
+ nextVersion,
375
+ commits,
376
+ changelog,
377
+ };
378
+ }
379
+
366
380
  module.exports = {
381
+ run,
382
+ runWithOutput,
367
383
  ensureGitRepo,
368
- fetchTagsLocally,
369
- validateBranchName,
384
+ getCurrentBranch,
385
+ fetchTags,
370
386
  getAllBranchCommits,
371
387
  expandCommitInfo,
372
388
  calculateNextVersionAndChangelog,
373
389
  processVersionInfo,
374
390
  parseConventionalCommit,
375
391
  };
376
-