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

  1. Fix the VERSION value in core/settings.py to the exact 'major.minor' form, e.g. VERSION = "0.53".
  2. 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+).
  3. 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

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


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)