@logickernel/agileflow 0.16.0 → 0.17.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logickernel/agileflow",
3
- "version": "0.16.0",
3
+ "version": "0.17.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/index.js CHANGED
@@ -15,6 +15,7 @@ Commands:
15
15
  push [remote] Push a semantic version tag to the remote repository (default: origin)
16
16
  gitlab Create a semantic version tag via GitLab API (for GitLab CI)
17
17
  github Create a semantic version tag via GitHub API (for GitHub Actions)
18
+ version Print the agileflow tool version
18
19
 
19
20
  Options:
20
21
  --quiet Only output the next version (or empty if no bump)
@@ -33,7 +34,7 @@ const VALID_OPTIONS = ['--quiet', '--help', '-h', '--version', '-v'];
33
34
  /**
34
35
  * Valid commands.
35
36
  */
36
- const VALID_COMMANDS = ['push', 'gitlab', 'github'];
37
+ const VALID_COMMANDS = ['push', 'gitlab', 'github', 'version'];
37
38
 
38
39
  /**
39
40
  * Parses command line arguments and validates them.
package/src/utils.js CHANGED
@@ -98,7 +98,9 @@ function fetchTags() {
98
98
  try {
99
99
  const remotes = runWithOutput('git remote').trim();
100
100
  if (!remotes) return false;
101
- runWithOutput('git fetch --tags --prune --prune-tags');
101
+ // --force allows updating local tags that conflict with remote tags.
102
+ // Avoid --prune-tags which can fail in shallow CI clones.
103
+ runWithOutput('git fetch --tags --force');
102
104
  return true;
103
105
  } catch {
104
106
  return false;
@@ -106,22 +108,40 @@ function fetchTags() {
106
108
  }
107
109
 
108
110
  /**
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
+ * Builds a tag map from locally stored tags.
111
112
  * @returns {Map<string, string[]>}
112
113
  */
