Verwaiste Repo-Platzhalter blockieren keine Zuordnung mehr

Ein in Gitea geloeschtes Org-Repo liess sein unverknuepftes Projekt-Dokument
liegen. Der Unique-Index auf repoFullName ignoriert Gross-/Kleinschreibung,
darum scheiterte beim Zuweisen eines gleich benannten neuen Repos das
Umbenennen des Projekts; alle Pushes landeten im Platzhalter ohne Ticket,
also ohne Arbeitsblatt und ohne Push-Zeitmail (WEBklar/Schmacko-napf).

- repoMove: vor dem Umbenennen ein unverknuepftes fremdes Dokument mit dem
  Zielnamen entfernen (verknuepfte bleiben unangetastet)
- Webhook repository/deleted: unverknuepfte Platzhalter des geloeschten
  Repos entfernen, nur wenn Gitea das Repo wirklich nicht mehr kennt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 21:58:11 +00:00
1 parent a64a996291
commit 6718018211
5 files changed
+132 -7

No files matched your search

+39 -2
View File
@@ -1,9 +1,10 @@
import { config } from '../config.js'
import { listDocuments, updateDocument, getDocument, upsertWebsiteProjectByRepo, Query } from './appwriteAdmin.js'
import { listDocuments, updateDocument, getDocument, deleteDocument, upsertWebsiteProjectByRepo, Query } from './appwriteAdmin.js'
import { importNewRepos } from './giteaAdmin.js'
import { giteaFetch } from './giteaClient.js'
import { withLocks } from './keyedLock.js'
import { repoLockKey } from './repoMove.js'
import { repoLockKey, isUnlinkedProject } from './repoMove.js'
import { invalidateProjectCache } from './projectInsights.js'
import { shouldSendAssignMail, dueReminder, sendRepoAssignMail } from './repoAssignMail.js'
/**
@@ -84,6 +85,42 @@ export async function handleRepositoryCreated(payload, { now = new Date(), sendM
return { created, mail }
}
/**
* Webhook `repository` / `deleted`: unverknuepfte Platzhalter-Dokumente des geloeschten Repos
* entfernen. Sonst belegen sie den Namen (Unique-Index ohne Gross-/Kleinschreibung), und ein spaeter
* gleich benanntes Repo kann nach dem Zuweisen nicht auf ihn umbenannt werden. Verknuepfte Dokumente
* (Ticket, Kunde, Art, Preview) bleiben, ebenso alles, solange Gitea das Repo noch liefert.
*/
export async function handleRepositoryDeleted(payload) {
const fullName = payload?.repository?.full_name
if (!fullName) return { removed: 0 }
return withLocks(
[repoLockKey(fullName)],
async () => {
try {
await giteaFetch(`/repos/${fullName}`)
return { removed: 0 } // Repo (oder eine Weiterleitung darauf) existiert noch
} catch (err) {
if (err.status !== 404) throw err
}
const docs = await listDocuments(collection(), [Query.equal('repoFullName', fullName), Query.limit(10)])
let removed = 0
for (const doc of docs) {
if (!isUnlinkedProject(doc)) {
console.warn(`[repoAssign] Repo ${fullName} gelöscht, Projekt ${doc.$id} ist verknüpft und bleibt`)
continue
}
await deleteDocument(collection(), doc.$id)
removed += 1
}
invalidateProjectCache(fullName)
return { removed }
},
{ waitMs: 10000 }
)
}
/** Ein Lauf: neue Repos importieren + Mail, Wiederholungen, Erinnerungen. Wirft bei Appwrite-Fehlern nicht. */
export async function runRepoAssignJob({
now = new Date(),
+29 -1
View File
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
import { config } from '../config.js'
import { giteaFetch, splitRepo } from './giteaClient.js'
import { withLocks } from './keyedLock.js'
import { listDocuments, updateDocument, Query } from './appwriteAdmin.js'
import { listDocuments, updateDocument, deleteDocument, Query } from './appwriteAdmin.js'
import { invalidateProjectCache } from './projectInsights.js'
import { previewWorkDir } from './previewDeploy.js'
@@ -101,8 +101,36 @@ function classifyGiteaError(err) {
return { moved: null, warnings: [`Umzug nach WEBklar nicht möglich (${err.message}).`] }
}
/**
* Projekt-Dokument ohne jede Verknuepfung (kein Ticket, kein Kunde, keine Art, keine Preview):
* ein reiner Platzhalter, wie ihn der Webhook `repository/created` oder der Import anlegt.
*/
export function isUnlinkedProject(project) {
return !project?.ticketId && !project?.customerId && !project?.kind && !project?.subdomain
}
/**
* Gibt den Namen `to` fuer `projectId` frei, falls ihn ein anderes, unverknuepftes Dokument belegt -
* z. B. das Ueberbleibsel eines in Gitea geloeschten Repos gleichen Namens. Der Unique-Index auf
* `repoFullName` ignoriert Gross-/Kleinschreibung; ohne Freigabe scheitert das Umbenennen, und alle
* Pushes landen danach im Platzhalter statt im zugewiesenen Projekt (keine Arbeitsblaetter, keine
* Zeitmail). Ein verknuepftes fremdes Dokument bleibt unangetastet - dann scheitert das Update wie bisher.
*/
async function releaseRepoName(projectId, to) {
const others = await listDocuments(config.collections.websiteProjects, [
Query.equal('repoFullName', to),
Query.limit(10),
])
for (const other of others) {
if (other.$id === projectId || !isUnlinkedProject(other)) continue
await deleteDocument(config.collections.websiteProjects, other.$id)
console.warn(`[repoMove] Verwaistes Projekt-Dokument ${other.$id} (${other.repoFullName}) entfernt, Name frei für ${projectId}`)
}
}
/** Schritt 6: Projektdokument auf den neuen Repo-Namen nachziehen. */
async function updateProjectDocument(project, to) {
await releaseRepoName(project.$id, to)
const name = splitRepo(to)[1]
return updateDocument(config.collections.websiteProjects, project.$id, {
repoFullName: to,