@edcalderon/versioning 1.2.0 → 1.3.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.
Files changed (31) hide show
  1. package/README.md +121 -14
  2. package/dist/cli.js +76 -4
  3. package/dist/extensions/{cleanup-repo-extension.d.ts → cleanup-repo/index.d.ts} +2 -2
  4. package/dist/extensions/{cleanup-repo-extension.js → cleanup-repo/index.js} +40 -32
  5. package/dist/extensions/lifecycle-hooks/index.d.ts +4 -0
  6. package/dist/extensions/{lifecycle-hooks.js → lifecycle-hooks/index.js} +1 -1
  7. package/dist/extensions/npm-publish/index.d.ts +4 -0
  8. package/dist/extensions/{npm-publish.js → npm-publish/index.js} +1 -1
  9. package/dist/extensions/reentry-status/config-manager.js +3 -1
  10. package/dist/extensions/reentry-status/constants.d.ts +1 -1
  11. package/dist/extensions/reentry-status/constants.js +1 -1
  12. package/dist/extensions/reentry-status/extension.d.ts +4 -0
  13. package/dist/extensions/{reentry-status-extension.js → reentry-status/extension.js} +14 -14
  14. package/dist/extensions/reentry-status/index.d.ts +2 -0
  15. package/dist/extensions/reentry-status/index.js +5 -0
  16. package/dist/extensions/sample-extension/index.d.ts +4 -0
  17. package/dist/extensions/{sample-extension.js → sample-extension/index.js} +1 -1
  18. package/dist/extensions/secrets-check/index.d.ts +16 -0
  19. package/dist/extensions/secrets-check/index.js +264 -0
  20. package/dist/extensions.js +27 -15
  21. package/dist/release.d.ts +16 -12
  22. package/dist/release.js +35 -1
  23. package/dist/versioning.d.ts +49 -0
  24. package/dist/versioning.js +313 -2
  25. package/examples/versioning.config.branch-aware.json +51 -0
  26. package/package.json +2 -2
  27. package/versioning.config.json +44 -1
  28. package/dist/extensions/lifecycle-hooks.d.ts +0 -4
  29. package/dist/extensions/npm-publish.d.ts +0 -4
  30. package/dist/extensions/reentry-status-extension.d.ts +0 -4
  31. package/dist/extensions/sample-extension.d.ts +0 -4
@@ -65,6 +65,32 @@ class VersionManager {
65
65
  await this.updateVersion(newVersion);
66
66
  return newVersion;
67
67
  }
