Add the ability to directly set version parts. #2

Merged
jdb merged 1 commits from allow-direct-version-part-updates into main 2025-06-18 01:54:06 +00:00
4 changed files with 219 additions and 74 deletions
+2 -2
View File
@@ -26,9 +26,9 @@ jobs:
- name: Build unittests - name: Build unittests
run: | run: |
printf "🔨\033[0;32m Compiling unit tests...\033[0m\n" >&1 printf "🔨\033[0;32m Compiling unit tests...\033[0m\n" >&1
nimble build nimble c tupdate_version.nim
- name: Run unittests - name: Run unittests
run: | run: |
printf "🧪\033[0;32m Running unit tests...\033[0m\n" >&1 printf "🧪\033[0;32m Running unit tests...\033[0m\n" >&1
./update_version test ./tupdate_version
+1
View File
@@ -1,2 +1,3 @@
.*sw? .*sw?
update_version update_version
tupdate_version
+98
View File
@@ -0,0 +1,98 @@
import std/[paths, tables, unittest]
import ./update_version
suite "update_version":
test "incrementLastVersionPart":
check:
incrementLastVersionPart("1.0.0") == "1.0.1"
incrementLastVersionPart("1.0.0-alpha.1") == "1.0.0-alpha.2"
incrementLastVersionPart("cicd_alphe.1-prerelease") == "cicd_alphe.2-prerelease"
incrementLastVersionPart("2024.04.1") == "2024.04.2"
test "incrementSemverPart":
check:
incrementSemverPart("1.0.0", major) == "2.0.0"
incrementSemverPart("1.0.0", minor) == "1.1.0"
incrementSemverPart("1.0.0", patch) == "1.0.1"
incrementSemverPart("1.5.10", patch) == "1.5.11"
incrementSemverPart("1.5.10", minor) == "1.6.0"
incrementSemverPart("1.5.10-alpha.1", prerelease) == "1.5.10-alpha.2"
incrementSemverPart("1.5.10-alpha.1+build.10", prerelease) == "1.5.10-alpha.2"
incrementSemverPart("1.5.10-alpha.1+build.10", buildmetadata) == "1.5.10-alpha.1+build.11"
incrementSemverPart("1.5.10", buildmetadata) == "1.5.10+build.0"
test "fmtSemver":
check:
fmtSemver(newTable([
(major, "5"), (minor, "2"), (patch, "62"),
(prerelease, "alpha.59"), (buildmetadata, "githash_123098")])) ==
"5.2.62-alpha.59+githash_123098"
test "setLastVersionPart":
check:
setLastVersionPart("1.0.0", "5") == "1.0.5"
setLastVersionPart("cicd.2", "3-alpha.1") == "cicd.3-alpha.1"
test "setSemverPart":
check:
setSemverPart("1.4.2-alpha.5+a3e4b69", minor, "7") == "1.7.0"
setSemverPart("1.4.2-alpha.5+a3e4b69", major, "3") == "3.0.0"
setSemverPart("1.4.2-alpha.5+a3e4b69", prerelease, "beta.1") == "1.4.2-beta.1"
setSemverPart("1.4.2-alpha.5+a3e4b69", buildmetadata, "testbuild") == "1.4.2-alpha.5+testbuild"
test "replaceVersionInSource - Nim":
let pkgVersion = PackageVersion(
lang: lNim,
file: Path("./testpackage.nimble"),
version: "1.0.3",
name: "testpackage")
let source = """
import std/[sequtils, strutils]
const IGNORED_VERSION* = "1.0.0"
const OTHER_PKG_VERSION* = "4.2.1"
const TESTPKG_VERSION* = "1.0.2"
when isMainModule:
echo "sample file"
"""
check replaceVersionInSource(source, pkgVersion, "1.0.2") == """
import std/[sequtils, strutils]
const IGNORED_VERSION* = "1.0.0"
const OTHER_PKG_VERSION* = "4.2.1"
const TESTPKG_VERSION* = "1.0.3"
when isMainModule:
echo "sample file"
"""
test "replaceVersionInSource - JavaScript":
let pkgVersion = PackageVersion(
lang: lNode,
file: Path("./package.json"),
version: "1.0.3",
name: "testpackage")
let source = """
import { createApp } from 'vue'
import './styles/main.css'
import App from './App.vue'
export const TESTPACKAGE_VERSION = "1.0.2"
createApp(App)
"""
check replaceVersionInSource(source, pkgVersion, "1.0.2") == """
import { createApp } from 'vue'
import './styles/main.css'
import App from './App.vue'
export const TESTPACKAGE_VERSION = "1.0.3"
createApp(App)
"""
+117 -71
View File
@@ -3,11 +3,10 @@ import std/nre except toSeq
import docopt, zero_functional import docopt, zero_functional
const USAGE = """Usage: const USAGE = """Usage:
update_nim_package_version bump <part> [<src-file> ...] [options] update_nim_package_version bump [<src-file> ...] [options]
update_nim_package_version set <new-version> [<src-file> ...] [options] update_nim_package_version set <new-version> [<src-file> ...] [options]
update_nim_package_version get [<src-file> ...] [options] update_nim_package_version get [<src-file> ...] [options]
update_nim_package_version interactive [<src-file> ...] [options] update_nim_package_version interactive [<src-file> ...] [options]
update_nim_package_version test
Options: Options:
@@ -18,22 +17,28 @@ Options:
values are: 'nim' or 'node'. If not provided, this values are: 'nim' or 'node'. If not provided, this
is auto-detected by the presence of either a *.nimble is auto-detected by the presence of either a *.nimble
or package.json file. or package.json file.
-p, --part <part> Choose which part of the version string to set or
bump. Valid options are: 'major', 'minor', 'patch',
'prerelease', 'buildmetadata', and 'last'. Defaults
to 'last'
Details: Details:
bump bump
Assuming the project with a semver-like versioning scheme, update one of Assuming the project with a semver-like versioning scheme, update one of
the version parts by one. <version-part> must be one of 'major', 'minor', the version parts by one. <part> must be one of 'major', 'minor', 'patch',
'patch', or 'last'. Semver looks like 'major.minor.patch' Last is a special 'prerelease', 'buildmetadata', or 'last'. Semver looks like
case to support looser version strings that end in '.x' like 'alpha.1' or 'major.minor.patch-prerelease+buildmetadata' Last is a special case to
'1.5' and matches '.*(\.\d+)$' support looser version strings that end in '.x' like 'alpha.1' or '1.5' and
matches '.*(\.\d+)$'
The matched portion is interpreted as an integer and incremented by one. The matched portion is interpreted as an integer and incremented by one.
set set
Update the version string entirely with a new string. In this case, the Update the version string with a new string. If --part is supplied, only
versioning scheme of the project doesn't matter that part is updated. Otherwise the entire version string is replaced.
get get
@@ -61,18 +66,18 @@ Details:
Node: ^\s*export\s+const\s+\S*VERSION\S*\s*=\s*"(<old-version>)"\s*;?$ Node: ^\s*export\s+const\s+\S*VERSION\S*\s*=\s*"(<old-version>)"\s*;?$
""" """
const UV_VERSION = "1.0.2" const UV_VERSION* = "1.0.2"
type type
LangType = enum lNim, lNode LangType* = enum lNim, lNode
SemVerParts = enum major, minor, patch, prerelease, buildmetadata SemverParts* = enum major, minor, patch, prerelease, buildmetadata
type PackageVersion = object type PackageVersion* = object
file: Path file*: Path
version: string version*: string
name: string name*: string
case lang: LangType case lang*: LangType
of lNim: of lNim:
lines: seq[string] lines: seq[string]
versionLine: int versionLine: int
@@ -86,13 +91,29 @@ type PackageVersion = object
# See also: # See also:
# https://regex101.com/r/Ly7O1x/3/ # https://regex101.com/r/Ly7O1x/3/
# #
let SemVerRegex = let SemverRegex =
re"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" re"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
let NimConstPattern = "^\\s*const\\s+\\S*VERSION\\S*\\*?\\s*=\\s*\"($#)\"\\s*$" let NimConstPattern = "^\\s*const\\s+\\S*VERSION\\S*\\*?\\s*=\\s*\"($#)\"\\s*$"
let NodeConstPattern = let NodeConstPattern =
"^\\s*export\\s+const\\s+\\S*VERSION\\S*\\s*=\\s*\"($#)\"\\s*;?$" "^\\s*export\\s+const\\s+\\S*VERSION\\S*\\s*=\\s*\"($#)\"\\s*;?$"
let VERSION_DEFAULTS: TableRef[SemverParts, string] = newTable([
(major, "0"),
(minor, "0"),
(patch, "0"),
(prerelease, ""),
(buildmetadata, "")])
proc parseSemverPart(str: string): SemverParts =
try: parseEnum[SemverParts](str)
except:
stderr.writeLine(
"update_version: Invalid --part value: '" & str & "'. Valid values are:" &
"\n\tmajor, minor, patch, prerelease, buildmetadata, last")
quit(QuitFailure)
proc parseNimblePackage(dir: Path): PackageVersion = proc parseNimblePackage(dir: Path): PackageVersion =
result = PackageVersion(lang: lNim) result = PackageVersion(lang: lNim)
@@ -151,14 +172,18 @@ proc writePackage(pkg: PackageVersion) =
writeFile($pkg.file, pkg.nodePackage.pretty) writeFile($pkg.file, pkg.nodePackage.pretty)
proc replaceVersionInFile(file: Path, pkg: PackageVersion, vOld: string) = proc replaceVersionInSource*(
source: string,
pkg: PackageVersion,
vOld: string): string =
let rgx = let rgx =
case pkg.lang case pkg.lang
of lNim: re(NimConstPattern % [vOld.replace(".", "\\.")]) of lNim: re(NimConstPattern % [vOld.replace(".", "\\.")])
of lNode: re(NodeConstPattern % [vOld.replace(".", "\\.")]) of lNode: re(NodeConstPattern % [vOld.replace(".", "\\.")])
var newLines = newSeq[string]() var newLines = newSeq[string]()
for l in lines($file): for l in source.splitLines():
let mOpt = l.match(rgx) let mOpt = l.match(rgx)
if mOpt.isSome: if mOpt.isSome:
let m = mOpt.get let m = mOpt.get
@@ -168,9 +193,60 @@ proc replaceVersionInFile(file: Path, pkg: PackageVersion, vOld: string) =
l[m.captureBounds[0].b+1 .. ^1]) l[m.captureBounds[0].b+1 .. ^1])
else: newLines.add(l) else: newLines.add(l)
writeFile($file, newLines.join("\p")) return newLines.join("\p")
proc incrementLastVersionPart(version: string): string = proc replaceVersionInFile(file: Path, pkg: PackageVersion, vOld: string) =
writeFile($file, replaceVersionInSource(readFile($file), pkg, vOld))
proc fmtSemver*(semver: TableRef[SemverParts, string]): string =
result = "$#.$#.$#" % [
semver[major], semver[minor], semver[patch] ]
if semver[prerelease].len > 0:
result &= "-" & semver[prerelease]
if semver[buildmetadata].len > 0:
result &= "+" & semver[buildmetadata]
proc setLastVersionPart*(version: string, newVersion: string): string =
let versionParts = toSeq(findIter(version, re"([^\d.]+)?\.?(\d+)"))
let lastVersionPartMatch = versionParts[^1]
return
version[0..<lastVersionPartMatch.captureBounds[1].a] &
newVersion &
version[lastVersionPartMatch.captureBounds[1].b+1 .. ^1]
proc setSemverPart*(
version: string,
part: SemverParts,
newVersionPart: string,
defaults = VERSION_DEFAULTS): string =
let matchOpt = match(version, SemverRegex)
if matchOpt.isNone:
raise newException(ValueError,
"Version [$#] is not a valid Semantic Version number" % version)
let m = matchOpt.get
let versionParts = newTable[SemverParts, string]()
for p in SemverParts.items:
if p == part: versionParts[p] = newVersionPart
elif not m.captures.contains($p) or p > part: versionParts[p] = defaults[p]
else: versionParts[p] = m.captures[$p]
result = fmtSemver(versionParts)
if match(result, SemverRegex).isNone:
raise newException(ValueError,
"Refusing to update: Version [$#] would not be a valid Semantic Version number" % result)
proc incrementLastVersionPart*(version: string): string =
let versionParts = toSeq(findIter(version, re"([^\d.]+)?\.?(\d+)")) let versionParts = toSeq(findIter(version, re"([^\d.]+)?\.?(\d+)"))
let lastVersionPartMatch = versionParts[^1] let lastVersionPartMatch = versionParts[^1]
let lastVersionPartInt = parseInt(lastVersionPartMatch.captures[1]) let lastVersionPartInt = parseInt(lastVersionPartMatch.captures[1])
@@ -180,22 +256,12 @@ proc incrementLastVersionPart(version: string): string =
version[lastVersionPartMatch.captureBounds[1].b+1 .. ^1] version[lastVersionPartMatch.captureBounds[1].b+1 .. ^1]
let VERSION_DEFAULTS: TableRef[SemVerParts, string] = newTable([ proc incrementSemverPart*(
(major, "0"),
(minor, "0"),
(patch, "0"),
(prerelease, ""),
(buildmetadata, "")])
proc incrementSemverPart(
version: string, version: string,
part: SemVerParts, part: SemverParts,
defaults = VERSION_DEFAULTS): string = defaults = VERSION_DEFAULTS): string =
result = "" let matchOpt = match(version, SemverRegex)
let matchOpt = match(version, SemVerRegex)
if matchOpt.isNone: if matchOpt.isNone:
raise newException(ValueError, raise newException(ValueError,
@@ -203,8 +269,8 @@ proc incrementSemverPart(
let m = matchOpt.get let m = matchOpt.get
let versionParts = newTable[SemVerParts, string]() let versionParts = newTable[SemverParts, string]()
for p in SemVerParts.items: for p in SemverParts.items:
if p == part: if p == part:
if m.captures.contains($p): if m.captures.contains($p):
versionParts[p] = incrementLastVersionPart(m.captures[$p]) versionParts[p] = incrementLastVersionPart(m.captures[$p])
@@ -217,42 +283,11 @@ proc incrementSemverPart(
elif not m.captures.contains($p) or p > part: versionParts[p] = defaults[p] elif not m.captures.contains($p) or p > part: versionParts[p] = defaults[p]
else: versionParts[p] = m.captures[$p] else: versionParts[p] = m.captures[$p]
result = "$#.$#.$#" % [ return fmtSemver(versionParts)
versionParts[major], versionParts[minor], versionParts[patch] ]
if versionParts[prerelease].len > 0:
result &= "-" & versionParts[prerelease]
if versionParts[buildmetadata].len > 0:
result &= "+" & versionParts[buildmetadata]
when isMainModule: when isMainModule:
let args = docopt(USAGE, version = UV_VERSION) let args = docopt(USAGE, version = UV_VERSION)
if args["test"]:
# incrementLastVersionPart
assert incrementLastVersionPart("1.0.0") == "1.0.1"
assert incrementLastVersionPart("1.0.0-alpha.1") == "1.0.0-alpha.2"
assert incrementLastVersionPart("cicd_alphe.1-prerelease") == "cicd_alphe.2-prerelease"
assert incrementLastVersionPart("2024.04.1") == "2024.04.2"
# incrementSemverPart
assert incrementSemverPart("1.0.0", major) == "2.0.0"
assert incrementSemverPart("1.0.0", minor) == "1.1.0"
assert incrementSemverPart("1.0.0", patch) == "1.0.1"
assert incrementSemverPart("1.5.10", patch) == "1.5.11"
assert incrementSemverPart("1.5.10", minor) == "1.6.0"
assert incrementSemverPart("1.5.10-alpha.1", prerelease) == "1.5.10-alpha.2"
assert incrementSemverPart("1.5.10-alpha.1+build.10", prerelease) == "1.5.10-alpha.2"
assert incrementSemverPart("1.5.10-alpha.1+build.10", buildmetadata) == "1.5.10-alpha.1+build.11"
assert incrementSemverPart("1.5.10", buildmetadata) == "1.5.10+build.0"
echo "All tests passed."
quit(QuitSuccess)
let dir = let dir =
if args["--dir"]: Path($args["--dir"]) if args["--dir"]: Path($args["--dir"])
else: Path(".") else: Path(".")
@@ -268,10 +303,14 @@ when isMainModule:
let oldVersion = pkg.version let oldVersion = pkg.version
if args["bump"]: if args["bump"]:
if $args["<part>"] == "last": let partName =
if args["--part"]: $args["--part"]
else: "last"
if partName == "last":
pkg.version = incrementLastVersionPart(pkg.version) pkg.version = incrementLastVersionPart(pkg.version)
else: else:
let part = parseEnum[SemVerParts]($args["<part>"]) let part = parseSemverPart(partName)
pkg.version = incrementSemverPart(pkg.version, part) pkg.version = incrementSemverPart(pkg.version, part)
writePackage(pkg) writePackage(pkg)
@@ -282,7 +321,14 @@ when isMainModule:
echo pkg.version echo pkg.version
elif args["set"]: elif args["set"]:
pkg.version = $args["<new-version>"]
if not args["--part"]:
pkg.version = $args["<new-version>"]
elif $args["--part"] == "last":
pkg.version = setLastVersionPart(pkg.version, $args["<new-version>"])
else:
let part = parseSemverPart($args["--part"])
pkg.version = setSemverPart(pkg.version, part, $args["<new-version>"])
for filePath in args["<src-file>"]: for filePath in args["<src-file>"]:
replaceVersionInFile(Path(filePath), pkg, oldVersion) replaceVersionInFile(Path(filePath), pkg, oldVersion)