113
- function buildTagMap() {
114
+ function buildTagMapFromLocal() {
114
115
  try {
115
- const output = runWithOutput('git tag --format=%(refname:short)|%(*objectname)|%(objectname)').trim();
116
+ // %(objecttype) distinguishes lightweight (commit) from annotated (tag) refs.
117
+ // %(*objectname) is the peeled commit SHA for annotated tags, but may be empty
118
+ // in shallow clones where git has not computed the peeled ref.
119
+ const output = runWithOutput('git tag --format=%(refname:short)|%(objecttype)|%(*objectname)|%(objectname)').trim();
116
120
  if (!output) return new Map();
117
121
  const map = new Map();
118
122
  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 [name, type, deref, obj] = line.split('|');
123
124
  const tagName = (name || '').trim();
124
- if (!sha || !tagName) continue;
125
+ if (!tagName) continue;
126
+
127
+ let sha;
128
+ if (type === 'commit') {
129
+ // Lightweight tag: %(objectname) is the commit SHA directly.
130
+ sha = (obj || '').trim();
131
+ } else {
132
+ // Annotated tag: use peeled commit SHA if available.
133
+ sha = (deref || '').trim();
134
+ if (!sha) {
135
+ // Peeling unavailable (shallow clone) — dereference via rev-parse.
136
+ try {
137
+ sha = runWithOutput(`git rev-parse "${tagName}^{}"`).trim();
138
+ } catch {
139
+ continue;
140
+ }
141
+ }
142
+ }
143
+
144
+ if (!sha) continue;
125
145
  if (!map.has(sha)) map.set(sha, []);
126
146
  map.get(sha).push(tagName);
127
147
  }
@@ -131,6 +151,60 @@ function buildTagMap() {
131
151
  }
132
152
  }
133
153
 
154
+ /**
155
+ * Builds a tag map by reading tags directly from the remote via ls-remote.
156
+ * Used as a fallback when git fetch --tags fails (e.g. in shallow CI clones).
157
+ * Does not store anything locally.
158
+ * @returns {Map<string, string[]>}
159
+ */
160
+ function buildTagMapFromRemote() {
161
+ try {
162
+ const remotes = runWithOutput('git remote').trim();
163
+ if (!remotes) return new Map();
164
+ const remote = remotes.split('\n')[0].trim();
165
+ const output = runWithOutput(`git ls-remote --tags ${remote}`).trim();
166
+ if (!output) return new Map();
167
+
168
+ const map = new Map();
169
+ const direct = new Map(); // tagName -> SHA (tag obj or commit)
170
+ const peeled = new Map(); // tagName -> commit SHA (from ^{} entries)
171
+
172
+ for (const line of output.split('\n')) {
173
+ const tabIdx = line.indexOf('\t');
174
+ if (tabIdx === -1) continue;
175
+ const sha = line.slice(0, tabIdx).trim();
176
+ const ref = line.slice(tabIdx + 1).trim();
177
+ if (ref.endsWith('^{}')) {
178
+ // Annotated tag: peeled entry gives the commit SHA
179
+ peeled.set(ref.slice('refs/tags/'.length, -3), sha);
180
+ } else if (ref.startsWith('refs/tags/')) {
181
+ direct.set(ref.slice('refs/tags/'.length), sha);
182
+ }
183
+ }
184
+
185
+ for (const [name, sha] of direct) {
186
+ const commitSha = peeled.get(name) || sha;
187
+ if (!map.has(commitSha)) map.set(commitSha, []);
188
+ map.get(commitSha).push(name);
189
+ }
190
+ return map;
191
+ } catch {
192
+ return new Map();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Builds a map of commit SHA → tag names for all tags in the repository.
198
+ * Tries local tags first; falls back to reading from the remote when local
199
+ * tags are absent (common in shallow CI clones where git fetch --tags fails).
200
+ * @returns {Map<string, string[]>}
201
+ */
202
+ function buildTagMap() {
203
+ const local = buildTagMapFromLocal();
204
+ if (local.size > 0) return local;
205
+ return buildTagMapFromRemote();
206
+ }
207
+
134
208
  /**
135
209
  * Parses a conventional commit message.
136
210
  * @param {string} message - The commit message to parse
@@ -158,28 +232,50 @@ function expandCommitInfo(commits) {
158
232
  if (!commits?.length) {
159
233
  return { latestVersion: null, commits: [] };
160
234
  }
161
-
162
- const taggedIndex = commits.findIndex(commit =>
163
- commit.tags?.some(tag => SEMVER_PATTERN.test(tag))
164
- );
165
-
166
- if (taggedIndex === -1) {
167
- return { latestVersion: null, commits };
168
- }
169
-
170
- const latestVersion = commits[taggedIndex].tags
171
- .filter(tag => SEMVER_PATTERN.test(tag))
172
- .sort((a, b) => {
235
+
236
+ // Find the commit tagged with the highest semver version in the history.
237
+ // Using highest-version (not first-found) handles the case where a lower version
238
+ // was accidentally tagged on a recent commit (e.g. a failed CI run).
239
+ let bestIndex = -1;
240
+ let bestVersion = null;
241
+
242
+ for (let i = 0; i < commits.length; i++) {
243
+ const semverTags = commits[i].tags?.filter(tag => SEMVER_PATTERN.test(tag));
244
+ if (!semverTags?.length) continue;
245
+
246
+ const highest = semverTags.sort((a, b) => {
173
247
  const pa = parseVersion(a);
174
248
  const pb = parseVersion(b);
175
249
  if (pb.major !== pa.major) return pb.major - pa.major;
176
250
  if (pb.minor !== pa.minor) return pb.minor - pa.minor;
177
251
  return pb.patch - pa.patch;
178
252
  })[0];
179
- // Exclude the tagged commit itself - only return commits since the tag
253
+
254
+ if (!bestVersion) {
255
+ bestVersion = highest;
256
+ bestIndex = i;
257
+ } else {
258
+ const pBest = parseVersion(bestVersion);
259
+ const pCand = parseVersion(highest);
260
+ const isHigher =
261
+ pCand.major > pBest.major ||
262
+ (pCand.major === pBest.major && pCand.minor > pBest.minor) ||
263
+ (pCand.major === pBest.major && pCand.minor === pBest.minor && pCand.patch > pBest.patch);
264
+ if (isHigher) {
265
+ bestVersion = highest;
266
+ bestIndex = i;
267
+ }
268
+ }
269
+ }
270
+
271
+ if (bestIndex === -1) {
272
+ return { latestVersion: null, commits };
273
+ }
274
+
275
+ // Return only commits newer than the highest-version tag
180
276
  return {
181
- latestVersion,
182
- commits: commits.slice(0, taggedIndex),
277
+ latestVersion: bestVersion,
278
+ commits: commits.slice(0, bestIndex),
183
279
  };
184
280
  }
185
281