import { config } from '../config.js' import { moveRepoToOrg, WARN } from './repoMove.js' import { listRecentPushes } from './pushHistory.js' import { createGitPushWorksheet, isSystemPusher } from './gitWorksheet.js' import { withLocks } from './keyedLock.js' import { ensureProjectTicket } from './ticketAdmin.js' import { getDocument, updateDocument } from './appwriteAdmin.js' /** * Commit-Nachrichten (erste Zeile), die frueher das System mit Kensos Schluessel geschrieben hat * (Preview-Config, automatisch erstellte Preview-Seite) - siehe Plan, Abschnitt "Global * Constraints". Beim Nachtragen zaehlt ein Push nicht, wenn die erste Zeile JEDER seiner * Commit-Nachrichten hier steht - unabhaengig vom Pusher, denn diese alten Commits tragen oft * "knso" als Pusher, nicht das heutige System-Konto (siehe `isSystemPusher`, das dafuer allein * NICHT greift). */ export const SYSTEM_COMMIT_MESSAGES = ['Update preview config from ticket system', 'Automatisch erstellte Preview-Seite'] function firstLine(message) { return String(message || '').split('\n')[0] } /** true, wenn jede Commit-Nachricht des Pushes (erste Zeile) ein alter System-Commit ist. */ function isOldSystemCommitPush(push) { const commits = push.commits || [] if (commits.length === 0) return false return commits.every((commit) => SYSTEM_COMMIT_MESSAGES.includes(firstLine(commit.message))) } /** * Traegt die Pushes der letzten `GITEA_BACKFILL_DAYS` Tage eines Repos als Git-Arbeitsblaetter * nach (fuer neu zugewiesene Projekte, deren bisherige Pushes noch nicht abgerechnet sind). * Ueberspringt Systempushes: das aktuelle System-Konto (`isSystemPusher`) und alte, mit Kensos * Schluessel gemachte System-Commits (`SYSTEM_COMMIT_MESSAGES`). Legt bei Bedarf automatisch ein * Ticket an - aber erst, nachdem feststeht, dass tatsaechlich etwas nachzutragen ist, damit ein * Projekt ohne (relevante) Pushes kein Ticket aufgedraengt bekommt. Wirft nie wegen eines * Gitea-Problems (siehe `reconcileProject`, das sich - wie `moveRepoToOrg` - darauf verlaesst, * dass eine Zuweisung in jedem Fall bestehen bleibt). */ export async function backfillPushes(project, { now = new Date() } = {}) { let pushes try { pushes = await listRecentPushes(project.repoFullName, { now }) } catch (err) { return { backfilled: 0, warnings: [WARN.giteaDown(err.message)] } } const relevant = pushes.filter((push) => !isSystemPusher(push.pusher) && !isOldSystemCommitPush(push)) if (relevant.length === 0) return { backfilled: 0, warnings: [] } const ticketId = project.ticketId || (await ensureProjectTicket(project, { repoFullName: project.repoFullName })) if (!ticketId) return { backfilled: 0, warnings: [] } let backfilled = 0 // Aelteste zuerst (siehe `listRecentPushes`), damit die Arbeitsblaetter in derselben // Reihenfolge entstehen, in der die Pushes tatsaechlich passiert sind. for (const push of relevant) { const result = await createGitPushWorksheet({ ticketId, repoFullName: project.repoFullName, ...push }) if (result?.created) backfilled += 1 } return { backfilled, warnings: [] } } /** * Liest das Projekt, verschiebt sein Repo bei Bedarf nach `GITEA_ORG` und traegt danach dessen * Pushes nach - alles unter der Projekt-Sperre, damit ein Doppelklick auf "Zuweisen" (oder ein * gleichzeitiger Aufruf ueber die Migration, Task 9) nicht zweimal umzieht oder doppelte * Arbeitsblaetter erzeugt (Review Focus 3, Plan). Das Projektdokument wird ERST innerhalb der * Sperre gelesen, damit ein zweiter, gleichzeitiger Aufruf nie mit einem inzwischen veralteten * Objekt arbeitet. */ export async function reconcileProject(projectId, { now } = {}) { return withLocks(['project:' + projectId], async () => { let project = await getDocument(config.collections.websiteProjects, projectId) const { moved, warnings: moveWarnings } = await moveRepoToOrg(project) // Immer neu lesen, nicht nur wenn `moved` gesetzt ist: der Selbstheilungs-Zweig in // `moveRepoToOrg` (von Hand verschobenes/umbenanntes Repo) kann das Dokument umbenennen und // dabei trotzdem `moved: null` liefern (Controller-Ruling R13). project = await getDocument(config.collections.websiteProjects, projectId) const { backfilled, warnings: backfillWarnings } = await backfillPushes(project, { now }) return { project, moved, backfilled, warnings: [...moveWarnings, ...backfillWarnings] } }) } /** * Weist ein Projekt einem Kunden/Ticket zu und stoesst danach Umzug + Nachtragen an. Die * Zuweisung selbst wird ZUERST und ausserhalb der Projekt-Sperre gespeichert (reines Appwrite, * ohne Gitea) - sie bleibt deshalb bestehen, selbst wenn Gitea gerade nicht erreichbar ist * (siehe `reconcileProject`/`moveRepoToOrg`, die deshalb nie wegen Gitea werfen duerfen). */ export async function assignProject(projectId, { customerId = '', ticketId = '' }, { now } = {}) { // 404 fuer ein unbekanntes Projekt, bevor ueberhaupt geschrieben wird. await getDocument(config.collections.websiteProjects, projectId) await updateDocument(config.collections.websiteProjects, projectId, { customerId, ticketId, updatedAt: new Date().toISOString(), }) const { project, moved, backfilled, warnings } = await reconcileProject(projectId, { now }) return { ok: true, project, moved, backfilled, warnings } }