tinysemver 2.0.2__tar.gz → 2.0.4__tar.gz
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.
- tinysemver-2.0.4/MANIFEST.in +3 -0
- {tinysemver-2.0.2/tinysemver.egg-info → tinysemver-2.0.4}/PKG-INFO +17 -63
- {tinysemver-2.0.2 → tinysemver-2.0.4}/README.md +16 -62
- {tinysemver-2.0.2 → tinysemver-2.0.4}/pyproject.toml +2 -1
- {tinysemver-2.0.2 → tinysemver-2.0.4/tinysemver.egg-info}/PKG-INFO +17 -63
- {tinysemver-2.0.2 → tinysemver-2.0.4}/tinysemver.egg-info/SOURCES.txt +2 -0
- tinysemver-2.0.4/tinysemver.py +665 -0
- {tinysemver-2.0.2 → tinysemver-2.0.4}/LICENSE +0 -0
- {tinysemver-2.0.2 → tinysemver-2.0.4}/setup.cfg +0 -0
- {tinysemver-2.0.2 → tinysemver-2.0.4}/tinysemver.egg-info/dependency_links.txt +0 -0
- {tinysemver-2.0.2 → tinysemver-2.0.4}/tinysemver.egg-info/entry_points.txt +0 -0
- {tinysemver-2.0.2 → tinysemver-2.0.4}/tinysemver.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinysemver
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: Tiny Semantic Versioning (SemVer) library, that doesn't depend on 300K lines of JavaScript
|
|
5
5
|
Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>, Guillaume de Rouville <31691250+grouville@users.noreply.github.com>
|
|
6
6
|
License: Apache License
|
|
@@ -224,9 +224,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
224
224
|
Description-Content-Type: text/markdown
|
|
225
225
|
License-File: LICENSE
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+

