Compare commits

...
2 Commits
Author SHA1 Message Date
florian.wilde 1e1d6c4418 Merge branch 'phase/020b-gitea-token' 2026-09-17 21:44:01 +02:00
florian.wildeandClaude Opus 5 2dbb30041b feat(tooling): Gitea-Token aus den Git-Zugangsdaten lesen (#020)
Serveradresse und Repository stammen aus dem Remote, der Token aus dem
Schluesselbund bzw. ~/.git-credentials. Ein zweiter Token in der .env entfaellt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:44:01 +02:00
6 changed files with 82 additions and 20 deletions
+5
View File
@@ -17,6 +17,11 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
### Sicherheit ### 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 ## [0.4.1] – 2026-09-17
### Hinzugefügt ### Hinzugefügt
+6 -4
View File
@@ -2,7 +2,7 @@
## Zeitübersicht ## Zeitübersicht
**Gesamt:** 2 h 48 min **Gesamt:** 2 h 58 min
| Issue | Zeit | | Issue | Zeit |
|---|---| |---|---|
@@ -13,7 +13,7 @@
| #005 Agent-Skript | 0 h 25 min | | #005 Agent-Skript | 0 h 25 min |
| #006 Kunden und Hostzuweisung | 0 h 35 min | | #006 Kunden und Hostzuweisung | 0 h 35 min |
| #007 Host-Tabelle und Detailansichten | 0 h 25 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 | | Meilenstein | Zeit |
|---|---| |---|---|
@@ -22,12 +22,13 @@
| v0.3.0 | 0 h 35 min | **abgeschlossen** | v0.3.0 | 0 h 35 min | **abgeschlossen**
| v0.4.0 | 0 h 25 min | **abgeschlossen** | v0.4.0 | 0 h 25 min | **abgeschlossen**
| v0.4.1 | 0 h 10 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 ## 2026-09-17 · Sitzung 1 · 19:36–laufend
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
**Version:** 0.4.1 **Version:** 0.4.2
### Erledigt ### Erledigt
- Auftrag gelesen, Rückfragen gestellt - 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 - #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 - 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) - #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 ### 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). - **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) - Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen)
### Nächster Schritt ### 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 - #008 Laufzeiten und Rechnungen: Vorschläge bestätigen, Rechnungen erfassen, Häkchen direkt in der Tabelle
+8 -8
View File
@@ -52,16 +52,16 @@ jedem Klon mit. Wer sie zusätzlich im Gitea-Tracker haben möchte, gleicht sie
```bash ```bash
npm run issues:sync -- --dry-run # zeigt nur, was passieren würde npm run issues:sync -- --dry-run # zeigt nur, was passieren würde
npm run issues:sync # echter Abgleich
GITEA_URL=https://git.example.de \
GITEA_REPO=benutzer/ProxLedger \
GITEA_TOKEN=... \
npm run issues:sync
``` ```
Der Token braucht das Recht `issue:write` (Gitea: Einstellungen → Anwendungen → Zugriffstoken). Er kann auch in Serveradresse und Repository liest das Skript aus dem Git-Remote, den Token aus den hinterlegten
`.env` stehen, die Datei ist von Git ausgenommen. Der Abgleich legt Labels und Meilensteine an, erkennt Git-Zugangsdaten (Schlüsselbund oder `~/.git-credentials`) – derselbe Token wie fürs Pushen, er braucht
vorhandene Issues an der Nummer im Titel, aktualisiert sie und schließt erledigte. 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 ## Dokumentation
+6 -2
View File
@@ -27,5 +27,9 @@ aktuell bleiben – ohne dass sie von Hand nachgepflegt werden müssen.
## Notizen / Entscheidungen ## Notizen / Entscheidungen
- Die Markdown-Dateien bleiben die Quelle der Wahrheit: Sie liegen im Repo, sind ohne Netz lesbar und - 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. 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 Token kommt aus den Git-Zugangsdaten (`git credential fill`), Adresse und Repo aus dem Remote.
Der Trockenlauf liest alle 20 Dateien korrekt. 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "proxledger", "name": "proxledger",
"version": "0.4.1", "version": "0.4.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
+56 -5
View File
@@ -10,6 +10,7 @@
* Die Markdown-Dateien bleiben die Quelle der Wahrheit; Gitea erhält eine Kopie zum Kommentieren. * 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 { readFile, readdir } from "node:fs/promises";
import "dotenv/config"; import "dotenv/config";
import path from "node:path"; 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. */ /** Titel in Gitea, z. B. „#004 Agent-Skript“ – daran werden vorhandene Issues wiedererkannt. */
function giteaTitel(issue: IssueDatei): string { function giteaTitel(issue: IssueDatei): string {
return `#${String(issue.nummer).padStart(3, "0")} ${issue.titel}`; return `#${String(issue.nummer).padStart(3, "0")} ${issue.titel}`;
@@ -145,9 +189,11 @@ function giteaTitel(issue: IssueDatei): string {
async function main() { async function main() {
const trockenlauf = process.argv.includes("--dry-run"); const trockenlauf = process.argv.includes("--dry-run");
const basis = (process.env.GITEA_URL ?? "").replace(/\/$/, ""); const remote = ausRemote();
const repo = process.env.GITEA_REPO ?? ""; const basis = (process.env.GITEA_URL ?? remote?.basis ?? "").replace(/\/$/, "");
const token = process.env.GITEA_TOKEN ?? ""; 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 verzeichnis = path.join(process.cwd(), "docs", "issues");
const issues = await dateienLesen(verzeichnis); const issues = await dateienLesen(verzeichnis);
@@ -155,7 +201,10 @@ async function main() {
if (trockenlauf || !basis || !repo || !token) { if (trockenlauf || !basis || !repo || !token) {
if (!trockenlauf) { 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) { for (const issue of issues) {
console.log( console.log(
@@ -165,11 +214,13 @@ async function main() {
); );
} }
console.log( 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; return;
} }
console.log(`Ziel: ${basis}/${repo} (Token aus: ${tokenQuelle})`);
const gitea = new Gitea(basis, repo, token); const gitea = new Gitea(basis, repo, token);
const vorhandeneLabels = new Map((await gitea.labels()).map((l) => [l.name.toLowerCase(), l.id])); const vorhandeneLabels = new Map((await gitea.labels()).map((l) => [l.name.toLowerCase(), l.id]));