6 Commits

Author SHA1 Message Date
Jonathan Bernard d3f2b8fc6d Migrate to nitely/nim-regex away from std/nre.
PR Validation / Unit Tests (pull_request) Successful in 22s
2026-03-09 21:42:57 -05:00
Jonathan Bernard 4ff6d2bc95 Fix bug when loading node package file.
PR Validation / Unit Tests (pull_request) Successful in 1m8s
2025-07-17 14:07:42 -05:00
Jonathan Bernard edda65c0c0 Add --skip-package-file option. 2025-07-17 13:16:05 -05:00
Jonathan Bernard 7aebd02140 Bump version to 1.1.0 2025-06-17 20:59:24 -05:00
Jonathan Bernard 8f71630e78 Add the ability to directly set version parts.
PR Validation / Unit Tests (pull_request) Successful in 15s
- Move unittests to a dedicated file.
- Rework CLI making version part selection an option that can be passed
  into many commands.
- Add support for updates to version constants defined in soruce files.
2025-06-17 20:50:33 -05:00
Jonathan Bernard 833fc35adf Add Gitea Actions workflow to run unit tests before allowing merge to main.
PR Validation / Unit Tests (pull_request) Successful in 44s
2025-06-15 05:30:11 -05:00
5 changed files with 299 additions and 104 deletions
+34
View File
@@ -0,0 +1,34 @@
---
name: PR Validation
on:
pull_request:
branches:
- main
jobs:
unittest:
name: Unit Tests
runs-on: [pbm_api_cicd]
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Cache dependecies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: |
~/.nimble
key: ${{ runner.os }}-nimble-${{ hashFiles('update_version.nimble') }}
restore-keys: |
${{ runner.os }}-nimble-
- name: Build unittests
run: |
printf "🔨\033[0;32m Compiling unit tests...\033[0m\n" >&1
nimble c tupdate_version.nim
- name: Run unittests
run: |
printf "🧪\033[0;32m Running unit tests...\033[0m\n" >&1
./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_alpha.1-prerelease") == "cicd_alpha.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)
"""
+163 -101
View File
@@ -1,13 +1,11 @@
import std/[dirs, json, paths, sequtils, strutils, syncio, tables] import std/[dirs, json, paths, sequtils, strutils, syncio, tables]
import std/nre except toSeq import docopt, regex, 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 +16,47 @@ 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'
--skip-package-file Don't write changes to the package file (*.nimble or
package.json). This is useful mostly in temporary
circumstances where you want to update other source
files without updating the package file. Be careful
in these cases, however, as update_version uses the
current version (as reported in the package file) as
the canonical version. When updating versions in
other source files, update_version ignores lines that
do not match the current version. Consider the
following:
update_version set 1.0.4 file1.nim, file2.nim --skip-package-file # works
update_version set 1.0.0 file1.nim, file2.nim --skip-package-file # fails
The second command will *not* rewrite the version
strings in file1.nim and file2.nim because the
versions in those files (1.0.4) do not match what is
in the package.
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 +84,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.2.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 +109,29 @@ type PackageVersion = object
# See also: # See also:
# https://regex101.com/r/Ly7O1x/3/ # https://regex101.com/r/Ly7O1x/3/
# #
let SemVerRegex = const 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-]+)*))?$" re2"^(?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)
@@ -115,8 +154,9 @@ proc parseNimblePackage(dir: Path): PackageVersion =
proc parseNodePackage(dir: Path): PackageVersion = proc parseNodePackage(dir: Path): PackageVersion =
result = PackageVersion( result = PackageVersion(
file: dir / Path("package.json"), file: dir / Path("package.json"),
lang: lNode, lang: lNode)
nodePackage: parseFile($result.file))
result.nodePackage = parseFile($result.file)
if not result.nodePackage.hasKey("name") or if not result.nodePackage.hasKey("name") or
not result.nodePackage.hasKey("version") or not result.nodePackage.hasKey("version") or
@@ -134,7 +174,7 @@ proc detectAndParsePackage(dir: Path): PackageVersion =
except: except:
let nimEx = getCurrentException() let nimEx = getCurrentException()
try: result = parseNodePackage(dir) try: result = parseNodePackage(dir)
except: except Exception:
let nodeEx = getCurrentException() let nodeEx = getCurrentException()
stderr.writeLine( stderr.writeLine(
"Unable to find a package definitions file. Errors:\n\t$#\n\t$#" % "Unable to find a package definitions file. Errors:\n\t$#\n\t$#" %
@@ -151,108 +191,116 @@ 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: re2(NimConstPattern % [vOld.replace(".", "\\.")])
of lNode: re(NodeConstPattern % [vOld.replace(".", "\\.")]) of lNode: re2(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) var m = RegexMatch2()
if mOpt.isSome: if match(l, rgx, m):
let m = mOpt.get
newLines.add( newLines.add(
l[0..<m.captureBounds[0].a] & l[0..<m.group(0).a] &
pkg.version & pkg.version &
l[m.captureBounds[0].b+1 .. ^1]) l[m.group(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) =
let versionParts = toSeq(findIter(version, re"([^\d.]+)?\.?(\d+)")) writeFile($file, replaceVersionInSource(readFile($file), pkg, vOld))
let lastVersionPartMatch = versionParts[^1]
let lastVersionPartInt = parseInt(lastVersionPartMatch.captures[1])
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 = findAll(version, re2"([^\d.]+)?\.?(\d+)")
let lastVersionPart = versionParts[^1]
return return
version[0..<lastVersionPartMatch.captureBounds[1].a] & version[0..<lastVersionPart.group(1).a] &
$(lastVersionPartInt + 1) & newVersion &
version[lastVersionPartMatch.captureBounds[1].b+1 .. ^1] version[lastVersionPart.group(1).b+1 .. ^1]
let VERSION_DEFAULTS: TableRef[SemVerParts, string] = newTable([ proc setSemverPart*(
(major, "0"),
(minor, "0"),
(patch, "0"),
(prerelease, ""),
(buildmetadata, "")])
proc incrementSemverPart(
version: string, version: string,
part: SemVerParts, part: SemverParts,
newVersionPart: string,
defaults = VERSION_DEFAULTS): string = defaults = VERSION_DEFAULTS): string =
result = "" var m: RegexMatch2
if not match(version, SemverRegex, m):
let matchOpt = match(version, SemVerRegex)
if matchOpt.isNone:
raise newException(ValueError, raise newException(ValueError,
"Version [$#] is not a valid Semantic Version number" % version) "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 m.group($p).b < m.group($p).a or p > part: versionParts[p] = defaults[p]
else: versionParts[p] = version[m.group($p)]
let versionParts = newTable[SemVerParts, string]() result = fmtSemver(versionParts)
for p in SemVerParts.items: if not match(result, SemverRegex):
raise newException(ValueError,
"Refusing to update: Version [$#] would not be a valid Semantic Version number" % result)
proc incrementLastVersionPart*(version: string): string =
let versionParts = findAll(version, re2"([^\d.]+)?\.?(\d+)")
let lastVersionPart = versionParts[^1]
let lastVersionPartInt = parseInt(version[lastVersionPart.group(1)])
return
version[0..<lastVersionPart.group(1).a] &
$(lastVersionPartInt + 1) &
version[lastVersionPart.group(1).b+1 .. ^1]
proc incrementSemverPart*(
version: string,
part: SemverParts,
defaults = VERSION_DEFAULTS): string =
var m: RegexMatch2
if not match(version, SemverRegex, m):
raise newException(ValueError,
"Version [$#] is not a valid Semantic Version number" % version)
let versionParts = newTable[SemverParts, string]()
for p in SemverParts.items:
if p == part: if p == part:
if m.captures.contains($p): if not m.group($p).b < m.group($p).a:
versionParts[p] = incrementLastVersionPart(m.captures[$p]) versionParts[p] = incrementLastVersionPart(version[m.group($p)])
elif p < prerelease or defaults[p].len > 0: elif p < prerelease or defaults[p].len > 0:
versionParts[p] = defaults[p] versionParts[p] = defaults[p]
# if this part specifically has been requested to be incremented but # if this part specifically has been requested to be incremented but
# the default is empty, we are still going to give it *something* # the default is empty, we are still going to give it *something*
elif p == prerelease: versionParts[p] = "prerelease.0" elif p == prerelease: versionParts[p] = "prerelease.0"
elif p == buildmetadata: versionParts[p] = "build.0" elif p == buildmetadata: versionParts[p] = "build.0"
elif not m.captures.contains($p) or p > part: versionParts[p] = defaults[p] elif m.group($p).b < m.group($p).a or p > part: versionParts[p] = defaults[p]
else: versionParts[p] = m.captures[$p] else: versionParts[p] = version[m.group($p)]
result = "$#.$#.$#" % [
versionParts[major], versionParts[minor], versionParts[patch] ]
if versionParts[prerelease].len > 0:
result &= "-" & versionParts[prerelease]
if versionParts[buildmetadata].len > 0:
result &= "+" & versionParts[buildmetadata]
return fmtSemver(versionParts)
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,13 +316,18 @@ 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) if not args["--skip-package-file"]:
writePackage(pkg)
for filePath in args["<src-file>"]: for filePath in args["<src-file>"]:
replaceVersionInFile(Path(filePath), pkg, oldVersion) replaceVersionInFile(Path(filePath), pkg, oldVersion)
@@ -282,12 +335,20 @@ 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)
writePackage(pkg) if not args["--skip-package-file"]:
writePackage(pkg)
echo pkg.version echo pkg.version
elif args["get"]: echo pkg.version elif args["get"]: echo pkg.version
@@ -303,8 +364,9 @@ when isMainModule:
let isCorrect = stdin.readLine let isCorrect = stdin.readLine
acceptNewVersion = "yes".startsWith(isCorrect.toLower) acceptNewVersion = "yes".startsWith(isCorrect.toLower)
echo "Updating version definition in " & $pkg.file if not args["--skip-package-file"]:
writePackage(pkg) echo "Updating version definition in " & $pkg.file
writePackage(pkg)
for filePath in args["<src-file>"]: for filePath in args["<src-file>"]:
echo "Updating version definition in " & $filePath echo "Updating version definition in " & $filePath
+2 -2
View File
@@ -1,6 +1,6 @@
# Package # Package
version = "1.0.2" version = "1.2.2"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Small util to update version consistently for nim and node packages." description = "Small util to update version consistently for nim and node packages."
license = "MIT" license = "MIT"
@@ -10,4 +10,4 @@ bin = @["update_version"]
# Dependencies # Dependencies
requires @["nim >= 1.0.4", "docopt >= 0.7.1", "zero_functional"] requires @["nim >= 1.0.4", "docopt >= 0.7.1", "regex", "zero_functional"]