Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab56ba0c64 | ||
|
|
6613c9ef29 |
No files matched your search
@@ -36,7 +36,7 @@
|
|||||||
{
|
{
|
||||||
"name": "redmine",
|
"name": "redmine",
|
||||||
"source": "./plugins/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",
|
"name": "grill-me",
|
||||||
|
|||||||
@@ -110,11 +110,11 @@ self-contained `wiki.py` (`fetch`/`render`/`search`/`update`). Instanzen `wiki`
|
|||||||
|
|
||||||
### redmine
|
### redmine
|
||||||
|
|
||||||
Redmine (redmine.inmedias.it) über die REST-API: Projekte und Tickets auflisten,
|
Redmine (redmine.inmedias.it) über die REST-API: Projekte, Tickets, Versionen
|
||||||
anzeigen, anlegen, aktualisieren und kommentieren — via mitgelieferter,
|
und News auflisten, anzeigen, anlegen, aktualisieren und kommentieren — via
|
||||||
self-contained `redmine.py` (`me`/`projects`/`issues`/`show`/`create`/`update`/
|
mitgelieferter, self-contained `redmine.py` (`me`/`projects`/`issues`/`show`/
|
||||||
`comment`/`meta`). Namen (Projekt/Tracker/Status/Priorität) werden auf IDs
|
`create`/`update`/`comment`/`news`/`versions`/`meta`). Namen (Projekt/Tracker/
|
||||||
aufgelöst.
|
Status/Priorität/Version) werden auf IDs aufgelöst.
|
||||||
|
|
||||||
- Voraussetzung: `REDMINE_URL` und persönlicher `REDMINE_API_KEY` als
|
- Voraussetzung: `REDMINE_URL` und persönlicher `REDMINE_API_KEY` als
|
||||||
Umgebungsvariable bzw. dotenv-Datei (kein Key im Plugin). API-Key unter
|
Umgebungsvariable bzw. dotenv-Datei (kein Key im Plugin). API-Key unter
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "redmine",
|
"name": "redmine",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Redmine via REST-API: Projekte/Tickets auflisten, anlegen, aktualisieren, kommentieren (redmine.py).",
|
"description": "Redmine via REST-API: Projekte/Tickets/Versionen/News auflisten, anlegen, aktualisieren, kommentieren (redmine.py).",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "inmedias.it"
|
"name": "inmedias.it"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
---
|
---
|
||||||
name: redmine
|
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".
|
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.1.0
|
version: 0.2.0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Redmine (redmine.py)
|
# Redmine (redmine.py)
|
||||||
|
|
||||||
Interagiert mit Redmine (https://redmine.inmedias.it) über die REST-API:
|
Interagiert mit Redmine (https://redmine.inmedias.it) über die REST-API:
|
||||||
Projekte und Tickets auflisten, anzeigen, anlegen, aktualisieren und
|
Projekte, Tickets, Versionen und News auflisten, anzeigen, anlegen,
|
||||||
kommentieren — über die mitgelieferte, self-contained `redmine.py`. Namen
|
aktualisieren und kommentieren - über die mitgelieferte, self-contained
|
||||||
werden auf IDs aufgelöst (Projekt, Tracker, Status, Priorität).
|
`redmine.py`. Namen werden auf IDs aufgelöst (Projekt, Tracker, Status,
|
||||||
|
Priorität, Version).
|
||||||
|
|
||||||
## Ausführen
|
## Ausführen
|
||||||
|
|
||||||
@@ -43,9 +44,10 @@ werden auf IDs aufgelöst (Projekt, Tracker, Status, Priorität).
|
|||||||
### Anlegen / Aktualisieren / Kommentieren
|
### Anlegen / Aktualisieren / Kommentieren
|
||||||
|
|
||||||
`--project` und `--subject` sind bei `create` Pflicht. Beschreibung inline
|
`--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`,
|
als langes Shell-Argument (Sonderzeichen). Weitere Felder: `--tracker`,
|
||||||
`--status`, `--priority`, `--assigned me`, `--parent <id>`, `--done <0-100>`.
|
`--status`, `--priority`, `--assigned me`, `--parent <id>`, `--version <name|id>`,
|
||||||
|
`--done <0-100>`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./redmine.py create --project pg-wissen --subject "Titel" \
|
./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"
|
./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
|
## Zugangsdaten
|
||||||
|
|
||||||
`redmine.py` bringt **keinen** Schlüssel mit. Jede/r hinterlegt einen eigenen
|
`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
|
- Schreibzugriff nur in Projekten mit „Add/Edit issues"-Recht; ein Nicht-Admin
|
||||||
kann keine Tickets in fremden Projekten ändern.
|
kann keine Tickets in fremden Projekten ändern.
|
||||||
- Ein neues Milestone (Version) anzulegen erfordert das Recht *manage_versions*
|
- **Ticket-Beziehungen** (`blocks`, `relates`, `precedes`) deckt `redmine.py`
|
||||||
und ist über `redmine.py` nicht abgedeckt — dafür die Weboberfläche oder die
|
nicht ab - dafür `POST /issues/<id>/relations.json`. Ein Ticket kann nicht mit
|
||||||
REST-API (`POST /projects/<id>/versions.json`) nutzen.
|
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".
|
||||||
@@ -19,6 +19,12 @@ Beispiele:
|
|||||||
uv run redmine.py update 42 --status Erledigt --notes "gefixt"
|
uv run redmine.py update 42 --status Erledigt --notes "gefixt"
|
||||||
uv run redmine.py comment 42 "kurzer Kommentar"
|
uv run redmine.py comment 42 "kurzer Kommentar"
|
||||||
uv run redmine.py meta trackers
|
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
|
import argparse
|
||||||
@@ -162,6 +168,45 @@ def cmd_show(s, a):
|
|||||||
+ "\n".join(" " + ln for ln in j["notes"].splitlines()))
|
+ "\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:
|
def build_issue_fields(s, a) -> dict:
|
||||||
f = {}
|
f = {}
|
||||||
if getattr(a, "project", None):
|
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)
|
f["assigned_to_id"] = resolve_assigned(s, a.assigned)
|
||||||
if getattr(a, "parent", None):
|
if getattr(a, "parent", None):
|
||||||
f["parent_issue_id"] = int(a.parent)
|
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:
|
if getattr(a, "done", None) is not None:
|
||||||
f["done_ratio"] = int(a.done)
|
f["done_ratio"] = int(a.done)
|
||||||
if getattr(a, "notes", None):
|
if getattr(a, "notes", None):
|
||||||
@@ -211,6 +258,65 @@ def cmd_comment(s, a):
|
|||||||
print(f"Kommentar an #{a.id} hinzugefügt.")
|
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):
|
def cmd_meta(s, a):
|
||||||
spec = {
|
spec = {
|
||||||
"trackers": ("/trackers.json", "trackers"),
|
"trackers": ("/trackers.json", "trackers"),
|
||||||
@@ -258,6 +364,7 @@ def main():
|
|||||||
c.add_argument("--priority", help="Priorität (Name oder ID)")
|
c.add_argument("--priority", help="Priorität (Name oder ID)")
|
||||||
c.add_argument("--assigned", help="me | User-ID")
|
c.add_argument("--assigned", help="me | User-ID")
|
||||||
c.add_argument("--parent", help="übergeordnetes Ticket (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)")
|
c.add_argument("--done", type=int, help="Fortschritt in %% (0-100)")
|
||||||
if name == "update":
|
if name == "update":
|
||||||
c.add_argument("--notes", help="Kommentar zur Änderung")
|
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 = sub.add_parser("comment", help="Kommentar an ein Ticket hängen")
|
||||||
pc.add_argument("id"); pc.add_argument("text")
|
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 = sub.add_parser("meta", help="Stammdaten auflisten")
|
||||||
pm.add_argument("kind", choices=["trackers", "statuses", "priorities"])
|
pm.add_argument("kind", choices=["trackers", "statuses", "priorities"])
|
||||||
|
|
||||||
@@ -272,7 +394,8 @@ def main():
|
|||||||
s, _ = session()
|
s, _ = session()
|
||||||
{
|
{
|
||||||
"me": cmd_me, "projects": cmd_projects, "issues": cmd_issues, "show": cmd_show,
|
"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)
|
}[a.cmd](s, a)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user