68
+ async bumpVersionBranchAware(releaseType, options = {}) {
69
+ if (options.build !== undefined && (!Number.isInteger(options.build) || options.build < 0)) {
70
+ throw new Error(`Invalid build number "${options.build}". Expected a non-negative integer.`);
71
+ }
72
+ const branch = options.targetBranch || await this.getCurrentBranch();
73
+ const { matchPattern, resolvedConfig } = this.resolveBranchConfig(branch, options.forceBranchAware === true);
74
+ const versionFormat = options.format || resolvedConfig.versionFormat;
75
+ const version = await this.buildBranchAwareVersion(releaseType, branch, versionFormat, resolvedConfig, options.build);
76
+ const syncFiles = resolvedConfig.syncFiles.length > 0
77
+ ? resolvedConfig.syncFiles
78
+ : this.getDefaultSyncFiles(versionFormat);
79
+ if (syncFiles.length === 0) {
80
+ throw new Error(`No sync files configured for branch "${branch}".`);
81
+ }
82
+ for (const filePath of syncFiles) {
83
+ await this.updateVersionFile(filePath, version);
84
+ }
85
+ return {
86
+ version,
87
+ branch,
88
+ matchPattern,
89
+ versionFormat,
90
+ tagFormat: resolvedConfig.tagFormat,
91
+ syncFiles
92
+ };
93
+ }
68
94
  async updateVersion(newVersion) {
69
95
  // Update root package.json
70
96
  await this.updatePackageJson(this.config.rootPackageJson, newVersion);
@@ -82,8 +108,13 @@ class VersionManager {
82
108
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
83
109
  }
84
110
  async createGitTag(version, message) {
85
- const tagMessage = message || `Release v${version}`;
86
- await this.git.addAnnotatedTag(`v${version}`, tagMessage);
111
+ await this.createGitTagWithFormat(version, 'v{version}', message);
112
+ }
113
+ async createGitTagWithFormat(version, tagFormat = 'v{version}', message) {
114
+ const tagName = this.renderTag(tagFormat, version);
115
+ const tagMessage = message || `Release ${tagName}`;
116
+ await this.git.addAnnotatedTag(tagName, tagMessage);
117
+ return tagName;
87
118
  }
88
119
  async commitChanges(version) {
89
120
  const commitMessage = this.config.conventionalCommits
@@ -92,6 +123,286 @@ class VersionManager {
92
123
  await this.git.add('.');
93
124
  await this.git.commit(commitMessage);
94
125
  }
126
+ async getCurrentBranch() {
127
+ const branch = (await this.git.revparse(['--abbrev-ref', 'HEAD'])).trim();
128
+ if (!branch) {
129
+ throw new Error('Unable to detect current Git branch.');
130
+ }
131
+ return branch;
132
+ }
133
+ resolveBranchConfig(branch, forceBranchAware) {
134
+ const branchAwareness = this.getBranchAwarenessConfig(forceBranchAware);
135
+ const { branches } = branchAwareness;
136
+ if (branches[branch]) {
137
+ return {
138
+ matchPattern: branch,
139
+ resolvedConfig: this.normalizeBranchRule(branches[branch])
140
+ };
141
+ }
142
+ const wildcardPatterns = Object.keys(branches)
143
+ .filter((pattern) => pattern.includes('*'))
144
+ .sort((a, b) => b.length - a.length);
145
+ for (const pattern of wildcardPatterns) {
146
+ if (this.matchesPattern(branch, pattern)) {
147
+ return {
148
+ matchPattern: pattern,
149
+ resolvedConfig: this.normalizeBranchRule(branches[pattern])
150
+ };
151
+ }
152
+ }
153
+ const fallbackPattern = branchAwareness.defaultBranch;
154
+ if (fallbackPattern && branches[fallbackPattern]) {
155
+ return {
156
+ matchPattern: fallbackPattern,
157
+ resolvedConfig: this.normalizeBranchRule(branches[fallbackPattern])
158
+ };
159
+ }
160
+ throw new Error(`No branch configuration matched "${branch}".`);
161
+ }
162
+ getBranchAwarenessConfig(forceBranchAware) {
163
+ const configured = this.config.branchAwareness;
164
+ const defaultBranch = configured?.defaultBranch || 'main';
165
+ const configuredBranches = configured?.branches && Object.keys(configured.branches).length > 0
166
+ ? configured.branches
167
+ : this.getDefaultBranchRules(defaultBranch);
168
+ if (!configured?.enabled && !forceBranchAware) {
169
+ throw new Error('Branch awareness is not enabled in versioning.config.json.');
170
+ }
171
+ return {
172
+ defaultBranch,
173
+ branches: configuredBranches
174
+ };
175
+ }
176
+ getDefaultBranchRules(defaultBranch) {
177
+ const rootPackageJson = this.config.rootPackageJson || 'package.json';
178
+ const defaults = {
179
+ main: {
180
+ versionFormat: 'semantic',
181
+ tagFormat: 'v{version}',
182
+ syncFiles: [rootPackageJson],
183
+ environment: 'production',
184
+ bumpStrategy: 'semantic'
185
+ },
186
+ develop: {
187
+ versionFormat: 'dev',
188
+ tagFormat: 'v{version}',
189
+ syncFiles: ['version.development.json'],
190
+ environment: 'development',
191
+ bumpStrategy: 'dev-build'
192
+ },
193
+ 'feature/*': {
194
+ versionFormat: 'feature',
195
+ tagFormat: 'v{version}',
196
+ syncFiles: ['version.development.json'],
197
+ environment: 'development',
198
+ bumpStrategy: 'feature-branch'
199
+ },
200
+ 'hotfix/*': {
201
+ versionFormat: 'hotfix',
202
+ tagFormat: 'v{version}',
203
+ syncFiles: ['version.development.json'],
204
+ environment: 'development',
205
+ bumpStrategy: 'hotfix'
206
+ }
207
+ };
208
+ if (!defaults[defaultBranch]) {
209
+ defaults[defaultBranch] = {
210
+ versionFormat: 'semantic',
211
+ tagFormat: 'v{version}',
212
+ syncFiles: [rootPackageJson],
213
+ environment: 'production',
214
+ bumpStrategy: 'semantic'
215
+ };
216
+ }
217
+ return defaults;
218
+ }
219
+ normalizeBranchRule(branchRule) {
220
+ const versionFormat = branchRule?.versionFormat || 'semantic';
221
+ return {
222
+ ...branchRule,
223
+ versionFormat,
224
+ tagFormat: branchRule?.tagFormat || 'v{version}',
225
+ syncFiles: branchRule?.syncFiles || [],
226
+ bumpStrategy: branchRule?.bumpStrategy || this.getDefaultBumpStrategy(versionFormat)
227
+ };
228
+ }
229
+ getDefaultBumpStrategy(versionFormat) {
230
+ switch (versionFormat) {
231
+ case 'semantic':
232
+ return 'semantic';
233
+ case 'feature':
234
+ return 'feature-branch';
235
+ case 'hotfix':
236
+ return 'hotfix';
237
+ case 'dev':
238
+ default:
239
+ return 'dev-build';
240
+ }
241
+ }
242
+ async buildBranchAwareVersion(releaseType, branch, versionFormat, branchConfig, explicitBuild) {
243
+ const currentVersion = await this.getCurrentVersion();
244
+ const baseVersion = this.coerceBaseVersion(currentVersion);
245
+ const shouldBumpSemantic = this.shouldBumpSemantic(releaseType, versionFormat, branchConfig.bumpStrategy);
246
+ const semanticVersion = shouldBumpSemantic ? this.incrementSemanticVersion(baseVersion, releaseType) : baseVersion;
247
+ if (versionFormat === 'semantic') {
248
+ return semanticVersion;
249
+ }
250
+ const buildNumber = explicitBuild ?? await this.resolveBuildNumber(branchConfig.syncFiles, versionFormat, branch);
251
+ return this.applyVersionFormat(versionFormat, semanticVersion, branch, buildNumber);
252
+ }
253
+ shouldBumpSemantic(releaseType, versionFormat, bumpStrategy) {
254
+ if (versionFormat === 'semantic') {
255
+ return true;
256
+ }
257
+ if (bumpStrategy === 'semantic') {
258
+ return true;
259
+ }
260
+ return releaseType !== 'patch';
261
+ }
262
+ coerceBaseVersion(version) {
263
+ const parsed = semver.parse(version) || semver.coerce(version);
264
+ if (!parsed) {
265
+ throw new Error(`Unable to parse semantic base version from "${version}".`);
266
+ }
267
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
268
+ }
269
+ incrementSemanticVersion(version, releaseType) {
270
+ const incremented = semver.inc(version, releaseType);
271
+ if (!incremented) {
272
+ throw new Error(`Invalid semantic version bump: ${releaseType} from ${version}`);
273
+ }
274
+ return incremented;
275
+ }
276
+ async resolveBuildNumber(syncFiles, versionFormat, branch) {
277
+ const candidateFiles = syncFiles.length > 0 ? syncFiles : this.getDefaultSyncFiles(versionFormat);
278
+ let highestBuild = 0;
279
+ for (const filePath of candidateFiles) {
280
+ const existingVersion = await this.readVersionFromFile(filePath);
281
+ if (!existingVersion) {
282
+ continue;
283
+ }
284
+ const existingBuild = this.extractBuildNumber(existingVersion, versionFormat, branch);
285
+ if (existingBuild !== null) {
286
+ highestBuild = Math.max(highestBuild, existingBuild);
287
+ }
288
+ }
289
+ const rootVersion = await this.getCurrentVersion();
290
+ const rootBuild = this.extractBuildNumber(rootVersion, versionFormat, branch);
291
+ if (rootBuild !== null) {
292
+ highestBuild = Math.max(highestBuild, rootBuild);
293
+ }
294
+ return highestBuild + 1;
295
+ }
296
+ getDefaultSyncFiles(versionFormat) {
297
+ if (versionFormat === 'semantic') {
298
+ return [this.config.rootPackageJson];
299
+ }
300
+ return ['version.development.json'];
301
+ }
302
+ applyVersionFormat(versionFormat, semanticVersion, branch, build) {
303
+ if (versionFormat === 'dev') {
304
+ return `${semanticVersion}-dev.${build}`;
305
+ }
306
+ if (versionFormat === 'feature' || versionFormat === 'hotfix') {
307
+ return `${semanticVersion}-${branch}.${build}`;
308
+ }
309
+ const normalizedFormat = versionFormat.includes('{branch}')
310
+ ? versionFormat.replace(/\{branch\}/g, branch)
311
+ : versionFormat;
312
+ return `${semanticVersion}-${normalizedFormat}.${build}`;
313
+ }
314
+ extractBuildNumber(version, versionFormat, branch) {
315
+ let match = null;
316
+ if (versionFormat === 'dev') {
317
+ match = version.match(/-dev\.(\d+)$/);
318
+ }
319
+ else if (versionFormat === 'feature') {
320
+ match = version.match(/-feature\/.+\.(\d+)$/);
321
+ }
322
+ else if (versionFormat === 'hotfix') {
323
+ match = version.match(/-hotfix\/.+\.(\d+)$/);
324
+ }
325
+ else {
326
+ const normalizedFormat = versionFormat.includes('{branch}')
327
+ ? versionFormat.replace(/\{branch\}/g, branch)
328
+ : versionFormat;
329
+ const escapedFormat = this.escapeRegex(normalizedFormat);
330
+ match = version.match(new RegExp(`-${escapedFormat}\\.(\\d+)$`));
331
+ }
332
+ if (!match) {
333
+ return null;
334
+ }
335
+ return Number.parseInt(match[1], 10);
336
+ }
337
+ async readVersionFromFile(filePath) {
338
+ if (!(await fs.pathExists(filePath))) {
339
+ return null;
340
+ }
341
+ const extension = path.extname(filePath).toLowerCase();
342
+ if (extension === '.json') {
343
+ try {
344
+ const jsonContent = await fs.readJson(filePath);
345
+ if (typeof jsonContent === 'string') {
346
+ return jsonContent;
347
+ }
348
+ if (jsonContent && typeof jsonContent === 'object') {
349
+ const version = jsonContent.version;
350
+ if (typeof version === 'string') {
351
+ return version;
352
+ }
353
+ }
354
+ }
355
+ catch {
356
+ return null;
357
+ }
358
+ return null;
359
+ }
360
+ const content = await fs.readFile(filePath, 'utf8');
361
+ const normalized = content.trim();
362
+ return normalized.length > 0 ? normalized : null;
363
+ }
364
+ async updateVersionFile(filePath, version) {
365
+ await fs.ensureDir(path.dirname(filePath));
366
+ const extension = path.extname(filePath).toLowerCase();
367
+ if (extension === '.json') {
368
+ let jsonContent = {};
369
+ if (await fs.pathExists(filePath)) {
370
+ try {
371
+ jsonContent = await fs.readJson(filePath);
372
+ }
373
+ catch {
374
+ jsonContent = {};
375
+ }
376
+ }
377
+ if (!jsonContent || typeof jsonContent !== 'object' || Array.isArray(jsonContent)) {
378
+ jsonContent = {};
379
+ }
380
+ const nextContent = jsonContent;
381
+ nextContent.version = version;
382
+ await fs.writeJson(filePath, nextContent, { spaces: 2 });
383
+ return;
384
+ }
385
+ await fs.writeFile(filePath, `${version}\n`, 'utf8');
386
+ }
387
+ matchesPattern(branch, pattern) {
388
+ if (!pattern.includes('*')) {
389
+ return branch === pattern;
390
+ }
391
+ const escapedPattern = pattern
392
+ .split('*')
393
+ .map((part) => this.escapeRegex(part))
394
+ .join('.*');
395
+ const regex = new RegExp(`^${escapedPattern}$`);
396
+ return regex.test(branch);
397
+ }
398
+ escapeRegex(value) {
399
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
400
+ }
401
+ renderTag(tagFormat, version) {
402
+ return tagFormat.includes('{version}')
403
+ ? tagFormat.replace(/\{version\}/g, version)
404
+ : tagFormat;
405
+ }
95
406
  }
96
407
  exports.VersionManager = VersionManager;
97
408
  //# sourceMappingURL=versioning.js.map
@@ -0,0 +1,51 @@
1
+ {
2
+ "rootPackageJson": "package.json",
3
+ "packages": [],
4
+ "changelogFile": "CHANGELOG.md",
5
+ "conventionalCommits": true,
6
+ "syncDependencies": false,
7
+ "ignorePackages": [],
8
+ "branchAwareness": {
9
+ "enabled": true,
10
+ "defaultBranch": "main",
11
+ "branches": {
12
+ "main": {
13
+ "versionFormat": "semantic",
14
+ "tagFormat": "v{version}",
15
+ "syncFiles": [
16
+ "package.json",
17
+ "version.production.json"
18
+ ],
19
+ "environment": "production",
20
+ "bumpStrategy": "semantic"
21
+ },
22
+ "develop": {
23
+ "versionFormat": "dev",
24
+ "tagFormat": "v{version}",
25
+ "syncFiles": [
26
+ "version.development.json"
27
+ ],
28
+ "environment": "development",
29
+ "bumpStrategy": "dev-build"
30
+ },
31
+ "feature/*": {
32
+ "versionFormat": "feature",
33
+ "tagFormat": "v{version}",
34
+ "syncFiles": [
35
+ "version.development.json"
36
+ ],
37
+ "environment": "development",
38
+ "bumpStrategy": "feature-branch"
39
+ },
40
+ "hotfix/*": {
41
+ "versionFormat": "hotfix",
42
+ "tagFormat": "v{version}",
43
+ "syncFiles": [
44
+ "version.development.json"
45
+ ],
46
+ "environment": "development",
47
+ "bumpStrategy": "hotfix"
48
+ }
49
+ }
50
+ }
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edcalderon/versioning",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "A comprehensive versioning and changelog management tool for monorepos",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -78,4 +78,4 @@
78
78
  "url": "git+https://github.com/edcalderon/my-second-brain.git",
79
79
  "directory": "packages/versioning"
80
80
  }
81
- }
81
+ }
@@ -5,6 +5,49 @@
5
5
  "conventionalCommits": true,
