5 Commits

Author SHA1 Message Date
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
Jonathan Bernard f3ac85c834 Add support for updating const's defined in Nim source files. 2025-06-15 05:25:51 -05:00
5 changed files with 315 additions and 83 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_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)
"""
+181 -82
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,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
@@ -57,21 +81,22 @@ Details:
Specifically it uses the following regexes: Specifically it uses the following regexes:
TODO Nim: ^\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.0" const UV_VERSION = "1.2.0"
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
@@ -85,9 +110,29 @@ type PackageVersion = object
# See also: # See also:
# https://regex101.com/r/Ly7O1x/3/ # https://regex101.com/r/Ly7O1x/3/
# #
let SemVerPattern = 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 NodeConstPattern =
"^\\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)
@@ -146,7 +191,81 @@ proc writePackage(pkg: PackageVersion) =
writeFile($pkg.file, pkg.nodePackage.pretty) writeFile($pkg.file, pkg.nodePackage.pretty)
proc incrementLastVersionPart(version: string): string = proc replaceVersionInSource*(
source: string,
pkg: PackageVersion,
vOld: string): string =
let rgx =
case pkg.lang
of lNim: re(NimConstPattern % [vOld.replace(".", "\\.")])
of lNode: re(NodeConstPattern % [vOld.replace(".", "\\.")])
var newLines = newSeq[string]()
for l in source.splitLines():
let mOpt = l.match(rgx)
if mOpt.isSome:
let m = mOpt.get
newLines.add(
l[0..<m.captureBounds[0].a] &
pkg.version &
l[m.captureBounds[0].b+1 .. ^1])
else: newLines.add(l)
return newLines.join("\p")
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])
@@ -156,22 +275,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, SemVerPattern)
if matchOpt.isNone: if matchOpt.isNone:
raise newException(ValueError, raise newException(ValueError,
@@ -179,8 +288,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])
@@ -193,42 +302,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(".")
@@ -241,40 +319,61 @@ when isMainModule:
else: else:
detectAndParsePackage(dir) detectAndParsePackage(dir)
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>"]:
replaceVersionInFile(Path(filePath), pkg, oldVersion)
echo pkg.version echo pkg.version
elif args["set"]: elif args["set"]:
pkg.version = $args["<new-version>"]
writePackage(pkg) 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>"]:
replaceVersionInFile(Path(filePath), pkg, oldVersion)
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
elif args["interactive"]: elif args["interactive"]:
var acceptNewVersion = false var acceptNewVersion = false
var newVersion = pkg.version
while not acceptNewVersion: while not acceptNewVersion:
stdout.writeLine "Current version is " & pkg.version stdout.writeLine "Current version is " & oldVersion
stdout.write "New version? " stdout.write "New version? "
newVersion = stdin.readLine pkg.version = stdin.readLine
stdout.write "New version will be set to '" & pkg.version & "'. Is this correct (yes/no)? " stdout.write "New version will be set to '" & pkg.version & "'. Is this correct (yes/no)? "
let isCorrect = stdin.readLine let isCorrect = stdin.readLine
let acceptNewVersion = "yes".startsWith(isCorrect.toLower) acceptNewVersion = "yes".startsWith(isCorrect.toLower)
pkg.version = newVersion if not args["--skip-package-file"]:
echo "Updating version definition in " & $pkg.file echo "Updating version definition in " & $pkg.file
writePackage(pkg) writePackage(pkg)
# TODO: for filePath in args["<src-file>"]:
# for f in filesToUpdate: echo "Updating version definition in " & $filePath
# echo "Updating version definition in " & $f replaceVersionInFile(Path(filePath), pkg, oldVersion)
# updateVersionInFile(f)
+1 -1
View File
@@ -1,6 +1,6 @@
# Package # Package
version = "1.0.1" version = "1.2.0"
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"