From 06f1e5423fd8f8d8997fbc8bd5b20a92d2c2a4c6 Mon Sep 17 00:00:00 2001 From: Yilin Jing Date: Wed, 9 Sep 2026 12:34:09 -0400 Subject: [PATCH] release: a file holding two version fields gets both MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatching Release with 1.1.0 failed at the bump, which is where it was supposed to fail if it was going to fail at all: the marketplace catalogue holds two of the seven fields, metadata.version and plugins.0.version, and the script had been reading every file before writing any of them. Each write then started from the text read before the other write, so the second one dropped the first and metadata.version stayed at 1.0.0. Reading first is still right — it is what keeps a missing file from leaving a half-versioned tree — so the reads are now keyed by path rather than by field, and every field of a file is applied to one text that is written once. No new test. `--check` immediately after the set is what caught this, it runs in the release workflow and in the local gates, and it fails on exactly the state this bug produces. --- scripts/bump-version.sh | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 3dbfc53..cde7565 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -74,7 +74,9 @@ def write_toml_version(text, parts, value): # Read every file before writing any of them. A bump that stops halfway leaves the # tree half-versioned, which is the one state this script exists to make impossible. -seen, failures, planned = {}, [], [] +# Keyed by path, because the marketplace catalogue holds two of these fields and one +# read per field would mean each write starting from the text before the other's. +seen, failures, planned = {}, [], {} for entry in spec["files"]: path = root / entry["path"] @@ -89,7 +91,9 @@ for entry in spec["files"]: failures.append(f"{entry['path']} is listed in .version-bump.json but does not exist") continue - text = path.read_text() + if path not in planned: + planned[path] = (path.read_text(), []) + text, fields = planned[path] if path.suffix == ".toml": current = read_toml_version(text, parts) @@ -97,7 +101,7 @@ for entry in spec["files"]: current = get_in(json.loads(text), parts) seen[label] = current - planned.append((path, parts, label, text, current)) + fields.append((parts, label, current)) if failures: print("", file=sys.stderr) @@ -107,17 +111,22 @@ if failures: sys.exit(1) if mode == "set": - for path, parts, label, text, current in planned: - if current == version: - print(f" = {label} already {version}") - continue - if path.suffix == ".toml": - path.write_text(write_toml_version(text, parts, version)) - else: - data = json.loads(text) - set_in(data, parts, version) - path.write_text(json.dumps(data, indent=2) + "\n") - print(f" → {label} {current} → {version}") + # Every field of a file is applied to one text, which is then written once. + for path, (text, fields) in planned.items(): + updated = text + for parts, label, current in fields: + if current == version: + print(f" = {label} already {version}") + continue + if path.suffix == ".toml": + updated = write_toml_version(updated, parts, version) + else: + data = json.loads(updated) + set_in(data, parts, version) + updated = json.dumps(data, indent=2) + "\n" + print(f" → {label} {current} → {version}") + if updated != text: + path.write_text(updated) if mode == "check": distinct = sorted(set(seen.values()))