stamparm/maltrail · error · SystemExit
[!] cannot bump %r - expected 'major.minor'
Error message
[!] cannot bump %r - expected 'major.minor'
What it means
next_minor() parses the current version string expecting at least two dot-separated numeric components ('major.minor'). If the version read from core/settings.py does not have two leading digit-only components, it refuses to bump and exits. This guards against silently producing a malformed version.
Solutions
- Fix the VERSION value in core/settings.py to the exact 'major.minor' form, e.g. VERSION = "0.53".
- If you intend a different scheme, bypass --next and pass the full target explicitly: python bump_version.py --version 0.54 (it still must match \d+\.\d+).
- Strip suffixes like -rc1 or build metadata from VERSION before running the bump script.
Example fix
// before (core/settings.py) VERSION = "0.5-rc1" // after VERSION = "0.5"
Defensive patterns
Strategy: validation
Validate before calling
import re
v = open('core/settings.py').read()
m = re.search(r'^VERSION\s*=\s*"([^"]+)"', v, re.M)
if not m or not re.match(r'^\d+\.\d+$', m.group(1)):
raise RuntimeError('VERSION must be major.minor, e.g. "0.53"')
Type guard
import re
def is_major_minor(version: str) -> bool:
return bool(re.match(r'^\d+\.\d+$', version))
Try / catch
try:
bump_version.main(['--next'])
except SystemExit as e:
if 'cannot bump' in str(e):
normalize_version_string()
Prevention
- Store VERSION strictly as digits.digits - no suffixes or single numbers
- Validate VERSION format in CI before release steps
- Keep pre-release metadata out of core/settings.py; track it elsewhere
When it happens
Trigger: Calling next_minor(version) (via --next in main) where version.split('.') yields fewer than 2 parts, or parts[0]/parts[1] are non-numeric (e.g. VERSION = "0" or VERSION = "0.beta").
Common situations: core/settings.py holds a single-component version (0), a word (dev), a suffix (0.5-rc1 where '5-rc1' isn't digits), or someone manually edited the version string.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- [!] no VERSION assignment in core/settings.py
- [!] : expected exactly 1 version line, matched Refusing to…
- [!] : expected exactly 1 date-released line, matched
- [!] %r is not 'major.minor' - Maltrail versions two…
- [!] no VERSION assignment found in
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/7ca34957e773bdf2.
Report an issue: GitHub.
Appendix: source
Thrown at sensor/tools/bump_version.py:79
(os.path.join(ROOT, "CITATION.cff"),
r'^(version\s*:\s*")[^"]*(")', r'\g<1>%s\g<2>' % version),
(os.path.join(ROOT, "sensor", "src", "settings_gen.rs"),
r'^(pub const VERSION: &str = ")[^"]*(";)', r'\g<1>%s\g<2>' % version),
]
def current():
match = re.search(r'^VERSION\s*=\s*"([^"]+)"',
_read(os.path.join(ROOT, "core", "settings.py")), re.M)
if not match:
raise SystemExit("[!] no VERSION assignment in core/settings.py")
return match.group(1)
def next_minor(version):
parts = version.split(".")
if len(parts) < 2 or not all(p.isdigit() for p in parts[:2]):
raise SystemExit("[!] cannot bump %r - expected 'major.minor'" % version)
return "%s.%d" % (parts[0], int(parts[1]) + 1)
def apply(version, date_released=None):
"""Rewrite every file, or none of them."""
planned = []
for path, pattern, repl in _edits(version):
text = _read(path)
new, n = re.subn(pattern, repl, text, flags=re.M)
# Exactly once. Zero means the file moved on and this script is now lying about
# covering it; more than once means the pattern is catching something it should not
# (a dependency's version key, say) and would corrupt the file.
if n != 1:
raise SystemExit("[!] %s: expected exactly 1 version line, matched %d\n"
" Refusing to write ANY file - a half-applied bump is the "
"failure this script exists to prevent." % (path, n))
planned.append((path, new, text != new))View on GitHub (pinned to 77cfb06d76)