|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
|
|
230
230
|
In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
|
|
231
231
|
Here is how to integrate it into your project CI:
|
|
232
232
|
|
|
@@ -239,6 +239,9 @@ $ tinysemver --dry-run --verbose
|
|
|
239
239
|
|
|
240
240
|
The `--dry-run` flag will only print the next version without changing any files.
|
|
241
241
|
Great for pre-release CI pipelines.
|
|
242
|
+
|
|
243
|
+
## Usage Details for the Command Line Interface
|
|
244
|
+
|
|
242
245
|
If you need more control over the default specification, here are more options you can run against the files in this repository:
|
|
243
246
|
|
|
244
247
|
```sh
|
|
@@ -301,7 +304,7 @@ Alternatively, you can just ask for `--help`:
|
|
|
301
304
|
$ tinysemver --help
|
|
302
305
|
```
|
|
303
306
|
|
|
304
|
-
## GitHub CI Action
|
|
307
|
+
## Usage Details for the GitHub CI Action
|
|
305
308
|
|
|
306
309
|
TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
|
|
307
310
|
Assuming the differences between YAML and shell notation, some arguments are passed in a different form, like `--update-version-in`.
|
|
@@ -387,11 +390,11 @@ Also keep in mind:
|
|
|
387
390
|
|
|
388
391
|
For more information on CI configurations and pushing changes in GitHub Actions, see the [semantic-release GitHub Actions guide](https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#pushing-packagejson-changes-to-a-master-branch).
|
|
389
392
|
|
|
390
|
-
## Why?
|
|
393
|
+
## Why Create Another SemVer Tool?
|
|
391
394
|
|
|
392
395
|
In the past I was using [semantic-release](https://github.com/semantic-release/semantic-release) for my 10+ projects.
|
|
393
396
|
At some point, a breaking change in the dependencies broke all my projects CI pipelines for a month, affecting dozens of tech companies using those libraries.
|
|
394
|
-
I felt miserable trying to trace the issue and reluctant to go through
|
|
397
|
+
I felt miserable trying to trace the issue and reluctant to go through __363K lines of low-quality JavaScript code__ to find the bug.
|
|
395
398
|
Yes, it's 363K lines of code:
|
|
396
399
|
|
|
397
400
|
```sh
|
|
@@ -406,26 +409,7 @@ Language files blank comment code
|
|
|
406
409
|
--------------------------------------------------------------------------------
|
|
407
410
|
JavaScript 4902 48080 81205 363424
|
|
408
411
|
TypeScript 732 7008 73034 79367
|
|
409
|
-
|
|
410
|
-
JSON 599 58 0 64808
|
|
411
|
-
HTML 86 1821 0 25365
|
|
412
|
-
Python 57 4985 9193 23704
|
|
413
|
-
CSS 97 1360 739 6346
|
|
414
|
-
YAML 73 79 51 1198
|
|
415
|
-
CoffeeScript 18 193 16 1122
|
|
416
|
-
EJS 1 67 0 521
|
|
417
|
-
Lua 22 95 29 434
|
|
418
|
-
Handlebars 11 30 0 188
|
|
419
|
-
C# 1 55 9 186
|
|
420
|
-
Bourne Shell 7 30 11 168
|
|
421
|
-
Bourne Again Shell 2 22 24 84
|
|
422
|
-
TOML 1 8 31 80
|
|
423
|
-
make 3 17 11 57
|
|
424
|
-
PowerShell 2 12 4 48
|
|
425
|
-
DOS Batch 5 9 0 42
|
|
426
|
-
Fish Shell 1 5 14 21
|
|
427
|
-
C++ 2 12 19 20
|
|
428
|
-
Nix 1 1 0 19
|
|
412
|
+
... ... ... ... ...
|
|
429
413
|
--------------------------------------------------------------------------------
|
|
430
414
|
SUM: 7256 90782 164390 634071
|
|
431
415
|
--------------------------------------------------------------------------------
|
|
@@ -435,20 +419,18 @@ Here is the `cloc` output for `tinysemver`:
|
|
|
435
419
|
|
|
436
420
|
```sh
|
|
437
421
|
$ tinysemver$ cloc .
|
|
438
|
-
|
|
439
|
-
|
|
422
|
+
17 text files.
|
|
423
|
+
13 unique files.
|
|
440
424
|
6 files ignored.
|
|
441
425
|
|
|
442
426
|
github.com/AlDanial/cloc v 1.96 T=0.01 s (660.7 files/s, 44267.6 lines/s)
|
|
443
427
|
-------------------------------------------------------------------------------
|
|
444
428
|
Language files blank comment code
|
|
445
429
|
-------------------------------------------------------------------------------
|
|
446
|
-
Python 1
|
|
447
|
-
|
|
448
|
-
TOML 1 6 2 33
|
|
449
|
-
Text 3 1 0 10
|
|
430
|
+
Python 1 79 93 493
|
|
431
|
+
... ... ... ... ...
|
|
450
432
|
-------------------------------------------------------------------------------
|
|
451
|
-
SUM:
|
|
433
|
+
SUM: 13 227 107 1124
|
|
452
434
|
-------------------------------------------------------------------------------
|
|
453
435
|
```
|
|
454
436
|
|
|
@@ -458,41 +440,13 @@ SUM: 6 65 29 308
|
|
|
458
440
|
- Pre-release versions, like `1.2.3-alpha.1`. Not needed for most projects.
|
|
459
441
|
- GenAI.
|
|
460
442
|
|
|
461
|
-
For reference, according to SemVer 2.0, all [following versions](https://regex101.com/r/Ly7O1x/3/) are valid:
|
|
462
|
-
|
|
463
|
-
```
|
|
464
|
-
1.1.2-prerelease+meta
|
|
465
|
-
1.1.2+meta
|
|
466
|
-
1.1.2+meta-valid
|
|
467
|
-
1.0.0-alpha
|
|
468
|
-
1.0.0-beta
|
|
469
|
-
1.0.0-alpha.beta.1
|
|
470
|
-
1.0.0-alpha.1
|
|
471
|
-
1.0.0-alpha0.valid
|
|
472
|
-
1.0.0-alpha.0valid
|
|
473
|
-
1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
|
|
474
|
-
1.0.0-rc.1+build.1
|
|
475
|
-
2.0.0-rc.1+build.123
|
|
476
|
-
1.2.3-beta
|
|
477
|
-
10.2.3-DEV-SNAPSHOT
|
|
478
|
-
1.2.3-SNAPSHOT-123
|
|
479
|
-
2.0.0+build.1848
|
|
480
|
-
2.0.1-alpha.1227
|
|
481
|
-
1.0.0-alpha+beta
|
|
482
|
-
1.2.3----RC-SNAPSHOT.12.9.1--.12+788
|
|
483
|
-
1.2.3----R-S.12.9.1--.12+meta
|
|
484
|
-
1.2.3----RC-SNAPSHOT.12.9.1--.12
|
|
485
|
-
1.0.0+0.build.1-rc.10000aaa-kk-0.1
|
|
486
|
-
1.0.0-0A.is.legal
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
Probably very useful for 2-3 projects, I didn't need to support any of them yet.
|
|
443
|
+
> For reference, according to SemVer 2.0, all [following versions](https://regex101.com/r/Ly7O1x/3/) are valid: `1.1.2-prerelease+meta`, `1.1.2+meta`, `1.1.2+meta-valid`, `1.0.0-alpha`, `1.0.0-beta`, `1.0.0-alpha.beta.1`, `1.0.0-alpha.1`, `1.0.0-alpha0.valid`, `1.0.0-alpha.0valid`, `1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay`, `1.0.0-rc.1+build.1`, `2.0.0-rc.1+build.123`, `1.2.3-beta`, `10.2.3-DEV-SNAPSHOT`, `1.2.3-SNAPSHOT-123`, `2.0.0+build.1848`, `2.0.1-alpha.1227`, `1.0.0-alpha+beta`, `1.2.3----RC-SNAPSHOT.12.9.1--.12+788`, `1.2.3----R-S.12.9.1--.12+meta`, `1.2.3----RC-SNAPSHOT.12.9.1--.12`, `1.0.0+0.build.1-rc.10000aaa-kk-0.1`, `1.0.0-0A.is.legal`.
|
|
490
444
|
|
|
491
445
|
## Examples
|
|
492
446
|
|
|
493
447
|
Assembling RegEx queries can be hard.
|
|
494
448
|
Luckily, there aren't too many files to update in most projects.
|
|
495
|
-
Below is an example of a pipeline for the USearch project, that has bindings to 10 programming languages.
|
|
449
|
+
Below is an example of a pipeline for the [USearch](https://github.com/unum-cloud/usearch) project, that has bindings to 10 programming languages.
|
|
496
450
|
Feel free to add other sources and examples.
|
|
497
451
|
|
|
498
452
|
```sh
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
|
|
4
4
|
In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
|
|
5
5
|
Here is how to integrate it into your project CI:
|
|
6
6
|
|
|
@@ -13,6 +13,9 @@ $ tinysemver --dry-run --verbose
|
|
|
13
13
|
|
|
14
14
|
The `--dry-run` flag will only print the next version without changing any files.
|
|
15
15
|
Great for pre-release CI pipelines.
|
|
16
|
+
|
|
17
|
+
## Usage Details for the Command Line Interface
|
|
18
|
+
|
|
16
19
|
If you need more control over the default specification, here are more options you can run against the files in this repository:
|
|
17
20
|
|
|
18
21
|
```sh
|
|
@@ -75,7 +78,7 @@ Alternatively, you can just ask for `--help`:
|
|
|
75
78
|
$ tinysemver --help
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
## GitHub CI Action
|
|
81
|
+
## Usage Details for the GitHub CI Action
|
|
79
82
|
|
|
80
83
|
TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
|
|
81
84
|
Assuming the differences between YAML and shell notation, some arguments are passed in a different form, like `--update-version-in`.
|
|
@@ -161,11 +164,11 @@ Also keep in mind:
|
|
|
161
164
|
|
|
162
165
|
For more information on CI configurations and pushing changes in GitHub Actions, see the [semantic-release GitHub Actions guide](https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#pushing-packagejson-changes-to-a-master-branch).
|
|
163
166
|
|
|
164
|
-
## Why?
|
|
167
|
+
## Why Create Another SemVer Tool?
|
|
165
168
|
|
|
166
169
|
In the past I was using [semantic-release](https://github.com/semantic-release/semantic-release) for my 10+ projects.
|
|
167
170
|
At some point, a breaking change in the dependencies broke all my projects CI pipelines for a month, affecting dozens of tech companies using those libraries.
|
|
168
|
-
I felt miserable trying to trace the issue and reluctant to go through
|
|
171
|
+
I felt miserable trying to trace the issue and reluctant to go through __363K lines of low-quality JavaScript code__ to find the bug.
|
|
169
172
|
Yes, it's 363K lines of code:
|
|
170
173
|
|
|
171
174
|
```sh
|
|
@@ -180,26 +183,7 @@ Language files blank comment code
|
|
|
180
183
|
--------------------------------------------------------------------------------
|
|
181
184
|
JavaScript 4902 48080 81205 363424
|
|
182
185
|
TypeScript 732 7008 73034 79367
|
|
183
|
-
|
|
184
|
-
JSON 599 58 0 64808
|
|
185
|
-
HTML 86 1821 0 25365
|
|
186
|
-
Python 57 4985 9193 23704
|
|
187
|
-
CSS 97 1360 739 6346
|
|
188
|
-
YAML 73 79 51 1198
|
|
189
|
-
CoffeeScript 18 193 16 1122
|
|
190
|
-
EJS 1 67 0 521
|
|
191
|
-
Lua 22 95 29 434
|
|
192
|
-
Handlebars 11 30 0 188
|
|
193
|
-
C# 1 55 9 186
|
|
194
|
-
Bourne Shell 7 30 11 168
|
|
195
|
-
Bourne Again Shell 2 22 24 84
|
|
196
|
-
TOML 1 8 31 80
|
|
197
|
-
make 3 17 11 57
|
|
198
|
-
PowerShell 2 12 4 48
|
|
199
|
-
DOS Batch 5 9 0 42
|
|
200
|
-
Fish Shell 1 5 14 21
|
|
201
|
-
C++ 2 12 19 20
|
|
202
|
-
Nix 1 1 0 19
|
|
186
|
+
... ... ... ... ...
|
|
203
187
|
--------------------------------------------------------------------------------
|
|
204
188
|
SUM: 7256 90782 164390 634071
|
|
205
189
|
--------------------------------------------------------------------------------
|
|
@@ -209,20 +193,18 @@ Here is the `cloc` output for `tinysemver`:
|
|
|
209
193
|
|
|
210
194
|
```sh
|
|
211
195
|
$ tinysemver$ cloc .
|
|
212
|
-
|
|
213
|
-
|
|
196
|
+
17 text files.
|
|
197
|
+
13 unique files.
|
|
214
198
|
6 files ignored.
|
|
215
199
|
|
|
216
200
|
github.com/AlDanial/cloc v 1.96 T=0.01 s (660.7 files/s, 44267.6 lines/s)
|
|
217
201
|
-------------------------------------------------------------------------------
|
|
218
202
|
Language files blank comment code
|
|
219
203
|
-------------------------------------------------------------------------------
|
|
220
|
-
Python 1
|
|
221
|
-
|
|
222
|
-
TOML 1 6 2 33
|
|
223
|
-
Text 3 1 0 10
|
|
204
|
+
Python 1 79 93 493
|
|
205
|
+
... ... ... ... ...
|
|
224
206
|
-------------------------------------------------------------------------------
|
|
225
|
-
SUM:
|
|
207
|
+
SUM: 13 227 107 1124
|
|
226
208
|
-------------------------------------------------------------------------------
|
|
227
209
|
```
|
|
228
210
|
|
|
@@ -232,41 +214,13 @@ SUM: 6 65 29 308
|
|
|
232
214
|
- Pre-release versions, like `1.2.3-alpha.1`. Not needed for most projects.
|
|
233
215
|
- GenAI.
|
|
234
216
|
|
|
235
|
-
For reference, according to SemVer 2.0, all [following versions](https://regex101.com/r/Ly7O1x/3/) are valid:
|
|
236
|
-
|
|
237
|
-
```
|
|
238
|
-
1.1.2-prerelease+meta
|
|
239
|
-
1.1.2+meta
|
|
240
|
-
1.1.2+meta-valid
|
|
241
|
-
1.0.0-alpha
|
|
242
|
-
1.0.0-beta
|
|
243
|
-
1.0.0-alpha.beta.1
|
|
244
|
-
1.0.0-alpha.1
|
|
245
|
-
1.0.0-alpha0.valid
|
|
246
|
-
1.0.0-alpha.0valid
|
|
247
|
-
1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
|
|
248
|
-
1.0.0-rc.1+build.1
|
|
249
|
-
2.0.0-rc.1+build.123
|
|
250
|
-
1.2.3-beta
|
|
251
|
-
10.2.3-DEV-SNAPSHOT
|
|
252
|
-
1.2.3-SNAPSHOT-123
|
|
253
|
-
2.0.0+build.1848
|
|
254
|
-
2.0.1-alpha.1227
|
|
255
|
-
1.0.0-alpha+beta
|
|
256
|
-
1.2.3----RC-SNAPSHOT.12.9.1--.12+788
|
|
257
|
-
1.2.3----R-S.12.9.1--.12+meta
|
|
258
|
-
1.2.3----RC-SNAPSHOT.12.9.1--.12
|
|
259
|
-
1.0.0+0.build.1-rc.10000aaa-kk-0.1
|
|
260
|
-
1.0.0-0A.is.legal
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Probably very useful for 2-3 projects, I didn't need to support any of them yet.
|
|
217
|
+
> For reference, according to SemVer 2.0, all [following versions](https://regex101.com/r/Ly7O1x/3/) are valid: `1.1.2-prerelease+meta`, `1.1.2+meta`, `1.1.2+meta-valid`, `1.0.0-alpha`, `1.0.0-beta`, `1.0.0-alpha.beta.1`, `1.0.0-alpha.1`, `1.0.0-alpha0.valid`, `1.0.0-alpha.0valid`, `1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay`, `1.0.0-rc.1+build.1`, `2.0.0-rc.1+build.123`, `1.2.3-beta`, `10.2.3-DEV-SNAPSHOT`, `1.2.3-SNAPSHOT-123`, `2.0.0+build.1848`, `2.0.1-alpha.1227`, `1.0.0-alpha+beta`, `1.2.3----RC-SNAPSHOT.12.9.1--.12+788`, `1.2.3----R-S.12.9.1--.12+meta`, `1.2.3----RC-SNAPSHOT.12.9.1--.12`, `1.0.0+0.build.1-rc.10000aaa-kk-0.1`, `1.0.0-0A.is.legal`.
|
|
264
218
|
|
|
265
219
|
## Examples
|
|
266
220
|
|
|
267
221
|
Assembling RegEx queries can be hard.
|
|
268
222
|
Luckily, there aren't too many files to update in most projects.
|
|
269
|
-
Below is an example of a pipeline for the USearch project, that has bindings to 10 programming languages.
|
|
223
|
+
Below is an example of a pipeline for the [USearch](https://github.com/unum-cloud/usearch) project, that has bindings to 10 programming languages.
|
|
270
224
|
Feel free to add other sources and examples.
|
|
271
225
|
|
|
272
226
|
```sh
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tinysemver"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.4"
|
|
8
8
|
description = "Tiny Semantic Versioning (SemVer) library, that doesn't depend on 300K lines of JavaScript"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -38,6 +38,7 @@ Changelog = "https://github.com/ashvardanian/tinysemver/blob/main/CHANGELOG.md"
|
|
|
38
38
|
|
|
39
39
|
[tool.setuptools]
|
|
40
40
|
package-dir = { "" = "." }
|
|
41
|
+
include-package-data = true
|
|
41
42
|
|
|
42
43
|
[tool.setuptools.packages.find]
|
|
43
44
|
where = ["."]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinysemver
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: Tiny Semantic Versioning (SemVer) library, that doesn't depend on 300K lines of JavaScript
|
|
5
5
|
Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>, Guillaume de Rouville <31691250+grouville@users.noreply.github.com>
|
|
6
6
|
License: Apache License
|
|
@@ -224,9 +224,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
224
224
|
Description-Content-Type: text/markdown
|
|
225
225
|
License-File: LICENSE
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+

|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
|
|
230
230
|
In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
|
|
231
231
|
Here is how to integrate it into your project CI:
|
|
232
232
|
|
|
@@ -239,6 +239,9 @@ $ tinysemver --dry-run --verbose
|
|
|
239
239
|
|
|
240
240
|
The `--dry-run` flag will only print the next version without changing any files.
|
|
241
241
|
Great for pre-release CI pipelines.
|
|
242
|
+
|
|
243
|
+
## Usage Details for the Command Line Interface
|
|
244
|
+
|
|
242
245
|
If you need more control over the default specification, here are more options you can run against the files in this repository:
|
|
243
246
|
|
|
244
247
|
```sh
|
|
@@ -301,7 +304,7 @@ Alternatively, you can just ask for `--help`:
|
|
|
301
304
|
$ tinysemver --help
|
|
302
305
|
```
|
|
303
306
|
|
|
304
|
-
## GitHub CI Action
|
|
307
|
+
## Usage Details for the GitHub CI Action
|
|
305
308
|
|
|
306
309
|
TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
|
|
307
310
|
Assuming the differences between YAML and shell notation, some arguments are passed in a different form, like `--update-version-in`.
|
|
@@ -387,11 +390,11 @@ Also keep in mind:
|
|
|
387
390
|
|
|
388
391
|
For more information on CI configurations and pushing changes in GitHub Actions, see the [semantic-release GitHub Actions guide](https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#pushing-packagejson-changes-to-a-master-branch).
|
|
389
392
|
|
|
390
|
-
## Why?
|
|
393
|
+
## Why Create Another SemVer Tool?
|
|
391
394
|
|
|
392
395
|
In the past I was using [semantic-release](https://github.com/semantic-release/semantic-release) for my 10+ projects.
|
|
393
396
|
At some point, a breaking change in the dependencies broke all my projects CI pipelines for a month, affecting dozens of tech companies using those libraries.
|
|
394
|
-
I felt miserable trying to trace the issue and reluctant to go through
|
|
397
|
+
I felt miserable trying to trace the issue and reluctant to go through __363K lines of low-quality JavaScript code__ to find the bug.
|
|
395
398
|
Yes, it's 363K lines of code:
|
|
396
399
|
|
|
397
400
|
```sh
|
|
@@ -406,26 +409,7 @@ Language files blank comment code
|
|
|
406
409
|
--------------------------------------------------------------------------------
|
|
407
410
|
JavaScript 4902 48080 81205 363424
|
|
408
411
|
TypeScript 732 7008 73034 79367
|
|
409
|
-
|
|
410
|
-
JSON 599 58 0 64808
|
|
411
|
-
HTML 86 1821 0 25365
|
|
412
|
-
Python 57 4985 9193 23704
|
|
413
|
-
CSS 97 1360 739 6346
|
|
414
|
-
YAML 73 79 51 1198
|
|
415
|
-
CoffeeScript 18 193 16 1122
|
|
416
|
-
EJS 1 67 0 521
|
|
417
|
-
Lua 22 95 29 434
|
|
418
|
-
Handlebars 11 30 0 188
|
|
419
|
-
C# 1 55 9 186
|
|
420
|
-
Bourne Shell 7 30 11 168
|
|
421
|
-
Bourne Again Shell 2 22 24 84
|
|
422
|
-
TOML 1 8 31 80
|
|
423
|
-
make 3 17 11 57
|
|
424
|
-
PowerShell 2 12 4 48
|
|
425
|
-
DOS Batch 5 9 0 42
|
|
426
|
-
Fish Shell 1 5 14 21
|
|
427
|
-
C++ 2 12 19 20
|
|
428
|
-
Nix 1 1 0 19
|
|
412
|
+
... ... ... ... ...
|
|
429
413
|
--------------------------------------------------------------------------------
|
|
430
414
|
SUM: 7256 90782 164390 634071
|
|
431
415
|
--------------------------------------------------------------------------------
|
|
@@ -435,20 +419,18 @@ Here is the `cloc` output for `tinysemver`:
|
|
|
435
419
|
|
|
436
420
|
```sh
|
|
437
421
|
$ tinysemver$ cloc .
|
|
438
|
-
|
|
439
|
-
|
|
422
|
+
17 text files.
|
|
423
|
+
13 unique files.
|
|
440
424
|
6 files ignored.
|
|
441
425
|
|
|
442
426
|
github.com/AlDanial/cloc v 1.96 T=0.01 s (660.7 files/s, 44267.6 lines/s)
|
|
443
427
|
-------------------------------------------------------------------------------
|
|
444
428
|
Language files blank comment code
|
|
445
429
|
-------------------------------------------------------------------------------
|
|
446
|
-
Python 1
|
|
447
|
-
|
|
448
|
-
TOML 1 6 2 33
|
|
449
|
-
Text 3 1 0 10
|
|
430
|
+
Python 1 79 93 493
|
|
431
|
+
... ... ... ... ...
|
|
450
432
|
-------------------------------------------------------------------------------
|
|
451
|
-
SUM:
|
|
433
|
+
SUM: 13 227 107 1124
|
|
452
434
|
-------------------------------------------------------------------------------
|
|
453
435
|
```
|
|
454
436
|
|
|
@@ -458,41 +440,13 @@ SUM: 6 65 29 308
|
|
|
458
440
|
- Pre-release versions, like `1.2.3-alpha.1`. Not needed for most projects.
|
|
459
441
|
- GenAI.
|
|
460
442
|
|
|
461
|
-
For reference, according to SemVer 2.0, all [following versions](https://regex101.com/r/Ly7O1x/3/) are valid:
|
|
462
|
-
|
|
463
|
-
```
|
|
464
|
-
1.1.2-prerelease+meta
|
|
465
|
-
1.1.2+meta
|
|
466
|
-
1.1.2+meta-valid
|
|
467
|
-
1.0.0-alpha
|
|
468
|
-
1.0.0-beta
|
|
469
|
-
1.0.0-alpha.beta.1
|
|
470
|
-
1.0.0-alpha.1
|
|
471
|
-
1.0.0-alpha0.valid
|
|
472
|
-
1.0.0-alpha.0valid
|
|
473
|
-
1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
|
|
474
|
-
1.0.0-rc.1+build.1
|
|
475
|
-
2.0.0-rc.1+build.123
|
|
476
|
-
1.2.3-beta
|
|
477
|
-
10.2.3-DEV-SNAPSHOT
|
|
478
|
-
1.2.3-SNAPSHOT-123
|
|
479
|
-
2.0.0+build.1848
|
|
480
|
-
2.0.1-alpha.1227
|
|
481
|
-
1.0.0-alpha+beta
|
|
482
|
-
1.2.3----RC-SNAPSHOT.12.9.1--.12+788
|
|
483
|
-
1.2.3----R-S.12.9.1--.12+meta
|
|
484
|
-
1.2.3----RC-SNAPSHOT.12.9.1--.12
|
|
485
|
-
1.0.0+0.build.1-rc.10000aaa-kk-0.1
|
|
486
|
-
1.0.0-0A.is.legal
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
Probably very useful for 2-3 projects, I didn't need to support any of them yet.
|
|
443
|
+
> For reference, according to SemVer 2.0, all [following versions](https://regex101.com/r/Ly7O1x/3/) are valid: `1.1.2-prerelease+meta`, `1.1.2+meta`, `1.1.2+meta-valid`, `1.0.0-alpha`, `1.0.0-beta`, `1.0.0-alpha.beta.1`, `1.0.0-alpha.1`, `1.0.0-alpha0.valid`, `1.0.0-alpha.0valid`, `1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay`, `1.0.0-rc.1+build.1`, `2.0.0-rc.1+build.123`, `1.2.3-beta`, `10.2.3-DEV-SNAPSHOT`, `1.2.3-SNAPSHOT-123`, `2.0.0+build.1848`, `2.0.1-alpha.1227`, `1.0.0-alpha+beta`, `1.2.3----RC-SNAPSHOT.12.9.1--.12+788`, `1.2.3----R-S.12.9.1--.12+meta`, `1.2.3----RC-SNAPSHOT.12.9.1--.12`, `1.0.0+0.build.1-rc.10000aaa-kk-0.1`, `1.0.0-0A.is.legal`.
|
|
490
444
|
|
|
491
445
|
## Examples
|
|
492
446
|
|
|
493
447
|
Assembling RegEx queries can be hard.
|
|
494
448
|
Luckily, there aren't too many files to update in most projects.
|
|
495
|
-
Below is an example of a pipeline for the USearch project, that has bindings to 10 programming languages.
|
|
449
|
+
Below is an example of a pipeline for the [USearch](https://github.com/unum-cloud/usearch) project, that has bindings to 10 programming languages.
|
|
496
450
|
Feel free to add other sources and examples.
|
|
497
451
|
|
|
498
452
|
```sh
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TinySemVer is a tiny Python script that helps you manage your project's versioning.
|
|
4
|
+
|
|
5
|
+
This module traces the commit history of a Git repository after the last tag, and based
|
|
6
|
+
on the commit messages, it determines the type of version bump (major, minor, or patch).
|
|
7
|
+
On "dry-runs" it simply reports the new version number, but on actual runs, it creates a
|
|
8
|
+
a new tag, and optionally pushes it to the repository, also updating the "version file",
|
|
9
|
+
the "changelog file", and all other files and RegEx patterns passed as CLI arguments.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
|
|
13
|
+
tinysemver --dry-run --verbose \
|
|
14
|
+
--major-verbs 'breaking,break,major' \
|
|
15
|
+
--minor-verbs 'feature,minor,add,new' \
|
|
16
|
+
--patch-verbs 'fix,patch,bug,improve,docs,make' \
|
|
17
|
+
--changelog-file 'CHANGELOG.md' \
|
|
18
|
+
--version-file 'VERSION' \
|
|
19
|
+
--update-version-in 'package.json' '"version": "(.*)"' \
|
|
20
|
+
--update-version-in 'CITATION.cff' '^version: (.*)' \
|
|
21
|
+
--update-major-version-in 'include/stringzilla/stringzilla.h' '^#define STRINGZILLA_VERSION_MAJOR (.*)' \
|
|
22
|
+
--update-minor-version-in 'include/stringzilla/stringzilla.h' '^#define STRINGZILLA_VERSION_MINOR (.*)' \
|
|
23
|
+
--update-patch-version-in 'include/stringzilla/stringzilla.h' '^#define STRINGZILLA_VERSION_PATCH (.*)'
|
|
24
|
+
|
|
25
|
+
Multiple "--update-version-in" arguments can be passed to update multiple files.
|
|
26
|
+
Each of them must be followed by a file path and a RegEx pattern.
|
|
27
|
+
The RegEx pattern must contain a capturing group to extract the version number,
|
|
28
|
+
that will be replaced by the new version number.
|
|
29
|
+
|
|
30
|
+
By default, the following conventions are used:
|
|
31
|
+
|
|
32
|
+
- The repository must be a Git repository.
|
|
33
|
+
- It must contain a "VERSION" and "CHANGELOG.md" files in the root directory.
|
|
34
|
+
- The changelog is append-only - sorted in chronological order.
|
|
35
|
+
|
|
36
|
+
Setting up a new project:
|
|
37
|
+
|
|
38
|
+
$ git init # Initialize a new Git repository
|
|
39
|
+
$ echo "0.1.0" > VERSION # Create a version file
|
|
40
|
+
$ echo "# Changelog" > CHANGELOG.md # Create a changelog file
|
|
41
|
+
$ git add VERSION CHANGELOG.md # Add the files to the repository
|
|
42
|
+
$ git commit -m "Add: Initial files" # Create the first commit
|
|
43
|
+
$ git tag v0.1.0 # Create the first tag
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
import argparse
|
|
47
|
+
import subprocess
|
|
48
|
+
import re
|
|
49
|
+
import os
|
|
50
|
+
from typing import List, Tuple, Literal, Union, Optional
|
|
51
|
+
from datetime import datetime
|
|
52
|
+
import traceback
|
|
53
|
+
|
|
54
|
+
SemVer = Tuple[int, int, int]
|
|
55
|
+
BumpType = Literal["major", "minor", "patch"]
|
|
56
|
+
PathLike = Union[str, os.PathLike]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_last_tag(repository_path: PathLike) -> str:
|
|
60
|
+
"""Retrieve the last Git tag name from the repository."""
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
63
|
+
stdout=subprocess.PIPE,
|
|
64
|
+
stderr=subprocess.PIPE,
|
|
65
|
+
cwd=repository_path,
|
|
66
|
+
)
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
return None
|
|
69
|
+
return result.stdout.strip().decode("utf-8")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_commits_since_tag(repository_path: PathLike, tag: str) -> Tuple[List[str], List[str]]:
|
|
73
|
+
"""Get commit hashes and messages since the specified Git tag."""
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["git", "log", f"{tag}..HEAD", "--no-merges", "--pretty=format:%h:%s"],
|
|
76
|
+
stdout=subprocess.PIPE,
|
|
77
|
+
stderr=subprocess.PIPE,
|
|
78
|
+
cwd=repository_path,
|
|
79
|
+
)
|
|
80
|
+
if result.returncode != 0:
|
|
81
|
+
return [], []
|
|
82
|
+
|
|
83
|
+
lines = result.stdout.strip().decode("utf-8").split("\n")
|
|
84
|
+
hashes = [line.partition(":")[0] for line in lines if line.strip()]
|
|
85
|
+
commits = [line.partition(":")[2] for line in lines if line.strip()]
|
|
86
|
+
return hashes, commits
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_version(tag: str) -> SemVer:
|
|
90
|
+
"""Parse a version string from a Git tag name, assuming it looks like `v1.2.3`."""
|
|
91
|
+
match = re.match(r"v?(\d+)\.(\d+)\.(\d+)", tag)
|
|
92
|
+
if not match:
|
|
93
|
+
raise ValueError(f"Tag {tag} is not in a recognized version format")
|
|
94
|
+
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def commit_starts_with_verb(commit: str, verb: str) -> bool:
|
|
98
|
+
"""Check if a commit message starts with a specific verb, ignoring capitalization and punctuation marks."""
|
|
99
|
+
if commit.lower().startswith(verb):
|
|
100
|
+
if len(commit) == len(verb) or commit[len(verb)].isspace() or commit[len(verb)] == ":":
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def normalize_verbs(verbs: Union[str, List[str]], defaults: List[str]) -> List[str]:
|
|
106
|
+
"""Normalize a list of verbs, allowing string input to be split by commas."""
|
|
107
|
+
if isinstance(verbs, str):
|
|
108
|
+
return [verb.strip("\"'") for verb in verbs.split(",")]
|
|
109
|
+
elif verbs is None:
|
|
110
|
+
return defaults
|
|
111
|
+
else:
|
|
112
|
+
return verbs
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def group_commits(
|
|
116
|
+
commits: List[str],
|
|
117
|
+
major_verbs: List[str],
|
|
118
|
+
minor_verbs: List[str],
|
|
119
|
+
patch_verbs: List[str],
|
|
120
|
+
) -> Tuple[List[str], List[str], List[str]]:
|
|
121
|
+
"""Group commits into major, minor, and patch categories based on keywords to simplify future `BumpType` resolution."""
|
|
122
|
+
major_commits = []
|
|
123
|
+
minor_commits = []
|
|
124
|
+
patch_commits = []
|
|
125
|
+
|
|
126
|
+
for commit in commits:
|
|
127
|
+
if any(commit_starts_with_verb(commit, verb) for verb in major_verbs):
|
|
128
|
+
major_commits.append(commit)
|
|
129
|
+
if any(commit_starts_with_verb(commit, verb) for verb in minor_verbs):
|
|
130
|
+
minor_commits.append(commit)
|
|
131
|
+
if any(commit_starts_with_verb(commit, verb) for verb in patch_verbs):
|
|
132
|
+
patch_commits.append(commit)
|
|
133
|
+
|
|
134
|
+
return major_commits, minor_commits, patch_commits
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def bump_version(version: SemVer, bump_type: BumpType) -> SemVer:
|
|
138
|
+
"""Bump the version based on the specified bump type (major, minor, patch)."""
|
|
139
|
+
major, minor, patch = version
|
|
140
|
+
if bump_type == "major":
|
|
141
|
+
return major + 1, 0, 0
|
|
142
|
+
elif bump_type == "minor":
|
|
143
|
+
return major, minor + 1, 0
|
|
144
|
+
elif bump_type == "patch":
|
|
145
|
+
return major, minor, patch + 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_tag(
|
|
149
|
+
*, # enforce keyword-only arguments
|
|
150
|
+
repository_path: PathLike,
|
|
151
|
+
version: SemVer,
|
|
152
|
+
user_name: str,
|
|
153
|
+
user_email: str,
|
|
154
|
+
default_branch: str = "main",
|
|
155
|
+
github_token: Optional[str] = None,
|
|
156
|
+
github_repository: Optional[str] = None,
|
|
157
|
+
push: bool = False,
|
|
158
|
+
create_release: bool = False,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Create a new Git tag and optionally push it to a remote GitHub repository."""
|
|
161
|
+
|
|
162
|
+
tag = f"v{version[0]}.{version[1]}.{version[2]}"
|
|
163
|
+
env = os.environ.copy()
|
|
164
|
+
env["GIT_COMMITTER_NAME"] = user_name
|
|
165
|
+
env["GIT_COMMITTER_EMAIL"] = user_email
|
|
166
|
+
env["GIT_AUTHOR_NAME"] = user_name
|
|
167
|
+
env["GIT_AUTHOR_EMAIL"] = user_email
|
|
168
|
+
env["GITHUB_TOKEN"] = github_token
|
|
169
|
+
message = f"Release: {tag} [skip ci]"
|
|
170
|
+
subprocess.run(["git", "add", "-A"], cwd=repository_path)
|
|
171
|
+
subprocess.run(["git", "commit", "-m", message], cwd=repository_path, env=env)
|
|
172
|
+
|
|
173
|
+
# Get the SHA of the new commit
|
|
174
|
+
new_commit_sha = subprocess.run(
|
|
175
|
+
["git", "rev-parse", "HEAD"],
|
|
176
|
+
cwd=repository_path,
|
|
177
|
+
capture_output=True,
|
|
178
|
+
text=True,
|
|
179
|
+
check=True,
|
|
180
|
+
).stdout.strip()
|
|
181
|
+
|
|
182
|
+
subprocess.run(["git", "tag", "-a", tag, "-m", message, new_commit_sha], cwd=repository_path, env=env)
|
|
183
|
+
print(f"Created new tag: {tag}")
|
|
184
|
+
if push:
|
|
185
|
+
url = None
|
|
186
|
+
if github_token and github_repository:
|
|
187
|
+
url = f"https://x-access-token:{github_token}@github.com/{github_repository}"
|
|
188
|
+
elif not github_token and not github_repository:
|
|
189
|
+
url = "origin"
|
|
190
|
+
else:
|
|
191
|
+
assert github_repository and not github_token, "You can't provide the GitHub token without the repository"
|
|
192
|
+
url = f"https://github.com/{github_repository}"
|
|
193
|
+
|
|
194
|
+
# Pull the latest changes from the remote repository
|
|
195
|
+
# pull_result = subprocess.run(["git", "pull", "--merge", url], cwd=repository_path, env=env)
|
|
196
|
+
# if pull_result.returncode != 0:
|
|
197
|
+
# raise RuntimeError("Failed to pull the latest changes from the remote repository")
|
|
198
|
+
|
|
199
|
+
# Push both commits and the tag
|
|
200
|
+
push_result = subprocess.run(
|
|
201
|
+
["git", "push", url, f"{new_commit_sha}:{default_branch}"],
|
|
202
|
+
cwd=repository_path,
|
|
203
|
+
capture_output=True,
|
|
204
|
+
env=env,
|
|
205
|
+
)
|
|
206
|
+
if push_result.returncode != 0:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Failed to push the new commits to the remote repository: '{url}' with error: {push_result.stderr}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
push_result = subprocess.run(["git", "push", url, "--tag"], cwd=repository_path, capture_output=True, env=env)
|
|
212
|
+
if push_result.returncode != 0:
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
f"Failed to push the new tag to the remote repository: '{url}' with error: {push_result.stderr}"
|
|
215
|
+
)
|
|
216
|
+
print(f"Pushed to: {url}")
|
|
217
|
+
|
|
218
|
+
# Create a release using GitHub CLI if available
|
|
219
|
+
if create_release and github_repository:
|
|
220
|
+
try:
|
|
221
|
+
# Check if GitHub CLI is available
|
|
222
|
+
subprocess.run(["gh", "--version"], check=True, capture_output=True)
|
|
223
|
+
|
|
224
|
+
# Create the release
|
|
225
|
+
release_command = [
|
|
226
|
+
"gh",
|
|
227
|
+
"release",
|
|
228
|
+
"create",
|
|
229
|
+
tag,
|
|
230
|
+
"--title",
|
|
231
|
+
f"Release {tag}",
|
|
232
|
+
"--notes",
|
|
233
|
+
message,
|
|
234
|
+
"--repo",
|
|
235
|
+
github_repository,
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
if github_token:
|
|
239
|
+
env["GITHUB_TOKEN"] = github_token
|
|
240
|
+
|
|
241
|
+
release_result = subprocess.run(
|
|
242
|
+
release_command, cwd=repository_path, capture_output=True, text=True, env=env
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if release_result.returncode == 0:
|
|
246
|
+
print(f"Created GitHub release for tag: {tag}")
|
|
247
|
+
else:
|
|
248
|
+
print(f"Failed to create GitHub release: {release_result.stderr}")
|
|
249
|
+
except subprocess.CalledProcessError:
|
|
250
|
+
print("GitHub CLI not available. Skipping release creation.")
|
|
251
|
+
except Exception as e:
|
|
252
|
+
print(f"An error occurred while creating the release: {str(e)}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def patch_with_regex(
|
|
256
|
+
file_path: str,
|
|
257
|
+
regex_pattern: str,
|
|
258
|
+
new_version: str,
|
|
259
|
+
dry_run: bool = False,
|
|
260
|
+
verbose: bool = False,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Update a file by replacing the first matched group of every RegEx match with a new version."""
|
|
263
|
+
|
|
264
|
+
with open(file_path, "r") as file:
|
|
265
|
+
old_content = file.read()
|
|
266
|
+
|
|
267
|
+
def replace_first_group(match):
|
|
268
|
+
assert len(match.groups()) == 1, f"Must contain exactly one capturing group in: {regex_pattern} for {file_path}"
|
|
269
|
+
range_to_replace = match.span(1)
|
|
270
|
+
old_slice = match.span(0)
|
|
271
|
+
old_string = match.string[old_slice[0] : old_slice[1]]
|
|
272
|
+
updated = (
|
|
273
|
+
old_string[: range_to_replace[0] - old_slice[0]]
|
|
274
|
+
+ new_version
|
|
275
|
+
+ old_string[range_to_replace[1] - old_slice[0] :]
|
|
276
|
+
)
|
|
277
|
+
return updated
|
|
278
|
+
|
|
279
|
+
# Without using the re.MULTILINE flag,
|
|
280
|
+
# the ^ and $ anchors match the start and end of the whole string.
|
|
281
|
+
regex_pattern = re.compile(regex_pattern, re.MULTILINE)
|
|
282
|
+
matches = list(re.finditer(regex_pattern, old_content))
|
|
283
|
+
new_content = re.sub(regex_pattern, replace_first_group, old_content, 1)
|
|
284
|
+
|
|
285
|
+
non_empty_matches = [m for m in matches if len(m.group(0).strip())]
|
|
286
|
+
assert len(non_empty_matches) > 0, f"No matches found in: {file_path}"
|
|
287
|
+
|
|
288
|
+
for match in non_empty_matches:
|
|
289
|
+
match_line = old_content.count("\n", 0, match.pos) + 1
|
|
290
|
+
old_slice = match.group(0)
|
|
291
|
+
new_slice = re.sub(regex_pattern, replace_first_group, old_slice, 1)
|
|
292
|
+
|
|
293
|
+
if verbose:
|
|
294
|
+
print(f"Will update file: {file_path}:{match_line}")
|
|
295
|
+
print(f"- {old_slice}")
|
|
296
|
+
print(f"+ {new_slice}")
|
|
297
|
+
|
|
298
|
+
if not dry_run:
|
|
299
|
+
with open(file_path, "w") as file:
|
|
300
|
+
file.write(new_content)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def bump(
|
|
304
|
+
*, # enforce keyword-only arguments
|
|
305
|
+
dry_run: bool = False,
|
|
306
|
+
verbose: bool = False,
|
|
307
|
+
major_verbs: List[str] = ["major", "breaking", "break"],
|
|
308
|
+
minor_verbs: List[str] = ["minor", "feature", "add", "new"],
|
|
309
|
+
patch_verbs: List[str] = ["patch", "fix", "bug", "improve", "docs", "make"],
|
|
310
|
+
path: Optional[PathLike] = None, # takes current directory as default
|
|
311
|
+
changelog_file: Optional[PathLike] = None, # relative or absolute path to the changelog file
|
|
312
|
+
version_file: Optional[PathLike] = None, # relative or absolute path to the version file
|
|
313
|
+
update_version_in: Optional[List[Tuple[PathLike, str]]] = None, # path + regex pattern pairs
|
|
314
|
+
update_major_version_in: Optional[List[Tuple[PathLike, str]]] = None, # path + regex pattern pairs
|
|
315
|
+
update_minor_version_in: Optional[List[Tuple[PathLike, str]]] = None, # path + regex pattern pairs
|
|
316
|
+
update_patch_version_in: Optional[List[Tuple[PathLike, str]]] = None, # path + regex pattern pairs
|
|
317
|
+
git_user_name: str = "TinySemVer",
|
|
318
|
+
git_user_email: str = "tinysemver@ashvardanian.com",
|
|
319
|
+
push: bool = True,
|
|
320
|
+
github_token: Optional[str] = None,
|
|
321
|
+
github_repository: Optional[str] = None,
|
|
322
|
+
default_branch: str = "main",
|
|
323
|
+
create_release: bool = False,
|
|
324
|
+
) -> SemVer:
|
|
325
|
+
"""Primary function used to update the files (version, changelog, etc.), create a new Git tag, push it to a GitHub repository.
|
|
326
|
+
It can be used for dry-runs to preview the changes without actually creating a new tag.
|
|
327
|
+
It can be used as a standalone script or as a library function.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
dry_run (bool): If True, performs a dry run without making any changes. Defaults to False.
|
|
331
|
+
verbose (bool): If True, enables verbose output. Defaults to False.
|
|
332
|
+
major_verbs (List[str]): List of keywords that indicate a major version bump. Defaults to ["major", "breaking", "break"].
|
|
333
|
+
minor_verbs (List[str]): List of keywords that indicate a minor version bump. Defaults to ["minor", "feature", "add", "new"].
|
|
334
|
+
patch_verbs (List[str]): List of keywords that indicate a patch version bump. Defaults to ["patch", "fix", "bug", "improve", "docs", "make"].
|
|
335
|
+
path (Optional[PathLike]): The path to the repository. If None, the current directory is used. Defaults to None.
|
|
336
|
+
changelog_file (Optional[PathLike]): The path to the changelog file to update. If None, no changelog update is performed. Defaults to None.
|
|
337
|
+
version_file (Optional[PathLike]): The path to the version file to update. If None, no version file update is performed. Defaults to None.
|
|
338
|
+
update_version_in (Optional[List[Tuple[PathLike, str]]]): List of (file, regex pattern) tuples to update the full version in specified files. Defaults to None.
|
|
339
|
+
update_major_version_in (Optional[List[Tuple[PathLike, str]]]): List of (file, regex pattern) tuples to update the major version in specified files. Defaults to None.
|
|
340
|
+
update_minor_version_in (Optional[List[Tuple[PathLike, str]]]): List of (file, regex pattern) tuples to update the minor version in specified files. Defaults to None.
|
|
341
|
+
update_patch_version_in (Optional[List[Tuple[PathLike, str]]]): List of (file, regex pattern) tuples to update the patch version in specified files. Defaults to None.
|
|
342
|
+
git_user_name (str): The Git user name for committing changes. Defaults to "TinySemVer".
|
|
343
|
+
git_user_email (str): The Git user email for committing changes. Defaults to "tinysemver@ashvardanian.com".
|
|
344
|
+
push (bool): If True, pushes the changes to the remote GitHub repository. Defaults to True.
|
|
345
|
+
github_token (Optional[str]): The GitHub token for authentication. If None, it attempts to use the GH_TOKEN environment variable. Defaults to None.
|
|
346
|
+
github_repository (Optional[str]): The GitHub repository in 'owner/repo' format. If None, it attempts to use the GH_REPOSITORY environment variable. Defaults to None.
|
|
347
|
+
default_branch (str): The default branch to push the changes to. Defaults to "main".
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
SemVer: The new version after the bump.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
repository_path = os.path.abspath(path) if path else os.getcwd()
|
|
354
|
+
assert os.path.isdir(os.path.join(repository_path, ".git")), f"Not a Git repository: {repository_path}"
|
|
355
|
+
|
|
356
|
+
def normalize_path(file_path: str) -> str:
|
|
357
|
+
if not file_path or len(file_path) == 0:
|
|
358
|
+
return None
|
|
359
|
+
if os.path.isabs(file_path):
|
|
360
|
+
return file_path
|
|
361
|
+
return os.path.join(repository_path, file_path)
|
|
362
|
+
|
|
363
|
+
changelog_file = normalize_path(changelog_file)
|
|
364
|
+
version_file = normalize_path(version_file)
|
|
365
|
+
if update_version_in:
|
|
366
|
+
update_version_in = [(normalize_path(file), pattern) for file, pattern in update_version_in]
|
|
367
|
+
if update_major_version_in:
|
|
368
|
+
update_major_version_in = [(normalize_path(file), pattern) for file, pattern in update_major_version_in]
|
|
369
|
+
if update_minor_version_in:
|
|
370
|
+
update_minor_version_in = [(normalize_path(file), pattern) for file, pattern in update_minor_version_in]
|
|
371
|
+
if update_patch_version_in:
|
|
372
|
+
update_patch_version_in = [(normalize_path(file), pattern) for file, pattern in update_patch_version_in]
|
|
373
|
+
|
|
374
|
+
# Let's pull the environment variables from the GitHub Action context
|
|
375
|
+
# https://cli.github.com/manual/gh_help_environment
|
|
376
|
+
if not dry_run:
|
|
377
|
+
github_token = github_token or os.getenv("GH_TOKEN", None)
|
|
378
|
+
assert not github_token or len(github_token) > 0, "GitHub token is empty or missing"
|
|
379
|
+
github_repository = github_repository or os.getenv("GH_REPOSITORY", None)
|
|
380
|
+
assert not github_repository or len(github_repository) > 0, "GitHub repository is empty or missing"
|
|
381
|
+
if github_repository:
|
|
382
|
+
matched_repository = re.match(r"[\w-]*\/[\w-]*", github_repository)
|
|
383
|
+
assert (
|
|
384
|
+
matched_repository and matched_repository.string == github_repository
|
|
385
|
+
), "GitHub repository must be in the 'owner/repo' format"
|
|
386
|
+
|
|
387
|
+
assert not version_file or (
|
|
388
|
+
not os.path.exists(version_file) or os.path.isfile(version_file)
|
|
389
|
+
), f"Version file is missing or isn't a regular file: {version_file}"
|
|
390
|
+
assert not changelog_file or (
|
|
391
|
+
not os.path.exists(changelog_file) or os.path.isfile(changelog_file)
|
|
392
|
+
), f"Changelog file is missing or isn't a regular file: {changelog_file}"
|
|
393
|
+
|
|
394
|
+
major_verbs = normalize_verbs(major_verbs, ["major", "breaking", "break"])
|
|
395
|
+
minor_verbs = normalize_verbs(minor_verbs, ["minor", "feature", "add", "new"])
|
|
396
|
+
patch_verbs = normalize_verbs(patch_verbs, ["patch", "fix", "bug", "improve", "docs", "make"])
|
|
397
|
+
|
|
398
|
+
last_tag = get_last_tag(repository_path)
|
|
399
|
+
assert last_tag, f"No tags found in the repository: {repository_path}"
|
|
400
|
+
|
|
401
|
+
current_version = parse_version(last_tag)
|
|
402
|
+
if verbose:
|
|
403
|
+
print(f"Current version: {current_version[0]}.{current_version[1]}.{current_version[2]}")
|
|
404
|
+
|
|
405
|
+
commits_hashes, commits_messages = get_commits_since_tag(repository_path, last_tag)
|
|
406
|
+
assert len(commits_hashes), f"No new commits since the last {last_tag} tag, aborting."
|
|
407
|
+
|
|
408
|
+
if verbose:
|
|
409
|
+
print(f"? Commits since last tag: {len(commits_hashes)}")
|
|
410
|
+
for hash, commit in zip(commits_hashes, commits_messages):
|
|
411
|
+
print(f"# {hash}: {commit}")
|
|
412
|
+
|
|
413
|
+
major_commits, minor_commits, patch_commits = group_commits(commits_messages, major_verbs, minor_verbs, patch_verbs)
|
|
414
|
+
assert (
|
|
415
|
+
len(major_commits) + len(minor_commits) + len(patch_commits)
|
|
416
|
+
), "No commit categories found to bump the version: " + ", ".join(commits_messages)
|
|
417
|
+
|
|
418
|
+
if len(major_commits):
|
|
419
|
+
bump_type = "major"
|
|
420
|
+
elif len(minor_commits):
|
|
421
|
+
bump_type = "minor"
|
|
422
|
+
else:
|
|
423
|
+
bump_type = "patch"
|
|
424
|
+
new_version = bump_version(current_version, bump_type)
|
|
425
|
+
if verbose:
|
|
426
|
+
print(f"Next version: {new_version[0]}.{new_version[1]}.{new_version[2]} (type: {bump_type})")
|
|
427
|
+
|
|
428
|
+
new_version_str = f"{new_version[0]}.{new_version[1]}.{new_version[2]}"
|
|
429
|
+
if version_file:
|
|
430
|
+
patch_with_regex(version_file, r"(.*)", new_version_str, dry_run=dry_run, verbose=verbose)
|
|
431
|
+
|
|
432
|
+
if changelog_file:
|
|
433
|
+
now = datetime.now()
|
|
434
|
+
changes = f"\n## {now:%B %d, %Y}: v{new_version_str}\n"
|
|
435
|
+
if len(major_commits):
|
|
436
|
+
changes += f"\n### Major\n\n" + "\n".join(f"- {c}" for c in major_commits) + "\n"
|
|
437
|
+
if len(minor_commits):
|
|
438
|
+
changes += f"\n### Minor\n\n" + "\n".join(f"- {c}" for c in minor_commits) + "\n"
|
|
439
|
+
if len(patch_commits):
|
|
440
|
+
changes += f"\n### Patch\n\n" + "\n".join(f"- {c}" for c in patch_commits) + "\n"
|
|
441
|
+
|
|
442
|
+
print(f"Will update file: {changelog_file}")
|
|
443
|
+
if verbose:
|
|
444
|
+
changes_lines = changes.count("\n") + 1
|
|
445
|
+
print(f"? Appending {changes_lines} lines")
|
|
446
|
+
|
|
447
|
+
if not dry_run:
|
|
448
|
+
with open(changelog_file, "a") as file:
|
|
449
|
+
file.write(changes)
|
|
450
|
+
|
|
451
|
+
if update_version_in:
|
|
452
|
+
for file_path, regex_pattern in update_version_in:
|
|
453
|
+
patch_with_regex(file_path, regex_pattern, new_version_str, dry_run=dry_run, verbose=verbose)
|
|
454
|
+
if bump_type in ["major"] and update_major_version_in:
|
|
455
|
+
for file_path, regex_pattern in update_major_version_in:
|
|
456
|
+
patch_with_regex(file_path, regex_pattern, str(new_version[0]), dry_run=dry_run, verbose=verbose)
|
|
457
|
+
if bump_type in ["major", "minor"] and update_minor_version_in:
|
|
458
|
+
for file_path, regex_pattern in update_minor_version_in:
|
|
459
|
+
patch_with_regex(file_path, regex_pattern, str(new_version[1]), dry_run=dry_run, verbose=verbose)
|
|
460
|
+
if bump_type in ["major", "minor", "patch"] and update_patch_version_in:
|
|
461
|
+
for file_path, regex_pattern in update_patch_version_in:
|
|
462
|
+
patch_with_regex(file_path, regex_pattern, str(new_version[2]), dry_run=dry_run, verbose=verbose)
|
|
463
|
+
|
|
464
|
+
if not dry_run:
|
|
465
|
+
create_tag(
|
|
466
|
+
repository_path=repository_path,
|
|
467
|
+
version=new_version,
|
|
468
|
+
user_name=git_user_name,
|
|
469
|
+
user_email=git_user_email,
|
|
470
|
+
default_branch=default_branch,
|
|
471
|
+
github_token=github_token,
|
|
472
|
+
github_repository=github_repository,
|
|
473
|
+
push=push,
|
|
474
|
+
create_release=create_release,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main():
|
|
479
|
+
"""
|
|
480
|
+
TinySemVer entrypoint responsible for parsing CLI arguments and environment variables,
|
|
481
|
+
preprocessing them and passing down to the `bump` function.
|
|
482
|
+
"""
|
|
483
|
+
if "GITHUB_ACTIONS" not in os.environ:
|
|
484
|
+
parser = argparse.ArgumentParser(description="Tiny Semantic Versioning tool")
|
|
485
|
+
parser.add_argument(
|
|
486
|
+
"--dry-run",
|
|
487
|
+
action="store_true",
|
|
488
|
+
default=False,
|
|
489
|
+
help="Do not create a new tag",
|
|
490
|
+
)
|
|
491
|
+
parser.add_argument(
|
|
492
|
+
"--verbose",
|
|
493
|
+
action="store_true",
|
|
494
|
+
default=False,
|
|
495
|
+
help="Print more information",
|
|
496
|
+
)
|
|
497
|
+
parser.add_argument(
|
|
498
|
+
"--push",
|
|
499
|
+
action="store_true",
|
|
500
|
+
default=False,
|
|
501
|
+
help="Push the new tag to the repository",
|
|
502
|
+
)
|
|
503
|
+
parser.add_argument(
|
|
504
|
+
"--major-verbs",
|
|
505
|
+
help="Comma-separated list of major verbs, like 'breaking,break,major'",
|
|
506
|
+
)
|
|
507
|
+
parser.add_argument(
|
|
508
|
+
"--minor-verbs",
|
|
509
|
+
help="Comma-separated list of minor verbs, like 'feature,minor,add,new'",
|
|
510
|
+
)
|
|
511
|
+
parser.add_argument(
|
|
512
|
+
"--patch-verbs",
|
|
513
|
+
help="Comma-separated list of patch verbs, like 'fix,patch,bug,improve,docs,make'",
|
|
514
|
+
)
|
|
515
|
+
parser.add_argument(
|
|
516
|
+
"--changelog-file",
|
|
517
|
+
help="Path to the changelog file, like 'CHANGELOG.md'",
|
|
518
|
+
)
|
|
519
|
+
parser.add_argument(
|
|
520
|
+
"--version-file",
|
|
521
|
+
help="Path to the version file, like 'VERSION'",
|
|
522
|
+
)
|
|
523
|
+
parser.add_argument(
|
|
524
|
+
"--update-version-in",
|
|
525
|
+
nargs=2,
|
|
526
|
+
action="append",
|
|
527
|
+
metavar=("FILE", "REGEX"),
|
|
528
|
+
help="File path and regex pattern to update version",
|
|
529
|
+
)
|
|
530
|
+
parser.add_argument(
|
|
531
|
+
"--update-major-version-in",
|
|
532
|
+
nargs=2,
|
|
533
|
+
action="append",
|
|
534
|
+
metavar=("FILE", "REGEX"),
|
|
535
|
+
help="File path and regex pattern to update major version",
|
|
536
|
+
)
|
|
537
|
+
parser.add_argument(
|
|
538
|
+
"--update-minor-version-in",
|
|
539
|
+
nargs=2,
|
|
540
|
+
action="append",
|
|
541
|
+
metavar=("FILE", "REGEX"),
|
|
542
|
+
help="File path and regex pattern to update minor version",
|
|
543
|
+
)
|
|
544
|
+
parser.add_argument(
|
|
545
|
+
"--update-patch-version-in",
|
|
546
|
+
nargs=2,
|
|
547
|
+
action="append",
|
|
548
|
+
metavar=("FILE", "REGEX"),
|
|
549
|
+
help="File path and regex pattern to update patch version",
|
|
550
|
+
)
|
|
551
|
+
parser.add_argument(
|
|
552
|
+
"--path",
|
|
553
|
+
default=".",
|
|
554
|
+
help="Path to the git repository",
|
|
555
|
+
)
|
|
556
|
+
parser.add_argument(
|
|
557
|
+
"--git-user-name",
|
|
558
|
+
default="TinySemVer",
|
|
559
|
+
help="Git user name for commits",
|
|
560
|
+
)
|
|
561
|
+
parser.add_argument(
|
|
562
|
+
"--git-user-email",
|
|
563
|
+
default="tinysemver@ashvardanian.com",
|
|
564
|
+
help="Git user email for commits",
|
|
565
|
+
)
|
|
566
|
+
parser.add_argument(
|
|
567
|
+
"--github-token",
|
|
568
|
+
help="GitHub access token to push to protected branches, if not set will use GH_TOKEN env var",
|
|
569
|
+
)
|
|
570
|
+
parser.add_argument(
|
|
571
|
+
"--default-branch",
|
|
572
|
+
help="Default branch name of the repo, if not set will default to 'main'",
|
|
573
|
+
)
|
|
574
|
+
parser.add_argument(
|
|
575
|
+
"--github-repository",
|
|
576
|
+
help="GitHub repository in the 'owner/repo' format, if not set will use GH_REPOSITORY env var",
|
|
577
|
+
)
|
|
578
|
+
parser.add_argument(
|
|
579
|
+
"--create-release",
|
|
580
|
+
action="store_true",
|
|
581
|
+
default=False,
|
|
582
|
+
help="Create a GitHub release using the GitHub CLI",
|
|
583
|
+
)
|
|
584
|
+
args = parser.parse_args()
|
|
585
|
+
else:
|
|
586
|
+
|
|
587
|
+
class Args:
|
|
588
|
+
pass
|
|
589
|
+
|
|
590
|
+
args = Args()
|
|
591
|
+
args.dry_run = os.environ.get("TINYSEMVER_DRY_RUN", "").lower() == "true"
|
|
592
|
+
args.verbose = os.environ.get("TINYSEMVER_VERBOSE", "").lower() == "true"
|
|
593
|
+
args.push = os.environ.get("TINYSEMVER_PUSH", "").lower() == "true"
|
|
594
|
+
args.major_verbs = os.environ.get("TINYSEMVER_MAJOR_VERBS") or "breaking,break,major"
|
|
595
|
+
args.minor_verbs = os.environ.get("TINYSEMVER_MINOR_VERBS") or "feature,minor,add,new"
|
|
596
|
+
args.patch_verbs = os.environ.get("TINYSEMVER_PATCH_VERBS") or "fix,patch,bug,improve,docs,make"
|
|
597
|
+
args.default_branch = os.environ.get("TINYSEMVER_DEFAULT_BRANCH") or "main"
|
|
598
|
+
args.changelog_file = os.environ.get("TINYSEMVER_CHANGELOG_FILE")
|
|
599
|
+
args.version_file = os.environ.get("TINYSEMVER_VERSION_FILE")
|
|
600
|
+
args.update_version_in = [
|
|
601
|
+
tuple(item.split(":", 1))
|
|
602
|
+
for item in os.environ.get("TINYSEMVER_UPDATE_VERSION_IN", "").split("\n") #
|
|
603
|
+
if item #
|
|
604
|
+
]
|
|
605
|
+
args.update_major_version_in = [
|
|
606
|
+
tuple(item.split(":", 1))
|
|
607
|
+
for item in os.environ.get("TINYSEMVER_UPDATE_MAJOR_VERSION_IN", "").split("\n")
|
|
608
|
+
if item
|
|
609
|
+
]
|
|
610
|
+
args.update_minor_version_in = [
|
|
611
|
+
tuple(item.split(":", 1))
|
|
612
|
+
for item in os.environ.get("TINYSEMVER_UPDATE_MINOR_VERSION_IN", "").split("\n")
|
|
613
|
+
if item
|
|
614
|
+
]
|
|
615
|
+
args.update_patch_version_in = [
|
|
616
|
+
tuple(item.split(":", 1))
|
|
617
|
+
for item in os.environ.get("TINYSEMVER_UPDATE_PATCH_VERSION_IN", "").split("\n")
|
|
618
|
+
if item
|
|
619
|
+
]
|
|
620
|
+
args.path = os.environ.get("TINYSEMVER_REPO_PATH")
|
|
621
|
+
args.git_user_name = os.environ.get("TINYSEMVER_GIT_USER_NAME", "TinySemVer")
|
|
622
|
+
args.git_user_email = os.environ.get("TINYSEMVER_GIT_USER_EMAIL", "tinysemver@ashvardanian.com")
|
|
623
|
+
args.github_token = os.environ.get("GITHUB_TOKEN")
|
|
624
|
+
args.github_repository = os.environ.get("GITHUB_REPOSITORY")
|
|
625
|
+
args.create_release = os.environ.get("TINYSEMVER_CREATE_RELEASE", "").lower() == "true"
|
|
626
|
+
|
|
627
|
+
# It's common for a CI pipeline to have multiple broken settings or missing files.
|
|
628
|
+
# For the best user experience, we want to catch all errors and print them at once,
|
|
629
|
+
# instead of failing at the first error and exiting the script... forcing the user to
|
|
630
|
+
# fix one issue at a time, and re-run the script multiple times.
|
|
631
|
+
#
|
|
632
|
+
# TODO: To achieve this, on even on "real runs", we should first perform a "dry run" to catch,
|
|
633
|
+
# accumulate all errors
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
bump(
|
|
637
|
+
path=args.path,
|
|
638
|
+
dry_run=args.dry_run,
|
|
639
|
+
verbose=args.verbose,
|
|
640
|
+
major_verbs=args.major_verbs,
|
|
641
|
+
minor_verbs=args.minor_verbs,
|
|
642
|
+
patch_verbs=args.patch_verbs,
|
|
643
|
+
changelog_file=args.changelog_file,
|
|
644
|
+
version_file=args.version_file,
|
|
645
|
+
update_version_in=args.update_version_in,
|
|
646
|
+
update_major_version_in=args.update_major_version_in,
|
|
647
|
+
update_minor_version_in=args.update_minor_version_in,
|
|
648
|
+
update_patch_version_in=args.update_patch_version_in,
|
|
649
|
+
git_user_name=args.git_user_name,
|
|
650
|
+
git_user_email=args.git_user_email,
|
|
651
|
+
github_token=args.github_token,
|
|
652
|
+
github_repository=args.github_repository,
|
|
653
|
+
push=args.push,
|
|
654
|
+
create_release=args.create_release,
|
|
655
|
+
)
|
|
656
|
+
except AssertionError as e:
|
|
657
|
+
print(f"! {e}")
|
|
658
|
+
exit(1)
|
|
659
|
+
except Exception as e:
|
|
660
|
+
traceback.print_exc(e)
|
|
661
|
+
exit(1)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
if __name__ == "__main__":
|
|
665
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|