167 lines
5.8 KiB
Python
167 lines
5.8 KiB
Python
|
|
#!/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())
|