Files
Webklar-Kundenbereich/server/services/projectAssignment.js
T

184 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { config } from '../config.js'
import { moveRepoToOrg, WARN } from './repoMove.js'
import { splitRepo } from './giteaClient.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'
/**
* Warnungen dieses Moduls, woertlich (Controller-Ruling R14, Fix-Runde 1) - Anfuehrungszeichen
* wie die bestehenden Warnungen in repoMove.js (‚...‘ mit U+201A/U+2018).
*/
const WARN_BACKFILL_FAILED = (reason) =>
`Pushes konnten nicht nachgetragen werden (${reason}). Bitte später ‚Pushes nachtragen‘ klicken.`
const WARN_RECONCILE_FAILED = (reason) =>
`Zuweisung gespeichert, aber Umzug und Nachtragen sind fehlgeschlagen (${reason}). Bitte später ‚Pushes nachtragen‘ klicken.`
/**
* 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)], ticketId: project.ticketId || '' }
}
const relevant = pushes.filter((push) => !isSystemPusher(push.pusher) && !isOldSystemCommitPush(push))
if (relevant.length === 0) return { backfilled: 0, warnings: [], ticketId: project.ticketId || '' }
// `ensureProjectTicket` und die Arbeitsblatt-Schleife sind hier bewusst in EINEM try/catch
// (Controller-Ruling R14): schlaegt eine der beiden Appwrite-Schreiboperationen fehl (z. B.
// Ausfall waehrend POST workorders/worksheets), bleibt die Zuweisung trotzdem bestehen - siehe
// `assignProject`, das sich darauf verlaesst, dass `backfillPushes` nie wirft.
let ticketId = project.ticketId || ''
let backfilled = 0
try {
ticketId = ticketId || (await ensureProjectTicket(project, { repoFullName: project.repoFullName }))
if (!ticketId) return { backfilled: 0, warnings: [], ticketId: '' }
// 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: [], ticketId }
} catch (err) {
return { backfilled, warnings: [WARN_BACKFILL_FAILED(err.message)], ticketId }
}
}
/**
* Kern von `reconcileProject`, OHNE selbst die Sperre zu nehmen - fuer `assignProject`, das
* seine eigene Lese-Pruef-Schreib-Folge und diesen Kern unter DERSELBEN Sperre laufen laesst
* (Controller-Ruling R15), statt die Sperre zweimal zu nehmen. `reconcileProject` (unten) bleibt
* der oeffentliche, selbst sperrende Einstieg (z. B. fuer die Migration, Task 9).
*
* Liest das Projekt, verschiebt sein Repo bei Bedarf nach `GITEA_ORG` und traegt danach dessen
* Pushes nach. Wirft nur noch, wenn schon das ERSTE Lesen des Projekts fehlschlaegt (dann gibt es
* nichts Sinnvolles zu verschieben/nachzutragen) - `assignProject` faengt das ab (R14).
*/
async function reconcileProjectLocked(projectId, { now } = {}) {
let project = await getDocument(config.collections.websiteProjects, projectId)
const { moved, warnings: moveWarnings } = await moveRepoToOrg(project)
try {
// 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)
} catch {
// R14: Schlaegt das Nachlesen fehl, mit dem VOR dem Umzug gelesenen Dokument weitermachen -
// ist tatsaechlich etwas verschoben worden, von Hand dessen `repoFullName`/`giteaRepoUrl`/
// `giteaRepoName` nachziehen (siehe `updateProjectDocument` in repoMove.js). Keine zusaetzliche
// Warnung - `moveWarnings` deckt einen etwaigen Gitea-Ausfall bereits ab.
if (moved) {
project = {
...project,
repoFullName: moved.to,
giteaRepoUrl: `${config.gitea.baseUrl}/${moved.to}`,
giteaRepoName: splitRepo(moved.to)[1],
}
}
}
const { backfilled, warnings: backfillWarnings, ticketId } = await backfillPushes(project, { now })
// R15: die Antwort soll die (evtl. gerade erst automatisch angelegte) ticketId zeigen, auch
// wenn `project` oben vor dem Anlegen gelesen wurde.
if (ticketId && project.ticketId !== ticketId) project = { ...project, ticketId }
return { project, moved, backfilled, warnings: [...moveWarnings, ...backfillWarnings] }
}
/**
* Oeffentlicher, selbst sperrender Einstieg fuer den Kern oben - 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).
*/
export async function reconcileProject(projectId, { now } = {}) {
return withLocks(['project:' + projectId], () => reconcileProjectLocked(projectId, { now }))
}
/**
* Ticket-Id fuer die Zuweisung (Controller-Ruling R15): eine mitgegebene `ticketId` gewinnt immer.
* Ist sie leer UND bleibt die Kunden-Id gleich, bleibt die gespeicherte `ticketId` erhalten - ein
* Entfernen laeuft ueber "Zuweisung aufheben", nicht ueber diesen Endpunkt (sonst loescht ein
* zweiter Klick ohne `ticketId` ein zwischenzeitlich automatisch angelegtes Ticket, Review I3).
* Aendert sich die Kunden-Id und wird keine `ticketId` mitgegeben, wird sie geleert.
*/
function resolveTicketId(payloadTicketId, customerId, existing) {
if (payloadTicketId) return payloadTicketId
if (customerId === existing.customerId) return existing.ticketId || ''
return ''
}
/**
* Weist ein Projekt einem Kunden/Ticket zu und stoesst danach Umzug + Nachtragen an. Laeuft
* komplett unter der Projekt-Sperre (Controller-Ruling R15): Lesen (fuer die 404-Pruefung und die
* `ticketId`-Aufloesung), Schreiben der Zuweisung und der Reconcile-Kern teilen sich dieselbe
* Sperre, statt sie zweimal zu nehmen - ein zweiter, gleichzeitiger Aufruf sieht daher nie einen
* veralteten Stand. Ein unbekanntes Projekt wirft weiterhin (`.status = 404`) BEVOR irgendetwas
* geschrieben wird. Schlaegt danach der Reconcile-Kern (Umzug/Nachtragen) mit etwas anderem als
* einem 404 fehl, bleibt die Zuweisung trotzdem gespeichert - Ruling R14 verlangt eine Warnung
* statt eines Abbruchs, denn Spec 5.2 verlangt genau das.
*/
export async function assignProject(projectId, { customerId = '', ticketId = '' }, { now } = {}) {
return withLocks(['project:' + projectId], async () => {
// 404 fuer ein unbekanntes Projekt, bevor ueberhaupt geschrieben wird.
const existing = await getDocument(config.collections.websiteProjects, projectId)
const nextTicketId = resolveTicketId(ticketId, customerId, existing)
const saved = await updateDocument(config.collections.websiteProjects, projectId, {
customerId,
ticketId: nextTicketId,
updatedAt: new Date().toISOString(),
})
try {
const result = await reconcileProjectLocked(projectId, { now })
return { ok: true, ...result }
} catch (err) {
if (err.status === 404) throw err
return {
ok: true,
project: saved,
moved: null,
backfilled: 0,
warnings: [WARN_RECONCILE_FAILED(err.message)],
}
}
})
}