feat(tasks): Aufgabe als Git-Issue anlegen, Zustand wandert mit

Die Zuordnung stand im Schema laengst – sourceIssue, sourceUrl und
sourceRepositoryId samt Eindeutigkeit je Repository, gefuellt von der
Uebernahme aus Issues seit v0.9.0. Es fehlte die Gegenrichtung.

Ein Knopf im Aufgabendialog legt das Issue an; Nummer und Adresse landen in
denselben Feldern, der Dublettenschutz gilt also fuer beide Wege. In den
Rumpf kommt ein Rueckverweis auf das Projekt.

Ausdruecklich per Knopf und nie nebenbei: was in ein fremdes System
geschrieben wird, soll jemand gewollt haben. Ist die Aufgabe erst
verknuepft, wandert der Zustand allerdings mit – erledigt schliesst das
Issue, ein Zurueckziehen oeffnet es wieder. Fehlertolerant: ein nicht
erreichbares Git-System darf das Abhaken nicht verhindern.

Die Zustaende passen nicht aufeinander, und das bleibt so. Nebula kennt
vier, ein Issue zwei; nach aussen geht nur die Grenze um ERLEDIGT. Ein
Rundlauf verliert damit IN ARBEIT und BLOCKIERT. Labels waeren die
Alternative, wuerden aber den Issue-Tracker mit Zustaenden fuellen, die
dort niemand pflegt.

Kein Widerspruch zu "keine eigenen Git-Operationen": Issues laufen ueber
die HTTP-API der Provider. Dafuer gibt es jetzt sendJson neben getJson –
durch dieselbe SSRF-Schleuse, aber ohne Umleitungen zu folgen. Ein 307 auf
eine interne Adresse naehme den Rumpf und den Token im Header mit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 00:35:11 +02:00
co-authored by Claude Opus 5
parent 6a7365946b
commit e1d435f10a
10 changed files with 553 additions and 3 deletions
+70
View File
@@ -0,0 +1,70 @@
import type { TaskStatus } from "@prisma/client";
import { prisma } from "@/lib/db";
import { clientForConnection } from "@/lib/git";
/**
* Die Bruecke zwischen Nebula-Aufgabe und Git-Issue.
*
* Die Zustaende passen nicht aufeinander und das bleibt so: Nebula kennt
* OFFEN, IN ARBEIT, BLOCKIERT und ERLEDIGT, ein Issue nur offen oder
* geschlossen. Nach aussen geht deshalb nur die Grenze zwischen "erledigt"
* und "nicht erledigt"; ein Rundlauf verliert IN ARBEIT und BLOCKIERT.
*
* Die Alternative waeren Labels wie "nebula:doing" – die wuerden den
* Issue-Tracker aber mit Zustaenden fuellen, die dort niemand pflegt. Die
* feine Abstufung bleibt in Nebula.
*/
/** Ist der Aufgabenzustand einer, bei dem das Issue offen sein soll? */
export function issueShouldBeOpen(status: TaskStatus): boolean {
return status !== "DONE";
}
/** Der Aufgabenzustand, der zu einem Issue-Zustand passt. */
export function statusForIssueState(open: boolean, current: TaskStatus): TaskStatus {
if (open) {
// Ein wieder geoeffnetes Issue macht aus ERLEDIGT wieder OFFEN. Stand
// die Aufgabe ohnehin nicht auf ERLEDIGT, bleibt ihr feinerer Zustand
// stehen – IN ARBEIT ist mehr Information als OFFEN.
return current === "DONE" ? "TODO" : current;
}
return "DONE";
}
/**
* Traegt den Zustand einer Aufgabe an ihrem Issue nach.
*
* Bewusst fehlertolerant: schlaegt der Aufruf fehl, ist die Aufgabe in
* Nebula trotzdem umgezogen. Ein nicht erreichbares Git-System darf das
* Abhaken nicht verhindern – der naechste Wechsel versucht es erneut.
*
* Gibt zurueck, ob es geklappt hat; null heisst "nichts zu tun".
*/
export async function mirrorStatusToIssue(
taskId: string,
status: TaskStatus,
): Promise<boolean | null> {
const task = await prisma.task.findUnique({
where: { id: taskId },
select: { sourceIssue: true, sourceRepositoryId: true },
});
if (!task?.sourceIssue || !task.sourceRepositoryId) return null;
const repository = await prisma.repository.findUnique({
where: { id: task.sourceRepositoryId },
include: { connection: true },
});
if (!repository?.connection || !repository.repoOwner || !repository.repoName) return null;
try {
const client = clientForConnection(repository.connection);
await client.setIssueState(
{ owner: repository.repoOwner, name: repository.repoName },
task.sourceIssue,
issueShouldBeOpen(status),
);
return true;
} catch {
return false;
}
}