fix(backfill): source task identities from the forge, not a workstation
The first implementation took local filesystem paths. Central has no workstation checkout and must not depend on one: ADR-012 decision 1 makes the forge the projection source, and a backfill reading someone's laptop would reintroduce the exact coupling that ADR removes. Surfaced concretely — central's postgres is not reachable from the workstation (only the API tunnel, which is HTTP), so the local-path variant cannot reach the database it needs to update, while the pod can clone the forge and already holds the connection. A repository that cannot be cloned contributes nothing rather than reducing what the rest can identify. Refs STATE-WP-0083-T06 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
parent
de853fc9e6
commit
cdd5cef373
1 changed files with 75 additions and 0 deletions
|
|
@ -114,3 +114,78 @@ async def backfill_task_record_ids(
|
|||
if not dry_run:
|
||||
row.record_id = record_id
|
||||
return report
|
||||
|
||||
|
||||
async def backfill_from_forge(
|
||||
session: Any,
|
||||
repo_slugs: list[str],
|
||||
*,
|
||||
forge_base: str | None = None,
|
||||
dry_run: bool = True,
|
||||
) -> BackfillReport:
|
||||
"""Backfill from repositories cloned out of the forge.
|
||||
|
||||
The local-path variant above needs a workstation checkout, which central
|
||||
does not have and should not depend on: `ADR-012` decision 1 makes the forge
|
||||
the projection source, and a backfill sourced from someone's laptop would
|
||||
reintroduce exactly the coupling that ADR removes.
|
||||
|
||||
Central can clone the forge directly, so it reads the pairing from the same
|
||||
place it derives everything else.
|
||||
"""
|
||||
import tempfile
|
||||
|
||||
from api.services.forge_projection import (
|
||||
DEFAULT_FORGE_BASE,
|
||||
ForgeDeriveError,
|
||||
_run_git,
|
||||
)
|
||||
|
||||
base = forge_base or DEFAULT_FORGE_BASE
|
||||
report = BackfillReport()
|
||||
pairs: dict[str, str] = {}
|
||||
|
||||
for slug in repo_slugs:
|
||||
url = f"{base.rstrip('/')}/{slug}.git"
|
||||
with tempfile.TemporaryDirectory(prefix=f"backfill-{slug}-") as tmp:
|
||||
try:
|
||||
_run_git("clone", "--depth", "1", "--quiet", url, tmp)
|
||||
except (ForgeDeriveError, Exception):
|
||||
# A repository that cannot be read contributes nothing. It must
|
||||
# not silently reduce what the rest can identify.
|
||||
continue
|
||||
repo_pairs, repo_report = collect_pairs([Path(tmp)])
|
||||
report.scanned_files += repo_report.scanned_files
|
||||
report.conflicts.extend(repo_report.conflicts)
|
||||
for uid, rid in repo_pairs.items():
|
||||
prior = pairs.get(uid)
|
||||
if prior and prior != rid:
|
||||
report.conflicts.append({"uuid": uid, "first": prior, "second": rid})
|
||||
continue
|
||||
pairs[uid] = rid
|
||||
|
||||
report.pairs_found = len(pairs)
|
||||
if not pairs:
|
||||
return report
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from api.models.task import Task
|
||||
|
||||
rows = list((await session.execute(select(Task))).scalars())
|
||||
by_id = {str(r.id): r for r in rows}
|
||||
for uid, rid in pairs.items():
|
||||
row = by_id.get(uid)
|
||||
if row is None:
|
||||
report.unmatched_uuids += 1
|
||||
continue
|
||||
if row.record_id == rid:
|
||||
report.already_set += 1
|
||||
continue
|
||||
if row.record_id and row.record_id != rid:
|
||||
report.conflicts.append({"uuid": uid, "first": row.record_id, "second": rid})
|
||||
continue
|
||||
report.updated += 1
|
||||
if not dry_run:
|
||||
row.record_id = rid
|
||||
return report
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue