Skip to content

Commit bd05099

Browse files
UN-3479 [FIX] Address Greptile: re-run idempotency for lookups versions + MR api keys
- lookups: adopt existing target published versions by name instead of re-publishing (fixes spurious failures / duplicate version history on re-runs and adopted definitions) - lookups: guard draft_cache so a failed fetch can't clobber a peer's valid draft id (TOCTOU) - manual_review: adopt ReviewApiKey by (class_name, description) so re-runs don't create duplicate keys; only warn for keys actually minted - tests for both adopt paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CsGrHbs5SWmQkKqiimg6CF
1 parent 3b23b17 commit bd05099

4 files changed

Lines changed: 148 additions & 11 deletions

File tree

src/unstract/clone/phases/lookups.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ def _replay_versions(
303303
"""Reproduce the source's published versions on the target in
304304
``version_number`` order: set the target draft to each version's
305305
content + files, then publish. Records a ``lookup_version`` remap per
306-
published version so its pinned assignments resolve.
306+
published version so its pinned assignments resolve. A target version
307+
with the same ``version_name`` is adopted (no re-publish) so re-runs
308+
and adopted definitions stay idempotent.
307309
"""
308310
try:
309311
versions = self.ctx.source.list_lookup_versions(src_lookup_id)
@@ -316,11 +318,41 @@ def _replay_versions(
316318
)
317319
return
318320

321+
# Existing target versions — re-publishing a name that already exists
322+
# would error or duplicate history; adopt those instead.
323+
try:
324+
tgt_versions = self.ctx.target.list_lookup_versions(tgt_lookup_id)
325+
except Exception as e:
326+
logger.warning(
327+
"lookup '%s': target version listing failed: %s", name, e
328+
)
329+
tgt_versions = []
330+
tgt_by_name: dict[str, str] = {
331+
v["version_name"]: str(v["version_id"])
332+
for v in tgt_versions
333+
if v.get("version_name") and v.get("version_id")
334+
}
335+
319336
published = sorted(
320337
(v for v in versions if not v.get("is_draft")),
321338
key=lambda v: v.get("version_number") or 0,
322339
)
323340
for v in published:
341+
existing_id = tgt_by_name.get(v.get("version_name"))
342+
if existing_id is not None:
343+
src_version_id = v.get("version_id")
344+
with lock:
345+
result.adopted += 1
346+
if src_version_id:
347+
self.ctx.remap.record(
348+
"lookup_version", str(src_version_id), existing_id
349+
)
350+
logger.info(
351+
"lookup '%s': adopted published version '%s' (already present)",
352+
name,
353+
v.get("version_name"),
354+
)
355+
continue
324356
self._replay_one_version(
325357
name, src_lookup_id, tgt_lookup_id, v, result, lock
326358
)
@@ -825,5 +857,8 @@ def _target_draft_version(
825857
logger.warning("target lookup %s draft fetch failed: %s", tgt_lookup_id, e)
826858
draft_id = None
827859
with lock:
828-
draft_cache[tgt_lookup_id] = draft_id
829-
return draft_id
860+
# A racing peer may have already cached a valid id; the GET is
861+
# read-only so a real id always wins over this thread's failure.
862+
if draft_cache.get(tgt_lookup_id) is None:
863+
draft_cache[tgt_lookup_id] = draft_id
864+
return draft_cache[tgt_lookup_id]

src/unstract/clone/phases/manual_review.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,30 @@ def _clone_api_keys(self, result: PhaseResult) -> None:
401401
logger.info("[dry-run] would recreate %d review api key(s)", len(src_keys))
402402
return
403403

404-
# The api_key secret is server-minted and non-copyable; recreating
405-
# yields a NEW secret, so external consumers must be re-wired.
406-
result.warnings.append(
407-
f"{len(src_keys)} review API key(s) recreated with freshly minted "
408-
"secrets — the original key values cannot be copied; re-wire any "
409-
"external consumers to the new keys"
410-
)
404+
# ReviewApiKey has no cross-org natural key (api_key is server-minted),
405+
# so adopt by (class_name, description) to stay idempotent on re-runs.
406+
try:
407+
tgt_keys = self.ctx.target.list_review_api_keys()
408+
except Exception as e:
409+
logger.exception(
410+
"manual_review: failed to list target review api keys: %s", e
411+
)
412+
result.failed += 1
413+
result.errors.append(f"list target review_api_keys: {e}")
414+
return
415+
existing = {
416+
(k.get("class_name"), k.get("description")) for k in tgt_keys
417+
}
418+
419+
recreated = 0
411420
for src in src_keys:
421+
if (src.get("class_name"), src.get("description")) in existing:
422+
result.adopted += 1
423+
logger.info(
424+
"adopted review api key class=%s (already present)",
425+
src.get("class_name"),
426+
)
427+
continue
412428
payload = {
413429
k: src[k]
414430
for k in ("class_name", "description", "is_active")
@@ -424,4 +440,14 @@ def _clone_api_keys(self, result: PhaseResult) -> None:
424440
result.errors.append(f"create review_api_key: {e}")
425441
continue
426442
result.created += 1
427-
logger.info("recreated %d review api key(s) on target", len(src_keys))
443+
recreated += 1
444+
445+
# The api_key secret is server-minted and non-copyable; newly created
446+
# keys get a NEW secret, so external consumers must be re-wired.
447+
if recreated:
448+
result.warnings.append(
449+
f"{recreated} review API key(s) recreated with freshly minted "
450+
"secrets — the original key values cannot be copied; re-wire any "
451+
"external consumers to the new keys"
452+
)
453+
logger.info("recreated %d review api key(s) on target", recreated)

tests/clone/test_lookups_phase.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,66 @@ def test_published_version_replayed_publishes_in_order_and_records_remap():
460460
assert remap.resolve("lookup_version", "src-v2") is not None
461461

462462

463+
def test_published_version_adopted_on_rerun_no_republish():
464+
# Target already has a same-name definition with the same published version
465+
# names: re-run must adopt them (no re-publish) and still record remaps.
466+
src = FakeClient(
467+
lookups=[_src_lookup("src-lk", "Vendors")],
468+
details={"src-lk": _src_detail("Current draft", llm="src-llm")},
469+
versions={
470+
"src-lk": [
471+
{
472+
"version_id": "src-v1",
473+
"is_draft": False,
474+
"version_name": "v1",
475+
"version_number": 1,
476+
},
477+
]
478+
},
479+
version_details={
480+
"src-v1": {
481+
"version_id": "src-v1",
482+
"is_draft": False,
483+
"version_name": "v1",
484+
"version_number": 1,
485+
"prompt_template": "Frozen v1",
486+
"adapters": {"llm": "src-llm", "x2text": None},
487+
"files": [],
488+
},
489+
},
490+
)
491+
tgt = FakeClient(
492+
lookups=[{"lookup_id": "tgt-lk", "name": "Vendors"}],
493+
details={
494+
"tgt-lk": {
495+
"prompt_template": "",
496+
"draft_version_id": "tgt-draft-lk",
497+
"adapters": {"llm": None, "x2text": None},
498+
}
499+
},
500+
versions={
501+
"tgt-lk": [
502+
{
503+
"version_id": "tgt-v1",
504+
"is_draft": False,
505+
"version_name": "v1",
506+
"version_number": 1,
507+
},
508+
]
509+
},
510+
)
511+
remap = RemapTable()
512+
remap.record("adapter", "src-llm", "tgt-llm")
513+
ctx = _ctx(src, tgt, remap=remap)
514+
report = CloneReport()
515+
516+
LookupsPhase(ctx).run(report)
517+
518+
# Nothing re-published; remap points at the existing target version.
519+
assert tgt.published_versions == []
520+
assert remap.resolve("lookup_version", "src-v1") == "tgt-v1"
521+
522+
463523
def test_published_pinned_assignment_resolves_via_version_remap():
464524
src = _src_published_lookup()
465525
tgt = FakeClient()

tests/clone/test_manual_review_phase.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,22 @@ def test_review_api_key_recreated_with_warning():
249249
assert any("re-wire any external consumers" in w for w in result.warnings)
250250

251251

252+
def test_review_api_key_adopted_on_rerun():
253+
# Target already carries a key with the same (class_name, description):
254+
# re-run must adopt, not create a duplicate or re-warn.
255+
key = {"class_name": "invoices", "description": "d", "is_active": True}
256+
src = FakeClient(workflows=[], api_keys=[{"id": "k1", "api_key": "s", **key}])
257+
tgt = FakeClient(api_keys=[{"id": "t1", "api_key": "other", **key}])
258+
ctx = _ctx(src, tgt, remap=RemapTable())
259+
report = CloneReport()
260+
261+
result = ManualReviewPhase(ctx).run(report)
262+
263+
assert tgt.created_api_keys == []
264+
assert result.adopted >= 1
265+
assert not any("re-wire any external consumers" in w for w in result.warnings)
266+
267+
252268
def test_dry_run_plans_without_writing():
253269
src = FakeClient(
254270
workflows=[{"id": "src-wf", "workflow_name": "WF"}],

0 commit comments

Comments
 (0)