Skip to content

A version bump that got left behind — fixing it with a single source of truth and a –check gate

A version bump that got left behind — fixing it with a single source of truth and a --check gate

Bumping a desktop app’s version number sounds like a one-line change. In practice it can mean editing five different files at once. During one release, exactly one of those files got missed, and an installer nearly got built carrying the old version number.

Note: A “version bump” just means incrementing the version number — say, from 1.6.9 to 1.6.10. The operation sounds trivial, but it can end up scattered across multiple files in practice.

What happened

This app’s version number lives in five separate places:

  • The VERSION variable in version.py
  • MyAppVersion in the Windows installer definition file (.iss)
  • The version number and download URLs in version.json
  • Two download links on the landing page (Japanese and English versions)

Every release, these got updated by hand, relying on memory of “which files need touching this time.” During one release, the .iss file on the Windows build side was left at the old version — and it only surfaced when someone happened to notice, visually, that the version number still looked old right before rebuilding. There was no automated check that would have caught it.

Why memory-based manual edits invite this kind of failure

Five files isn’t a huge number on its own, but the list of “files to touch this release” lived entirely in someone’s head. Succeeding once is no guarantee the same steps get reproduced correctly next time. On top of that, each file uses a different syntax (a Python variable assignment, an INI-style definition, JSON, link strings inside PHP), which makes a quick grep-based sanity check more tedious than it sounds.

The fix — list every target file in one place, and fold verification into the same script

tools/bump_version.py centralizes the list of target files and their matching patterns into a single script.

VERSION_TARGETS = [
    {"path": "version.py",
     "checks": [{"regex": r'^VERSION\s*=\s*"([^"]+)"',
                 "replace": 'VERSION = "{version}"', "label": "VERSION"}]},
    {"path": "WP_Maintenance_Pro.iss",
     "checks": [{"regex": r'#define\s+MyAppVersion\s+"([^"]+)"',
                 "replace": '#define MyAppVersion   "{version}"',
                 "label": "MyAppVersion"}]},
    # ... version.json and both index.php files (JP/EN) listed the same way
]

The script has two modes:

# Bump everything from the old version to the new one
python tools/bump_version.py 1.6.11 2026-06-11

# Don't bump anything — just verify current consistency
python tools/bump_version.py --check

--check mode treats VERSION in version.py as the source of truth and verifies every other file against it. If even one mismatch turns up, it prints exactly which file, which field, and what value it found, then exits with code 1.

def check_all(expected_version):
    mismatches = []
    for target in VERSION_TARGETS:
        content = _read(os.path.join(PROJECT_ROOT, target["path"]))
        for chk in target["checks"]:
            m = re.search(chk["regex"], content)
            actual = m.group(1) if m else "<not found>"
            if actual != expected_version:
                mismatches.append((target["path"], chk["label"], actual, expected_version))
    return mismatches

Wiring the check into the build script as a gate

Running --check on its own still leaves the same problem: someone has to remember to run it. So the verification gets wired directly into the app’s build script, which refuses to proceed if anything’s inconsistent.

_check_result = subprocess.run(
    [sys.executable, "tools/bump_version.py", "--check"],
    capture_output=True, text=True,
)
if _check_result.returncode != 0:
    print("Release file version consistency check failed")
    print(_check_result.stdout)
    sys.exit(1)

This means the check always runs before signing and distribution, regardless of whether the person building remembers --check exists. Because the same script runs on both Mac and Windows, it also doubles as a cross-platform consistency check.

What this design gets right

This isn’t really about “editing a version string” — it’s about answering who guarantees consistency across files scattered in multiple places, and where. Centralizing the list of target files in one script makes it obvious where to add a new file when the release process grows. And by folding “bump” and “verify” into two modes of the same script, rather than two separately maintained pieces of logic, there’s no duplicate implementation of the matching rules to drift out of sync.

Summary

Aspect Detail
Problem Version number scattered across 5 files; memory-based manual updates dropped one of them
Fix Centralize target files and regex patterns in one script with both bump and verify modes
Verification Treat version.py as the source of truth, regex-match every other file against it, report specific mismatches
Permanence Wire the check into the build script as a gate, removing the dependency on someone remembering to run it

Consistency across multiple files, even once fixed by hand, tends to drift again on the next pass. The core of this design is refusing to let verification depend on a choice — instead pinning it to a path (the build) that always gets taken anyway. The same principle — not perfecting the logic, but mechanically catching it when an invariant breaks — shows up again in the count sanity check on a reorder API.


Follow-up: The cp932 crash in the build gate that only happened on Windows