From 6613c9ef29d2285a2951601b3a7f06b23c9d7ec3 Mon Sep 17 00:00:00 2001 From: Serge Koenigsmann Date: Thu, 17 Sep 2026 08:49:55 +0200 Subject: [PATCH] redmine: News und Versionen steuerbar (0.2.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Übernimmt den Stand aus inmedias.it/dokumentierer (7097383). Beide Fassungen waren bis hierhin byte-identisch und sollen es bleiben. `news` liest, legt an und ändert; `versions` listet und legt an, `create`/`update` nehmen `--version`. Der Hinweis in SKILL.md, Versionen seien "über redmine.py nicht abgedeckt", stimmt damit nicht mehr und ist ersetzt - durch zwei Eigenheiten, die beim Anlegen des Projekts devops aufgefallen sind: Redmine begrenzt News-Titel auf 60 Zeichen (sonst HTTP 422), und die Priorität eines Elterntickets wird aus den Unteraufgaben abgeleitet, überschreibt also einen von Hand gesetzten Wert. Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/marketplace.json | 2 +- README.md | 10 +- plugins/redmine/.claude-plugin/plugin.json | 4 +- plugins/redmine/skills/redmine/SKILL.md | 63 +++++++++-- plugins/redmine/skills/redmine/redmine.py | 125 ++++++++++++++++++++- 5 files changed, 185 insertions(+), 19 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5da1c4f..79549c7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -36,7 +36,7 @@ { "name": "redmine", "source": "./plugins/redmine", - "description": "Redmine via REST-API: Projekte/Tickets auflisten, anzeigen, anlegen, aktualisieren und kommentieren via redmine.py (API-Key lokal)." + "description": "Redmine via REST-API: Projekte, Tickets, Versionen und News auflisten, anzeigen, anlegen, aktualisieren und kommentieren via redmine.py (API-Key lokal)." }, { "name": "grill-me", diff --git a/README.md b/README.md index 366d1fc..c9bcd15 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,11 @@ self-contained `wiki.py` (`fetch`/`render`/`search`/`update`). Instanzen `wiki` ### redmine -Redmine (redmine.inmedias.it) über die REST-API: Projekte und Tickets auflisten, -anzeigen, anlegen, aktualisieren und kommentieren — via mitgelieferter, -self-contained `redmine.py` (`me`/`projects`/`issues`/`show`/`create`/`update`/ -`comment`/`meta`). Namen (Projekt/Tracker/Status/Priorität) werden auf IDs -aufgelöst. +Redmine (redmine.inmedias.it) über die REST-API: Projekte, Tickets, Versionen +und News auflisten, anzeigen, anlegen, aktualisieren und kommentieren — via +mitgelieferter, self-contained `redmine.py` (`me`/`projects`/`issues`/`show`/ +`create`/`update`/`comment`/`news`/`versions`/`meta`). Namen (Projekt/Tracker/ +Status/Priorität/Version) werden auf IDs aufgelöst. - Voraussetzung: `REDMINE_URL` und persönlicher `REDMINE_API_KEY` als Umgebungsvariable bzw. dotenv-Datei (kein Key im Plugin). API-Key unter diff --git a/plugins/redmine/.claude-plugin/plugin.json b/plugins/redmine/.claude-plugin/plugin.json index b1e7164..068e76a 100644 --- a/plugins/redmine/.claude-plugin/plugin.json +++ b/plugins/redmine/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "redmine", - "version": "0.1.0", - "description": "Redmine via REST-API: Projekte/Tickets auflisten, anlegen, aktualisieren, kommentieren (redmine.py).", + "version": "0.2.0", + "description": "Redmine via REST-API: Projekte/Tickets/Versionen/News auflisten, anlegen, aktualisieren, kommentieren (redmine.py).", "author": { "name": "inmedias.it" } diff --git a/plugins/redmine/skills/redmine/SKILL.md b/plugins/redmine/skills/redmine/SKILL.md index 800c146..89e22c8 100644 --- a/plugins/redmine/skills/redmine/SKILL.md +++ b/plugins/redmine/skills/redmine/SKILL.md @@ -1,15 +1,16 @@ --- name: redmine -description: Redmine-Projekte und -Tickets lesen/anlegen/aktualisieren/kommentieren via REST-API. Use when the user mentions "Redmine", "Ticket", "Issue in Redmine", "Meilenstein/Milestone" or das Projekt "pg-wissen". -version: 0.1.0 +description: Redmine-Projekte, -Tickets, -Versionen und -News lesen/anlegen/aktualisieren via REST-API. Use when the user mentions "Redmine", "Ticket", "Issue in Redmine", "Meilenstein/Milestone/Version", "Projektstand/News" or die Projekte "pg-wissen"/"devops". +version: 0.2.0 --- # Redmine (redmine.py) Interagiert mit Redmine (https://redmine.inmedias.it) über die REST-API: -Projekte und Tickets auflisten, anzeigen, anlegen, aktualisieren und -kommentieren — über die mitgelieferte, self-contained `redmine.py`. Namen -werden auf IDs aufgelöst (Projekt, Tracker, Status, Priorität). +Projekte, Tickets, Versionen und News auflisten, anzeigen, anlegen, +aktualisieren und kommentieren - über die mitgelieferte, self-contained +`redmine.py`. Namen werden auf IDs aufgelöst (Projekt, Tracker, Status, +Priorität, Version). ## Ausführen @@ -43,9 +44,10 @@ werden auf IDs aufgelöst (Projekt, Tracker, Status, Priorität). ### Anlegen / Aktualisieren / Kommentieren `--project` und `--subject` sind bei `create` Pflicht. Beschreibung inline -(`--description`) oder aus Datei/stdin (`--description-file -`) übergeben — nicht +(`--description`) oder aus Datei/stdin (`--description-file -`) übergeben - nicht als langes Shell-Argument (Sonderzeichen). Weitere Felder: `--tracker`, -`--status`, `--priority`, `--assigned me`, `--parent `, `--done <0-100>`. +`--status`, `--priority`, `--assigned me`, `--parent `, `--version `, +`--done <0-100>`. ```bash ./redmine.py create --project pg-wissen --subject "Titel" \ @@ -54,6 +56,44 @@ als langes Shell-Argument (Sonderzeichen). Weitere Felder: `--tracker`, ./redmine.py comment 290 "kurzer Kommentar" ``` +### Versionen (Meilensteine) + +Versionen tragen die grobe Reihenfolge und speisen die Roadmap-Ansicht. + +```bash +./redmine.py versions --project devops # auflisten +./redmine.py versions --project devops --name "Phase 1" \ + --due 2026-10-15 --description "Was in dieser Phase zu tun ist" +./redmine.py create --project devops --subject "Schritt" --version "Phase 1" +./redmine.py update 350 --version 23 # ohne --project: ID +``` + +Ein Versionsname lässt sich nur mit `--project` auflösen, weil Versionen zu +einem Projekt gehören. Bei `update` ist `--project` meist nicht angegeben - +dort die Versions-ID verwenden. + +Anlegen erfordert das Recht *manage_versions*. + +### News (Projektstand, Ankündigungen) + +```bash +./redmine.py news --project devops # auflisten +./redmine.py news --project devops --title "Kurzer Titel" \ + --summary "Eine Zeile für die Übersicht" \ + --description-file text.md +./redmine.py news --id 1 --description-file korrigiert.md # ändern +``` + +Zwei Eigenheiten der Redmine-API: + +- **Der Titel darf höchstens 60 Zeichen haben**, sonst HTTP 422. `redmine.py` + prüft das vorab. +- Die Antwort beim Anlegen enthält **keine News-ID**; der Befehl gibt deshalb + die Projekt-URL aus statt eines toten Direktlinks. Ändern per `--id` geht + trotzdem (`PUT /news/{id}.json`). + +Format ist Markdown, wie bei Tickets. + ## Zugangsdaten `redmine.py` bringt **keinen** Schlüssel mit. Jede/r hinterlegt einen eigenen @@ -71,6 +111,9 @@ gesetzte Env-Variablen). Siehe `.env.example`: - Schreibzugriff nur in Projekten mit „Add/Edit issues"-Recht; ein Nicht-Admin kann keine Tickets in fremden Projekten ändern. -- Ein neues Milestone (Version) anzulegen erfordert das Recht *manage_versions* - und ist über `redmine.py` nicht abgedeckt — dafür die Weboberfläche oder die - REST-API (`POST /projects//versions.json`) nutzen. +- **Ticket-Beziehungen** (`blocks`, `relates`, `precedes`) deckt `redmine.py` + nicht ab - dafür `POST /issues//relations.json`. Ein Ticket kann nicht mit + einem eigenen Unterticket verknüpft werden (HTTP 422). +- Die Priorität eines Elterntickets leitet Redmine aus den Unteraufgaben ab; + ein von Hand gesetzter Wert wird überschrieben. Abschaltbar unter + Administration → Konfiguration → „Attribute übergeordneter Tickets". diff --git a/plugins/redmine/skills/redmine/redmine.py b/plugins/redmine/skills/redmine/redmine.py index 593f087..69bd01e 100755 --- a/plugins/redmine/skills/redmine/redmine.py +++ b/plugins/redmine/skills/redmine/redmine.py @@ -19,6 +19,12 @@ Beispiele: uv run redmine.py update 42 --status Erledigt --notes "gefixt" uv run redmine.py comment 42 "kurzer Kommentar" uv run redmine.py meta trackers + uv run redmine.py versions --project devops + uv run redmine.py versions --project devops --name "Phase 1" --due 2026-10-15 + uv run redmine.py create --project devops --subject "Titel" --version "Phase 1" + uv run redmine.py news --project devops + uv run redmine.py news --project devops --title "Kurz" --description-file text.md + uv run redmine.py news --id 1 --description-file korrigiert.md """ import argparse @@ -162,6 +168,45 @@ def cmd_show(s, a): + "\n".join(" " + ln for ln in j["notes"].splitlines())) +def resolve_version(s, project, wert: str) -> int: + """Version über Name oder ID auflösen. + + Ein Name lässt sich nur mit `--project` auflösen, weil Versionen zu einem + Projekt gehören. Beim Ändern eines Tickets ist `--project` aber meist + nicht angegeben - dort ist die Versions-ID anzugeben. + """ + if str(wert).isdigit(): + return int(wert) + if not project: + sys.exit("--version mit Namen braucht --project (oder gib die Versions-ID an)") + pid = resolve_project(s, project) + for v in api(s, "GET", f"/projects/{pid}/versions.json").get("versions", []): + if v["name"].lower() == str(wert).lower(): + return v["id"] + sys.exit(f"Version '{wert}' nicht in Projekt {pid} gefunden") + + +def cmd_versions(s, a): + """Versionen eines Projekts auflisten oder anlegen.""" + pid = resolve_project(s, a.project) + if a.name: + f = {"name": a.name} + if a.due: + f["due_date"] = a.due + if a.description: + f["description"] = a.description + v = api(s, "POST", f"/projects/{pid}/versions.json", + data=json.dumps({"version": f}))["version"] + return print(f"Angelegt: Version #{v['id']} {v['name']}") + items = api(s, "GET", f"/projects/{pid}/versions.json").get("versions", []) + if a.json: + return print(json.dumps(items, ensure_ascii=False, indent=2)) + if not items: + return print("— keine Versionen") + for v in items: + print(f"#{v['id']:>4} {v['status']:<8} {v.get('due_date') or '—':<12} {v['name']}") + + def build_issue_fields(s, a) -> dict: f = {} if getattr(a, "project", None): @@ -182,6 +227,8 @@ def build_issue_fields(s, a) -> dict: f["assigned_to_id"] = resolve_assigned(s, a.assigned) if getattr(a, "parent", None): f["parent_issue_id"] = int(a.parent) + if getattr(a, "version", None): + f["fixed_version_id"] = resolve_version(s, getattr(a, "project", None), a.version) if getattr(a, "done", None) is not None: f["done_ratio"] = int(a.done) if getattr(a, "notes", None): @@ -211,6 +258,65 @@ def cmd_comment(s, a): print(f"Kommentar an #{a.id} hinzugefügt.") +def cmd_news(s, a): + """News lesen, anlegen oder ändern. + + Drei Betriebsarten, unterschieden an den Angaben: + ohne --id, ohne --title auflisten + --title anlegen (--project ist Pflicht) + --id ändern (PUT, antwortet mit 204) + + Der Titel darf höchstens 60 Zeichen haben; darüber antwortet Redmine + mit HTTP 422. Geprüft wird das hier schon vorab, damit der Fehler beim + Aufruf steht und nicht im Serverprotokoll. + """ + if getattr(a, "id", None): + fields = {} + if a.title: + fields["title"] = a.title + if a.summary is not None: + fields["summary"] = a.summary + body = read_text(a.description, a.description_file) + if body is not None: + fields["description"] = body + if not fields: + sys.exit("news: nichts zu ändern angegeben") + if len(fields.get("title", "")) > 60: + sys.exit("news: Titel darf höchstens 60 Zeichen haben") + api(s, "PUT", f"/news/{a.id}.json", data=json.dumps({"news": fields})) + return print(f"Geändert: News #{a.id} ({', '.join(fields)}) -> {s.base}/news/{a.id}") + + if a.title is None: + pid = resolve_project(s, a.project) if a.project else None + path = f"/projects/{pid}/news.json" if pid else "/news.json" + items = api(s, "GET", path, params={"limit": a.limit}).get("news", []) + if a.json: + return print(json.dumps(items, ensure_ascii=False, indent=2)) + if not items: + return print("— keine News") + for n in items: + print(f"#{n['id']:>4} {n.get('created_on', '')[:10]} {n['title']}") + return + + if not a.project: + sys.exit("news: zum Anlegen ist --project Pflicht") + if len(a.title) > 60: + sys.exit("news: Titel darf höchstens 60 Zeichen haben") + body = read_text(a.description, a.description_file) + if body is None: + sys.exit("news: --description oder --description-file angeben") + pid = resolve_project(s, a.project) + fields = {"title": a.title, "description": body} + if a.summary: + fields["summary"] = a.summary + res = api(s, "POST", f"/projects/{pid}/news.json", data=json.dumps({"news": fields})) + n = res.get("news", {}) + if n.get("id"): + print(f"Angelegt: News #{n['id']} {n.get('title', '')} -> {s.base}/news/{n['id']}") + else: + print(f"Angelegt in Projekt {pid} -> {s.base}/projects/{pid}/news") + + def cmd_meta(s, a): spec = { "trackers": ("/trackers.json", "trackers"), @@ -258,6 +364,7 @@ def main(): c.add_argument("--priority", help="Priorität (Name oder ID)") c.add_argument("--assigned", help="me | User-ID") c.add_argument("--parent", help="übergeordnetes Ticket (ID)") + c.add_argument("--version", help="Zielversion (Name oder ID)") c.add_argument("--done", type=int, help="Fortschritt in %% (0-100)") if name == "update": c.add_argument("--notes", help="Kommentar zur Änderung") @@ -265,6 +372,21 @@ def main(): pc = sub.add_parser("comment", help="Kommentar an ein Ticket hängen") pc.add_argument("id"); pc.add_argument("text") + pn = sub.add_parser("news", help="News lesen, anlegen oder ändern") + pn.add_argument("--project", help="Projekt (ID/Identifier/Name); zum Anlegen Pflicht") + pn.add_argument("--id", help="News-ID - gesetzt heißt: ändern") + pn.add_argument("--title", help="Titel (max. 60 Zeichen); gesetzt heißt: anlegen statt auflisten") + pn.add_argument("--summary", help="Kurzfassung (optional, erscheint in der Übersicht)") + pn.add_argument("--description", help="Text") + pn.add_argument("--description-file", help="Datei oder '-' für stdin") + pn.add_argument("--limit", type=int, default=25) + + pv = sub.add_parser("versions", help="Versionen auflisten oder anlegen") + pv.add_argument("--project", required=True, help="Projekt (ID/Identifier/Name)") + pv.add_argument("--name", help="Name - gesetzt heißt: anlegen statt auflisten") + pv.add_argument("--due", help="Fälligkeitsdatum (YYYY-MM-DD)") + pv.add_argument("--description", help="Beschreibung") + pm = sub.add_parser("meta", help="Stammdaten auflisten") pm.add_argument("kind", choices=["trackers", "statuses", "priorities"]) @@ -272,7 +394,8 @@ def main(): s, _ = session() { "me": cmd_me, "projects": cmd_projects, "issues": cmd_issues, "show": cmd_show, - "create": cmd_create, "update": cmd_update, "comment": cmd_comment, "meta": cmd_meta, + "create": cmd_create, "update": cmd_update, "comment": cmd_comment, + "news": cmd_news, "versions": cmd_versions, "meta": cmd_meta, }[a.cmd](s, a) -- 2.39.5