feat(sync): geschlossene Issues haken ihre Aufgabe auch ohne Webhook ab
CI / pruefen (push) Canceled after 0s
CI / pruefen (push) Canceled after 0s
Behebt florian.wilde/nebula#1. Der Rueckweg "Issue geschlossen -> Aufgabe erledigt" gab es seit v1.7.0 schon, aber nur ueber den Webhook. Ist keiner eingerichtet - und am Repository war keiner eingetragen -, blieb er ganz aus. Damit hing die Funktion an einer Voraussetzung, die man ihr von aussen nicht ansieht. Der Abgleich fragt jetzt den Zustand verknuepfter Issues nach und hakt ab, was in Git geschlossen ist. Beides ergaenzt sich: der Webhook wirkt sofort, der Abgleich verlaesslich. Gespart wird, wo es geht: Nummern, die der gerade geholte Snapshot als offen fuehrt, werden nicht einzeln nachgefragt. Umgekehrt gilt das nicht - die Liste ist gedeckelt, aus dem Fehlen einer Nummer folgt nichts. Deshalb ein Deckel von 25 Nachfragen je Durchgang. **Nur abhaken, nie zuruecknehmen.** Der Abgleich sieht Zustaende, keine Ereignisse. Ein offenes Issue kann auch bedeuten, dass das Schliessen von Nebula aus fehlgeschlagen ist; ein symmetrischer Abgleich wuerde dann eine bewusst erledigte Aufgabe wieder aufmachen. Das Zuruecknehmen bleibt dem Webhook vorbehalten, der ein echtes "reopened" sieht. Ein geloeschtes Issue ist ebenfalls kein erledigtes. Nebenbei eine Unstimmigkeit aus v1.7.0 geradegezogen: der Webhook setzte nur den Status und liess aus, was die Oberflaeche tut - naechste Fassung einer wiederkehrenden Aufgabe, Eintrag im Verlauf. Beide Wege gehen jetzt durch dieselbe Funktion. Geprueft: 232 Unit-Tests, 31 E2E-Tests. Der neue E2E-Test laeuft als einziger wirklich nach draussen - er haengt eine Aufgabe an ein seit Jahren geschlossenes Issue und prueft, dass der Abgleich sie abhakt. Ist GitHub nicht erreichbar, ueberspringt er sich statt rot zu werden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,15 @@ export class GiteaClient implements GitClient {
|
||||
);
|
||||
}
|
||||
|
||||
async getIssueState(ref: RepoRef, number: number): Promise<"open" | "closed" | null> {
|
||||
const issue = await getJson<{ state?: string } | null>(
|
||||
`${this.repoBase(ref)}/issues/${number}`,
|
||||
{ headers: this.headers(), allow404: true },
|
||||
);
|
||||
if (!issue?.state) return null;
|
||||
return issue.state === "closed" ? "closed" : "open";
|
||||
}
|
||||
|
||||
async check(): Promise<ConnectionCheck> {
|
||||
try {
|
||||
const version = await getJsonSafe<{ version?: string }>(
|
||||
|
||||
@@ -70,6 +70,15 @@ export class GitHubClient implements GitClient {
|
||||
);
|
||||
}
|
||||
|
||||
async getIssueState(ref: RepoRef, number: number): Promise<"open" | "closed" | null> {
|
||||
const issue = await getJson<{ state?: string } | null>(
|
||||
`${this.repoBase(ref)}/issues/${number}`,
|
||||
{ headers: this.headers(), allow404: true },
|
||||
);
|
||||
if (!issue?.state) return null;
|
||||
return issue.state === "closed" ? "closed" : "open";
|
||||
}
|
||||
|
||||
async check(): Promise<ConnectionCheck> {
|
||||
try {
|
||||
if (!this.token) {
|
||||
|
||||
@@ -70,6 +70,16 @@ export class GitLabClient implements GitClient {
|
||||
);
|
||||
}
|
||||
|
||||
async getIssueState(ref: RepoRef, number: number): Promise<"open" | "closed" | null> {
|
||||
const issue = await getJson<{ state?: string } | null>(
|
||||
`${this.apiBase}/projects/${this.projectPath(ref)}/issues/${number}`,
|
||||
{ headers: this.headers(), allow404: true },
|
||||
);
|
||||
if (!issue?.state) return null;
|
||||
// GitLab nennt den offenen Zustand "opened".
|
||||
return issue.state === "closed" ? "closed" : "open";
|
||||
}
|
||||
|
||||
async check(): Promise<ConnectionCheck> {
|
||||
try {
|
||||
if (!this.token) {
|
||||
|
||||
@@ -190,6 +190,17 @@ export interface GitClient {
|
||||
* geoeffnet – daher der Schalter statt zweier Methoden.
|
||||
*/
|
||||
setIssueState(ref: RepoRef, number: number, open: boolean): Promise<void>;
|
||||
/**
|
||||
* Liest den Zustand eines einzelnen Issues.
|
||||
*
|
||||
* Gebraucht fuer den Abgleich: der Snapshot fuehrt nur die *offenen*
|
||||
* Issues und ist ausserdem gedeckelt – aus dem Fehlen einer Nummer laesst
|
||||
* sich deshalb nicht schliessen, dass sie geschlossen wurde. Sie koennte
|
||||
* ebenso geloescht oder einfach jenseits des Deckels sein.
|
||||
*
|
||||
* null heisst: gibt es nicht (mehr).
|
||||
*/
|
||||
getIssueState(ref: RepoRef, number: number): Promise<"open" | "closed" | null>;
|
||||
/** Repos, auf die der Token Zugriff hat (fuer die Auswahl beim Verknuepfen). */
|
||||
listRepos(query?: string): Promise<RepoListItem[]>;
|
||||
/** Vollstaendiger Stand eines Repositories. */
|
||||
|
||||
Reference in New Issue
Block a user