diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b321f..2a400aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.4.2] – 2026-09-17 + +### Geändert +- Der Issue-Abgleich holt Serveradresse und Repository aus dem Git-Remote und den Token aus den hinterlegten Git-Zugangsdaten; ein zweiter Token in der `.env` ist nicht mehr nötig (#020) + ## [0.4.1] – 2026-09-17 ### Hinzugefügt diff --git a/DEVLOG.md b/DEVLOG.md index 5b47f81..bbda1e1 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 2 h 48 min +**Gesamt:** 2 h 58 min | Issue | Zeit | |---|---| @@ -13,7 +13,7 @@ | #005 Agent-Skript | 0 h 25 min | | #006 Kunden und Hostzuweisung | 0 h 35 min | | #007 Host-Tabelle und Detailansichten | 0 h 25 min | -| #020 Issues nach Gitea übertragen | 0 h 10 min | +| #020 Issues nach Gitea übertragen | 0 h 20 min | | Meilenstein | Zeit | |---|---| @@ -22,12 +22,13 @@ | v0.3.0 | 0 h 35 min | **abgeschlossen** | v0.4.0 | 0 h 25 min | **abgeschlossen** | v0.4.1 | 0 h 10 min | **abgeschlossen** +| v0.4.2 | 0 h 10 min | **abgeschlossen** --- ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.4.1 +**Version:** 0.4.2 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -49,6 +50,8 @@ - #007 abgeschlossen: Host-Tabelle mit Suche, fünf Filtern, Sortierung, Seitenblättern und CSV-Export; Host-Detail mit sechs Tabs, Notizen, Tags, Kundenzuweisung und Token-Widerruf - Meilenstein v0.4.0 abgeschlossen und getaggt; 59 Tests - #020 abgeschlossen: Abgleichskript für den Gitea-Issue-Tracker (Missverständnis geklärt – die Issues lagen bisher nur als Dateien im Repo, nicht im Tracker) +- Token kommt aus den Git-Zugangsdaten statt aus einer zweiten Stelle; 20 Issues, 15 Labels und 13 Meilensteine in Gitea angelegt +- Alles nach Gitea gepusht: main bei 51243c2, Tags v0.1.0 bis v0.4.1 ### Entscheidungen - **Screenshots mit Firefox headless** statt Playwright: Firefox ist vorhanden, es muss kein Browser samt Systembibliotheken nachinstalliert werden (dafür wäre root nötig). @@ -75,5 +78,4 @@ - Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen) ### Nächster Schritt -- Gitea-Token erzeugen und `npm run issues:sync` einmal echt laufen lassen - #008 Laufzeiten und Rechnungen: Vorschläge bestätigen, Rechnungen erfassen, Häkchen direkt in der Tabelle diff --git a/README.md b/README.md index fd0a68b..eebba15 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,16 @@ jedem Klon mit. Wer sie zusätzlich im Gitea-Tracker haben möchte, gleicht sie ```bash npm run issues:sync -- --dry-run # zeigt nur, was passieren würde - -GITEA_URL=https://git.example.de \ -GITEA_REPO=benutzer/ProxLedger \ -GITEA_TOKEN=... \ -npm run issues:sync +npm run issues:sync # echter Abgleich ``` -Der Token braucht das Recht `issue:write` (Gitea: Einstellungen → Anwendungen → Zugriffstoken). Er kann auch in -`.env` stehen, die Datei ist von Git ausgenommen. Der Abgleich legt Labels und Meilensteine an, erkennt -vorhandene Issues an der Nummer im Titel, aktualisiert sie und schließt erledigte. +Serveradresse und Repository liest das Skript aus dem Git-Remote, den Token aus den hinterlegten +Git-Zugangsdaten (Schlüsselbund oder `~/.git-credentials`) – derselbe Token wie fürs Pushen, er braucht +zusätzlich das Recht `issue:write`. Wer beides trennen möchte, setzt `GITEA_URL`, `GITEA_REPO` und +`GITEA_TOKEN` in der `.env`. + +Der Abgleich legt Labels und Meilensteine an, erkennt vorhandene Issues an der Nummer im Titel, +aktualisiert sie und schließt erledigte. Ein zweiter Lauf erzeugt keine Dubletten. ## Dokumentation diff --git a/docs/issues/020-gitea-issue-abgleich.md b/docs/issues/020-gitea-issue-abgleich.md index 9285956..d416386 100644 --- a/docs/issues/020-gitea-issue-abgleich.md +++ b/docs/issues/020-gitea-issue-abgleich.md @@ -27,5 +27,9 @@ aktuell bleiben – ohne dass sie von Hand nachgepflegt werden müssen. ## Notizen / Entscheidungen - Die Markdown-Dateien bleiben die Quelle der Wahrheit: Sie liegen im Repo, sind ohne Netz lesbar und wandern mit jedem Klon mit. Gitea bekommt eine Kopie zum Kommentieren und Zuweisen. -- Der erste echte Lauf steht noch aus: Dafür braucht es einen Gitea-Token mit `issue:write`. - Der Trockenlauf liest alle 20 Dateien korrekt. +- Der Token kommt aus den Git-Zugangsdaten (`git credential fill`), Adresse und Repo aus dem Remote. + Damit muss derselbe Token nicht zweimal gepflegt werden. +- Gefragt werden zuerst die eingerichteten Helfer (Schlüsselbund), danach zusätzlich `~/.git-credentials` – + je nach Rechner ist mal das eine, mal das andere gefüllt. +- Erster echter Lauf am 17.09.2026: 20 Issues, 15 Labels und 13 Meilensteine angelegt, erledigte Issues + direkt geschlossen. Zweiter Lauf: 0 neue, 20 aktualisiert – keine Dubletten. diff --git a/package.json b/package.json index ca576e7..c8b1703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.4.1", + "version": "0.4.2", "private": true, "scripts": { "dev": "next dev", diff --git a/scripts/gitea-issues.ts b/scripts/gitea-issues.ts index 864dc68..9b0dcb7 100644 --- a/scripts/gitea-issues.ts +++ b/scripts/gitea-issues.ts @@ -10,6 +10,7 @@ * Die Markdown-Dateien bleiben die Quelle der Wahrheit; Gitea erhält eine Kopie zum Kommentieren. */ +import { execFileSync } from "node:child_process"; import { readFile, readdir } from "node:fs/promises"; import "dotenv/config"; import path from "node:path"; @@ -138,6 +139,49 @@ class Gitea { } } +/** + * Ermittelt Serveradresse und Repo aus dem Git-Remote, damit beides nicht doppelt gepflegt werden muss. + * Beispiel: https://git.example.de/benutzer/ProxLedger.git → { basis, repo } + */ +function ausRemote(): { basis: string; repo: string } | null { + try { + const url = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8" }).trim(); + const treffer = /^https?:\/\/(?:[^@/]*@)?([^/]+)\/(.+?)(?:\.git)?$/.exec(url); + if (!treffer) return null; + return { basis: `https://${treffer[1]}`, repo: treffer[2] }; + } catch { + return null; + } +} + +/** + * Holt den Token aus den Git-Zugangsdaten (Schlüsselbund oder `~/.git-credentials`). + * So genügt ein einziger Token für Push und API – er muss nicht zusätzlich in der .env stehen. + */ +function tokenAusGitCredentials(basis: string): string | null { + const host = new URL(basis).host; + + // Erst die eingerichteten Helfer (z. B. Schlüsselbund), dann zusätzlich die Datei + // ~/.git-credentials – je nach Rechner ist mal das eine, mal das andere gefüllt. + for (const zusatz of [[], ["-c", "credential.helper=store"]]) { + try { + const antwort = execFileSync("git", [...zusatz, "credential", "fill"], { + input: `protocol=https\nhost=${host}\n\n`, + encoding: "utf8", + // Keine Rückfrage im Terminal: Wenn nichts hinterlegt ist, soll der Aufruf scheitern. + env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, + stdio: ["pipe", "pipe", "ignore"], + }); + const passwort = /^password=(.*)$/m.exec(antwort); + if (passwort?.[1]) return passwort[1]; + } catch { + // Nächste Möglichkeit versuchen. + } + } + + return null; +} + /** Titel in Gitea, z. B. „#004 Agent-Skript“ – daran werden vorhandene Issues wiedererkannt. */ function giteaTitel(issue: IssueDatei): string { return `#${String(issue.nummer).padStart(3, "0")} ${issue.titel}`; @@ -145,9 +189,11 @@ function giteaTitel(issue: IssueDatei): string { async function main() { const trockenlauf = process.argv.includes("--dry-run"); - const basis = (process.env.GITEA_URL ?? "").replace(/\/$/, ""); - const repo = process.env.GITEA_REPO ?? ""; - const token = process.env.GITEA_TOKEN ?? ""; + const remote = ausRemote(); + const basis = (process.env.GITEA_URL ?? remote?.basis ?? "").replace(/\/$/, ""); + const repo = process.env.GITEA_REPO ?? remote?.repo ?? ""; + const token = process.env.GITEA_TOKEN || (basis ? (tokenAusGitCredentials(basis) ?? "") : ""); + const tokenQuelle = process.env.GITEA_TOKEN ? "Umgebungsvariable" : "Git-Zugangsdaten"; const verzeichnis = path.join(process.cwd(), "docs", "issues"); const issues = await dateienLesen(verzeichnis); @@ -155,7 +201,10 @@ async function main() { if (trockenlauf || !basis || !repo || !token) { if (!trockenlauf) { - console.log("\nGITEA_URL, GITEA_REPO oder GITEA_TOKEN fehlen – es läuft nur ein Trockenlauf.\n"); + console.log( + "\nAdresse, Repo oder Token nicht ermittelbar – es läuft nur ein Trockenlauf.\n" + + "Entweder GITEA_TOKEN setzen oder einmal `git push` ausführen, damit der Token hinterlegt wird.\n", + ); } for (const issue of issues) { console.log( @@ -165,11 +214,13 @@ async function main() { ); } console.log( - "\nEchter Abgleich: GITEA_URL, GITEA_REPO und GITEA_TOKEN setzen (Token mit Recht „issue:write“).", + `\nZiel wäre: ${basis || "?"}/${repo || "?"} – Token ${token ? "vorhanden" : "fehlt"}.` + + "\nDer Token wird aus den Git-Zugangsdaten gelesen; er braucht zusätzlich das Recht „issue:write“.", ); return; } + console.log(`Ziel: ${basis}/${repo} (Token aus: ${tokenQuelle})`); const gitea = new Gitea(basis, repo, token); const vorhandeneLabels = new Map((await gitea.labels()).map((l) => [l.name.toLowerCase(), l.id]));