Skip to content

Commit 886e583

Browse files
author
lpb-docs
committed
fix(devstack): bump syncs extension pins to the NEW version before committing
Post-stable-release, settings.json pins sit at the stable tag, so the first dev bump failed its own pre-commit validate (pins != VERSION) — and a post-hook sync would target the OLD version since the hook runs before the bump lands. cmd_bump now updates pins to the new VERSION (non-interactive, no-op when settings.json is absent) right after writing it.
1 parent 37ac705 commit 886e583

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

scripts/localpibox/stack/workspace.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,27 @@ def _update_pinned_versions(settings: dict, target_version: str) -> list[tuple[s
450450

451451
# ─── Workspace: sync extension pins ───────────────────────────────────────
452452

453+
def sync_pins_quiet(agent_dir: str | Path, target_version: str,
454+
cons: Console) -> int:
455+
"""Non-interactive pin update: write pins != *target_version* in
456+
settings.json. Returns the number of changed extension(s); 0 if nothing
457+
to do or no settings.json (a missing file is not an error here —
458+
callers treat it as 'nothing to sync'). Used by `lpb-devstack bump`
459+
so a VERSION commit never lands with stale pins (the pre-commit
460+
validate would fail, and a post-hook sync would target the OLD
461+
version)."""
462+
settings = _read_settings(agent_dir)
463+
if settings is None:
464+
return 0
465+
changed = _update_pinned_versions(settings, target_version)
466+
if not changed:
467+
return 0
468+
_write_settings(agent_dir, settings)
469+
for name, old, new in changed:
470+
cons.info(f" pin {name}: {old}{new}")
471+
return len(changed)
472+
473+
453474
def cmd_workspace_sync_pins(pipeline: str, cons: Console) -> int:
454475
"""Sync settings.json extension pins to the pipeline's stack version."""
455476
agent_dir = Path(DEFAULT_AGENT_DIR)

scripts/lpb-devstack

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ from localpibox.stack.workspace import ( # noqa: E402
7373
_resolve_repo_path,
7474
cmd_workspace_status,
7575
cmd_workspace_sync,
76+
sync_pins_quiet,
7677
)
7778
from localpibox.stack.validate import cmd_validate # noqa: E402
7879
from localpibox.stack.release import ( # noqa: E402
@@ -130,6 +131,17 @@ def cmd_bump(cons: Console, *, minor: bool = False, major: bool = False,
130131
vf.write_text(new + "\n")
131132
cons.info(f"VERSION: {current}{new}")
132133

134+
# Keep settings.json pins in lockstep with the new VERSION, BEFORE the
135+
# commit: the pre-commit validate checks pins == VERSION, and syncing
136+
# after the hook would target the OLD version (the hook runs before the
137+
# bump lands). Without this, the first dev bump after a stable release
138+
# (pins still at the stable tag) fails its own gate.
139+
from localpibox.stack.repos import DEFAULT_AGENT_DIR as _agent_dir
140+
n = sync_pins_quiet(_agent_dir, new, cons)
141+
if n:
142+
cons.info(f"Synced {n} extension pin(s) to {new} "
143+
f"(run 'pi update --extensions' after the tags exist)")
144+
133145
if no_commit:
134146
cons.info("--no-commit: file written, not committed (review + commit manually)")
135147
return 0

scripts/test_localpibox_devstack.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from testharness import run_lpbx_suite, _bare_remote, _quiet_console, _load_script, SCRIPTS_DIR
77

88
import os
9+
import json
910
import subprocess
11+
from pathlib import Path
1012
from contextlib import ExitStack
1113
from unittest import mock
1214

@@ -171,6 +173,46 @@ def test_devstack_bump_gate_blocks_and_force_bypasses(tmpdir):
171173
p.stop()
172174

173175

176+
def test_devstack_bump_syncs_pins_before_commit(tmpdir):
177+
"""bump updates settings.json pins to the NEW version before committing
178+
(post-stable-release case: pins sit at the stable tag; without this the
179+
pre-commit validate fails and a post-hook sync would target the old
180+
version)."""
181+
root = tmpdir / "devstack"
182+
root.mkdir()
183+
(root / "VERSION").write_text("0.0.78-lpb-dev\n")
184+
agent = tmpdir / "agent"
185+
agent.mkdir(parents=True, exist_ok=True)
186+
settings = {"packages": [
187+
"git:github.com/lpb-stack/lemonade-pi-plugin@0.0.77-lpb",
188+
"git:github.com/lpb-stack/lpb-memory@0.0.77-lpb",
189+
"git:github.com/lpb-stack/pi-subagents@0.0.77-lpb",
190+
]}
191+
(agent / "settings.json").write_text(json.dumps(settings))
192+
with _devstack_root_patch(root, tmpdir), \
193+
mock.patch.object(repos_mod, "DEFAULT_AGENT_DIR", str(agent)), \
194+
mock.patch.object(ws_mod, "_read_settings",
195+
lambda ad: json.loads((Path(ad) / "settings.json").read_text())), \
196+
mock.patch.object(ws_mod, "_write_settings",
197+
lambda ad, s: (Path(ad) / "settings.json").write_text(json.dumps(s))):
198+
code = ld.cmd_bump(_quiet_console(), no_commit=True)
199+
assert code == 0
200+
pins = json.loads((agent / "settings.json").read_text())
201+
if isinstance(pins, dict):
202+
pins = pins.get("packages", [])
203+
assert all(p.endswith("@0.0.79-lpb-dev") for p in pins)
204+
205+
206+
def test_devstack_bump_missing_settings_is_noop(tmpdir):
207+
"""No settings.json → pin sync is a no-op, bump still succeeds."""
208+
root = tmpdir / "devstack"
209+
root.mkdir()
210+
(root / "VERSION").write_text("0.0.57-lpb-dev\n")
211+
with _devstack_root_patch(root, tmpdir), \
212+
mock.patch.object(repos_mod, "DEFAULT_AGENT_DIR", str(tmpdir / "no-agent")):
213+
assert ld.cmd_bump(_quiet_console(), no_commit=True) == 0
214+
215+
174216
# ─── lpb-devstack: repo tagging ───────────────────────────────────────────
175217

176218
def test_devstack_tag_repos(tmpdir):

0 commit comments

Comments
 (0)