@logickernel/agileflow 0.15.1 → 0.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logickernel/agileflow",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
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,8 +14,9 @@ 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) {
17
+ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
18
18
  const safeTag = String(tagName).replace(/"/g, '\\"');
19
+ const safeRemote = String(remote).replace(/"/g, '\\"');
19
20
 
20
21
  // Write message to a temp file to avoid shell escaping issues with special characters
21
22
  const tempFile = path.join(os.tmpdir(), `agileflow-tag-${crypto.randomBytes(8).toString('hex')}.txt`);
@@ -25,8 +26,8 @@ async function pushTag(tagName, message, quiet = false) {
25
26
  // Create annotated tag using -F to read message from file
26
27
  execSync(`git tag -a "${safeTag}" -F "${tempFile}"`, { stdio: 'pipe' });
27
28
 
28
- // Push to origin
29
- execSync(`git push origin "${safeTag}"`, { stdio: 'pipe' });
29
+ // Push to remote
30
+ execSync(`git push "${safeRemote}" "${safeTag}"`, { stdio: 'pipe' });
30
31
 
31
32
  if (!quiet) {
32
33
  console.log(`Tag ${tagName} created and pushed successfully.`);
@@ -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) {
133
+ async function pushTag(tagName, message, quiet = false, 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;
@@ -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) {
99
+ async function pushTag(tagName, message, quiet = false, 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;
package/src/index.js CHANGED
@@ -12,7 +12,7 @@ Usage:
12
12
 
13
13
  Commands:
14
14
  <none> Prints the current version, next version, commits, and changelog
15
- push Push a semantic version tag to the remote repository
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
18
 
@@ -93,7 +93,7 @@ function displayVersionInfo(info, quiet) {
93
93
  * @param {string} pushType - 'push', 'gitlab', or 'github'
94
94
  * @param {{quiet: boolean}} options
95
95
  */
96
- async function handlePushCommand(pushType, options) {
96
+ async function handlePushCommand(pushType, options, remote = 'origin') {
97
97
  const info = await processVersionInfo();
98
98
 
99
99
  // Display version info
@@ -125,7 +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, options.quiet);
128
+ await pushModule.pushTag(info.newVersion, tagMessage, options.quiet, remote);
129
129
  }
130
130
 
131
131
  async function main() {
@@ -156,7 +156,8 @@ async function main() {
156
156
 
157
157
  // Handle push commands
158
158
  if (cmd === 'push' || cmd === 'gitlab' || cmd === 'github') {
159
- await handlePushCommand(cmd, options);
159
+ const remote = rest.find(arg => !arg.startsWith('-')) || 'origin';
160
+ await handlePushCommand(cmd, options, remote);
160
161
  return;
161
162
  }
162
163
 
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;
@@ -112,16 +114,35 @@ function fetchTags() {
112
114
  */
113
115
  function buildTagMap() {
114
116
  try {
115
- const output = runWithOutput('git tag --format=%(refname:short)|%(*objectname)|%(objectname)').trim();
117
+ // %(objecttype) distinguishes lightweight (commit) from annotated (tag) refs.
118
+ // %(*objectname) is the peeled commit SHA for annotated tags, but may be empty
119
+ // in shallow clones where git has not computed the peeled ref.
120
+ const output = runWithOutput('git tag --format=%(refname:short)|%(objecttype)|%(*objectname)|%(objectname)').trim();
116
121
  if (!output) return new Map();
117
122
  const map = new Map();
118
123
  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();
124
+ const [name, type, deref, obj] = line.split('|');
123
125
  const tagName = (name || '').trim();
124
- if (!sha || !tagName) continue;
126
+ if (!tagName) continue;
127
+
128
+ let sha;
129
+ if (type === 'commit') {
130
+ // Lightweight tag: %(objectname) is the commit SHA directly.
131
+ sha = (obj || '').trim();
132
+ } else {
133
+ // Annotated tag: use peeled commit SHA if available.
134
+ sha = (deref || '').trim();
135
+ if (!sha) {
136
+ // Peeling unavailable (shallow clone) — dereference via rev-parse.
137
+ try {
138
+ sha = runWithOutput(`git rev-parse "${tagName}^{}"`).trim();
139
+ } catch {
140
+ continue;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (!sha) continue;
125
146
  if (!map.has(sha)) map.set(sha, []);
126
147
  map.get(sha).push(tagName);
127
148
  }
@@ -158,28 +179,50 @@ function expandCommitInfo(commits) {
158
179
  if (!commits?.length) {
159
180
  return { latestVersion: null, commits: [] };
160
181
  }
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) => {
182
+
183
+ // Find the commit tagged with the highest semver version in the history.
184
+ // Using highest-version (not first-found) handles the case where a lower version
185
+ // was accidentally tagged on a recent commit (e.g. a failed CI run).
186
+ let bestIndex = -1;
187
+ let bestVersion = null;
188
+
189
+ for (let i = 0; i < commits.length; i++) {
190
+ const semverTags = commits[i].tags?.filter(tag => SEMVER_PATTERN.test(tag));
191
+ if (!semverTags?.length) continue;
192
+
193
+ const highest = semverTags.sort((a, b) => {
173
194
  const pa = parseVersion(a);
174
195
  const pb = parseVersion(b);
175
196
  if (pb.major !== pa.major) return pb.major - pa.major;
176
197
  if (pb.minor !== pa.minor) return pb.minor - pa.minor;
177
198
  return pb.patch - pa.patch;
178
199
  })[0];
179
- // Exclude the tagged commit itself - only return commits since the tag
200
+
201
+ if (!bestVersion) {
202
+ bestVersion = highest;
203
+ bestIndex = i;
204
+ } else {
205
+ const pBest = parseVersion(bestVersion);
206
+ const pCand = parseVersion(highest);
207
+ const isHigher =
208
+ pCand.major > pBest.major ||
209
+ (pCand.major === pBest.major && pCand.minor > pBest.minor) ||
210
+ (pCand.major === pBest.major && pCand.minor === pBest.minor && pCand.patch > pBest.patch);
211
+ if (isHigher) {
212
+ bestVersion = highest;
213
+ bestIndex = i;
214
+ }
215
+ }
216
+ }
217
+
218
+ if (bestIndex === -1) {
219
+ return { latestVersion: null, commits };
220
+ }
221
+
222
+ // Return only commits newer than the highest-version tag
180
223
  return {
181
- latestVersion,
182
- commits: commits.slice(0, taggedIndex),
224
+ latestVersion: bestVersion,
225
+ commits: commits.slice(0, bestIndex),
183
226
  };
184
227
  }
185
228
 
@@ -413,7 +456,12 @@ function getAllBranchCommits(branch) {
413
456
  try {
414
457
  resolvedSha = runWithOutput(`git rev-parse --verify -- origin/${branch}`).trim();
415
458
  } catch {
416
- return [];
459
+ // Last resort: use HEAD (detached HEAD in CI where remote tracking isn't set up)
460
+ try {
461
+ resolvedSha = runWithOutput('git rev-parse HEAD').trim();
462
+ } catch {
463
+ return [];
464
+ }
417
465
  }
418
466
  }
419
467