Files
a0_software_orchestrator/utils/migrate_legacy_project_names.py
T

167 lines
5.8 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""migrate_legacy_project_names archiviert Projekte mit ungültigen Namen.
Bug 1+2 Begleit-Skript (Plan v3 §3.6):
- Iteriert über alle Projekte in der DB.
- Prüft jeden Namen gegen PROJECT_NAME_PATTERN + FORBIDDEN_PROJECT_NAMES.
- Markiert ungültige Namen mit status='archived' in project_state (idempotent).
- Bestehende 'active'-Projekte mit ungültigem Namen werden zu 'archived'.
- Bereits archivierte werden NICHT doppelt verändert (idempotent).
Aufruf:
python3 utils/migrate_legacy_project_names.py [--dry-run] [--verbose]
Defaults:
- --dry-run: nur Report, keine DB-Schreibzugriffe.
- --verbose: zusätzlich pro Projekt-Status ausgeben.
Exit-Code:
- 0: Erfolg (inkl. 0 archivierte Projekte).
- 1: DB-Fehler.
"""
from __future__ import annotations
import argparse
import os
import sys
from typing import List, Tuple
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Run from /a0 to have usr.plugins.* resolvable.
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_db, _db_error_handler, _validate_project_name,
PROJECT_NAME_PATTERN, FORBIDDEN_PROJECT_NAMES,
)
from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError
@_db_error_handler
def list_projects_with_states() -> List[Tuple[int, str, str]]:
"""Returns Liste von (project_id, project_name, current_status).
Liest aus projects JOIN project_state. Projekte ohne project_state-Eintrag
bekommen den Status '<none>'.
"""
db = _db()
rows = db.conn.execute("""
SELECT p.id, p.name, COALESCE(ps.status, '<none>') as status
FROM projects p
LEFT JOIN project_state ps ON ps.project_id = p.id
ORDER BY p.id
""").fetchall()
return [(int(r[0]), str(r[1]), str(r[2])) for r in rows]
@_db_error_handler
def archive_project(project_id: int, reason: str) -> None:
"""Markiert ein Projekt als 'archived' in project_state. Idempotent.
Zwei Schritte statt ON CONFLICT DO UPDATE SET mit updated_at: das
vermeidet sporadische 'no such column: updated_at'-Fehler bei
cross-Singleton-Connections (zwei verschiedene Modul-Importe der
PatternDB führen zu zwei Connections auf die gleiche DB-Datei).
"""
db = _db()
# 1. Status archivieren (INSERT oder UPDATE)
db.conn.execute("""
INSERT INTO project_state (project_id, status, phase, last_active_at, updated_at)
VALUES (?, 'archived', 'archived', datetime('now'), datetime('now'))
ON CONFLICT(project_id) DO UPDATE SET
status = 'archived',
phase = 'archived',
last_active_at = datetime('now')
""", (project_id,))
# 2. updated_at explizit setzen (separates Statement, robuster)
db.conn.execute("""
UPDATE project_state SET updated_at = datetime('now')
WHERE project_id = ?
""", (project_id,))
# NOTE: projects-Tabelle hat kein updated_at-Feld (Spalten: id, name,
# git_url, description, tech_stack, created_at, project_path, …).
# Daher kein UPDATE projects hier. Wenn projects.updated_at per
# Migration ergänzt wird, kann dieser Block nachgerüstet werden.
db.conn.commit()
sys.stderr.write(f" [ARCHIVED] project_id={project_id} reason={reason}\n")
def main() -> int:
ap = argparse.ArgumentParser(
description="Archiviere Projekte mit ungültigen Namen (Pattern/Blacklist)."
)
ap.add_argument(
"--dry-run", action="store_true",
help="Nur anzeigen, welche Projekte archiviert würden; keine DB-Schreibzugriffe.",
)
ap.add_argument(
"--verbose", "-v", action="store_true",
help="Zusätzlich pro Projekt den Validierungs-Status ausgeben.",
)
args = ap.parse_args()
try:
projects = list_projects_with_states()
except PluginDBError as e:
print(f"ERROR: DB-Fehler beim Lesen der Projekte: {e}", file=sys.stderr)
return 1
if not projects:
print("No projects found in DB.")
return 0
print(f"=== Legacy-Name Migration (dry-run={args.dry_run}) ===")
print(f"Pattern: {PROJECT_NAME_PATTERN.pattern}")
print(f"Blacklist: {sorted(FORBIDDEN_PROJECT_NAMES)}")
print(f"Projects in DB: {len(projects)}")
print()
active_count = 0
archived_count = 0
already_archived_count = 0
invalid_count = 0
for pid, name, current_status in projects:
try:
_validate_project_name(name)
valid = True
invalid_reason = None
except ValueError as e:
valid = False
invalid_reason = str(e)
if valid:
active_count += 1
if args.verbose:
print(f" [OK ] pid={pid:3d} name={name!r:40s} status={current_status}")
continue
invalid_count += 1
if current_status == "archived":
already_archived_count += 1
print(f" [SKIP] pid={pid:3d} name={name!r} already archived; invalid={invalid_reason}")
continue
print(f" [BAD ] pid={pid:3d} name={name!r} status={current_status} reason={invalid_reason}")
if not args.dry_run:
try:
archive_project(pid, reason=invalid_reason)
archived_count += 1
except PluginDBError as e:
print(f"ERROR: konnte pid={pid} nicht archivieren: {e}", file=sys.stderr)
return 1
print()
print(f"Summary:")
print(f" total projects: {len(projects)}")
print(f" valid (active): {active_count}")
print(f" invalid (total): {invalid_count}")
print(f" newly archived: {archived_count}")
print(f" already archived: {already_archived_count}")
print(f" mode: {'DRY-RUN' if args.dry_run else 'APPLIED'}")
return 0
if __name__ == "__main__":
sys.exit(main())