6
6
  "syncDependencies": false,
7
7
  "ignorePackages": [],
8
+ "branchAwareness": {
9
+ "enabled": false,
10
+ "defaultBranch": "main",
11
+ "branches": {
12
+ "main": {
13
+ "versionFormat": "semantic",
14
+ "tagFormat": "v{version}",
15
+ "syncFiles": [
16
+ "package.json",
17
+ "version.production.json"
18
+ ],
19
+ "environment": "production",
20
+ "bumpStrategy": "semantic"
21
+ },
22
+ "develop": {
23
+ "versionFormat": "dev",
24
+ "tagFormat": "v{version}",
25
+ "syncFiles": [
26
+ "version.development.json"
27
+ ],
28
+ "environment": "development",
29
+ "bumpStrategy": "dev-build"
30
+ },
31
+ "feature/*": {
32
+ "versionFormat": "feature",
33
+ "tagFormat": "v{version}",
34
+ "syncFiles": [
35
+ "version.development.json"
36
+ ],
37
+ "environment": "development",
38
+ "bumpStrategy": "feature-branch"
39
+ },
40
+ "hotfix/*": {
41
+ "versionFormat": "hotfix",
42
+ "tagFormat": "v{version}",
43
+ "syncFiles": [
44
+ "version.development.json"
45
+ ],
46
+ "environment": "development",
47
+ "bumpStrategy": "hotfix"
48
+ }
49
+ }
50
+ },
8
51
  "changelog": {
9
52
  "preset": "angular",
10
53
  "releaseCount": 0
@@ -12,4 +55,4 @@
12
55
  "sync": {
13
56
  "includeRoot": true
14
57
  }
15
- }
58
+ }
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=lifecycle-hooks.d.ts.map
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=npm-publish.d.ts.map
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=reentry-status-extension.d.ts.map
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=sample-extension.d.ts.map