feat(bump_project_version): bump Project Information.Version one part
New MCP tool that increments one part of the 4-part
Project Information.Version field of the primary project, saves the
project, and reports the before/after.
Behaviour:
- level=major -> bump major, reset minor/revision/build to 0
- level=minor -> bump minor, reset revision/build to 0
- level=revision-> bump revision, reset build to 0
- level=build -> bump build only
Convention follows the rest of CODESYS / 3S / WAGO library practice
(visible in any X33 library reference like 'WagoAppCanLayer2,
1.6.1.4 (WAGO)'):
Major -- incompatible API break (rename FB, change public
signature, remove method).
Minor -- backward-compatible feature add.
Revision -- bug fix only, no API change.
Build -- internal / CI counter, often 0 for hand-released.
Implementation notes:
- Project Information lives as the first child node of the project
root. Its .version property is read/written directly; IronPython
coerces strings like '1.2.3.4' to a System.Version on assignment,
str(System.Version) gives the dotted form back. None / empty /
unset is treated as '0.0.0.0'.
- Verified live against X33 (MRCodesysX33_0021): set version to
'1.0.0.0' from None, project.save() persisted it; reload via
primary_project.get_children() found the same value. Probe done
via the inject-once.mjs bridge against the live watcher in PID
23056 before this commit landed.
- project.save() is called after the bump so the new value sticks
in the .project file. Soft-fails on save error (visible WARNING
in DEBUG output but the bump itself is still reported as
successful) so a save permission glitch doesn't mask the actual
version change.
Used by the X33 GitLab project's "version in README header" workflow:
the version surfaces at the top of README.md (and at the top of the
library list once list_project_libraries gets enriched in a follow-up
commit).
This commit is contained in:
parent
3e2467149f
commit
2ed3f17dc7
2 changed files with 134 additions and 0 deletions
107
src/scripts/bump_project_version.py
Normal file
107
src/scripts/bump_project_version.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import sys, scriptengine as script_engine, traceback
|
||||
|
||||
# Bumps one part of the 4-part Project Information.version field of the
|
||||
# primary project. Convention (per CODESYS / 3S / WAGO library practice):
|
||||
#
|
||||
# Major -- bump on incompatible API break.
|
||||
# Minor -- bump on backward-compatible feature add.
|
||||
# Revision -- bump on bug fix only (no API change).
|
||||
# Build -- internal counter, often 0 for hand-released versions.
|
||||
#
|
||||
# Bumping a higher part resets all lower parts to 0 (e.g. bumping minor
|
||||
# resets revision and build).
|
||||
#
|
||||
# The Version field is read/written as a property on the "Project
|
||||
# Information" node (first child of the primary project). IronPython
|
||||
# coerces strings like "1.2.3.4" to System.Version automatically; we
|
||||
# stringify on the read side because str(System.Version) gives
|
||||
# the dotted form back. None / empty are treated as "0.0.0.0".
|
||||
|
||||
LEVEL = "{LEVEL}" # major | minor | revision | build
|
||||
|
||||
VALID_LEVELS = ('major', 'minor', 'revision', 'build')
|
||||
|
||||
|
||||
def parse_version(v):
|
||||
"""Parse a version-like value into a 4-tuple of ints, defaulting missing
|
||||
parts to 0. Accepts None, '', '1.2', '1.2.3', '1.2.3.4', or a
|
||||
System.Version. Raises ValueError on anything that can't be parsed."""
|
||||
if v is None:
|
||||
return (0, 0, 0, 0)
|
||||
s = str(v).strip()
|
||||
if not s or s == 'None':
|
||||
return (0, 0, 0, 0)
|
||||
parts = s.split('.')
|
||||
if len(parts) > 4:
|
||||
raise ValueError("version '%s' has more than 4 parts" % s)
|
||||
nums = []
|
||||
for p in parts:
|
||||
try:
|
||||
nums.append(int(p))
|
||||
except ValueError:
|
||||
raise ValueError("version '%s' has non-integer part '%s'" % (s, p))
|
||||
while len(nums) < 4:
|
||||
nums.append(0)
|
||||
return tuple(nums)
|
||||
|
||||
|
||||
def bump(parts, level):
|
||||
major, minor, revision, build = parts
|
||||
if level == 'major':
|
||||
return (major + 1, 0, 0, 0)
|
||||
if level == 'minor':
|
||||
return (major, minor + 1, 0, 0)
|
||||
if level == 'revision':
|
||||
return (major, minor, revision + 1, 0)
|
||||
if level == 'build':
|
||||
return (major, minor, revision, build + 1)
|
||||
raise ValueError("unknown bump level '%s' (must be one of %s)" % (level, ', '.join(VALID_LEVELS)))
|
||||
|
||||
|
||||
try:
|
||||
if LEVEL not in VALID_LEVELS:
|
||||
raise ValueError("level must be one of %s, got '%s'" % (', '.join(VALID_LEVELS), LEVEL))
|
||||
|
||||
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
||||
|
||||
pi = None
|
||||
for child in primary_project.get_children(False):
|
||||
try:
|
||||
if child.get_name() == 'Project Information':
|
||||
pi = child
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if pi is None:
|
||||
raise RuntimeError(
|
||||
"Project Information node not found at the project root. "
|
||||
"Every CODESYS project should have one as its first child; "
|
||||
"if missing, recreate it via Project menu -> Project Information.")
|
||||
|
||||
before_raw = pi.version
|
||||
before_str = str(before_raw) if before_raw is not None else None
|
||||
before_parts = parse_version(before_raw)
|
||||
after_parts = bump(before_parts, LEVEL)
|
||||
after_str = '%d.%d.%d.%d' % after_parts
|
||||
|
||||
print("DEBUG: bump_project_version: level=%s before=%s -> after=%s" % (
|
||||
LEVEL, before_str, after_str))
|
||||
|
||||
pi.version = after_str
|
||||
|
||||
try:
|
||||
primary_project.save()
|
||||
print("DEBUG: project.save() succeeded after version bump.")
|
||||
except Exception as save_e:
|
||||
print("WARNING: project.save() raised %s -- bump applied in memory but may not persist across IDE close." % save_e)
|
||||
|
||||
print("Project Information.Version: %s -> %s" % (before_str, after_str))
|
||||
print("SCRIPT_SUCCESS: bump_project_version complete.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
detailed = traceback.format_exc()
|
||||
msg = "Error in bump_project_version for project '%s': %s\n%s" % (
|
||||
PROJECT_FILE_PATH, e, detailed)
|
||||
print(msg)
|
||||
print("SCRIPT_ERROR: %s" % msg)
|
||||
sys.exit(1)
|
||||
|
|
@ -1346,6 +1346,33 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
}
|
||||
);
|
||||
|
||||
// ─── Project metadata ────────────────────────────────────────────────
|
||||
|
||||
s.tool(
|
||||
'bump_project_version',
|
||||
"Bumps one part of the 4-part Project Information.Version field of the primary project (Major.Minor.Revision.Build) and saves the project. Convention: major = incompatible API break (rename FB / change public signature / remove method); minor = backward-compatible feature add (new FB / GVL / method); revision = bug fix only; build = internal counter, often 0 for hand-released versions. Bumping a higher part resets all lower parts to 0 (e.g. bumping minor resets revision and build to 0). Treats None / empty / unset version as '0.0.0.0'. The Version is exposed at runtime via the Project Information library's GetVersion() helper, which IEC code can call to surface the running version.",
|
||||
{
|
||||
projectFilePath: z.string().describe("Path to the project file."),
|
||||
level: z.enum(['major', 'minor', 'revision', 'build']).describe("Which part of the 4-part version to bump. Major = incompatible API break. Minor = backward-compatible feature add. Revision = bug fix only. Build = internal / CI counter."),
|
||||
},
|
||||
async (args: { projectFilePath: string; level: 'major' | 'minor' | 'revision' | 'build' }) => {
|
||||
const escaped = resolvePath(args.projectFilePath, workspaceDir);
|
||||
const script = scriptManager.prepareScriptWithHelpers(
|
||||
'bump_project_version',
|
||||
{
|
||||
PROJECT_FILE_PATH: escaped,
|
||||
LEVEL: args.level,
|
||||
},
|
||||
['ensure_project_open']
|
||||
);
|
||||
const result = await executor.executeScript(script);
|
||||
return formatToolResponse(
|
||||
result,
|
||||
`bump_project_version (${args.level}) complete for ${args.projectFilePath}.`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// ─── Filesystem mirror (Phase 1: read-only export) ────────────────────
|
||||
|
||||
s.tool(
|
||||
|
|
|
|||
Loading…
Reference in a new issue