61 Eintraege aus docs/ROADMAP.md stehen jetzt als Items im Deploymentcenter:
17 offen, 24 erledigt, 7 zurueckgestellt, 13 verworfen. Keine Waisen, jedes
Item mit doc_anchor. Der Abgleich laeuft ohne Befund durch.
Die Datei behaelt, was eine Tabellenzelle nicht traegt: die Begruendungen.
Der Zustand steht zwischen dc:roadmap-Markern und wird erzeugt.
- scripts/roadmap_import.py: wertet die Datei aus und uebertraegt sie.
Wiederholbar ueber client_ref, legt fehlende Themenbereiche an, wartet
bei Drosselung. Die Prioritaet liest es aus dem Abschnitt "Als
Naechstes" - solange die Datei die Quelle ist, soll auch die Prioritaet
von dort kommen.
- docs/ROADMAP.md: Statustabellen raus, erzeugter Block rein,
21 Ueberschriften und 14 Querverweise auf Item-Nummern umgestellt,
Abschnitt H mit Item-Nummern versehen.
WAS DIE UEBERNAHME ZUTAGE FOERDERTE
- G8 war zweimal vergeben: einmal offen, einmal erledigt. Genau die
Kollision handvergebener Kennungen, gegen die der Entwurf argumentiert.
Beide sind eigene Items.
- Bereich H fehlte in roadmap_epics - Migration 016 hatte nur A-G und I
gesetzt. Die Waisenerkennung meldete es. Das Importskript legt fehlende
Bereiche jetzt an, ueberschreibt aber keine vorhandenen: Ein
Uebernahmeskript darf anlegen, kuratierte Angaben nicht ueberschreiben.
- "Item #42" stand als erfundenes Beispiel im Rueckkanal-Absatz - und #42
gibt es wirklich, in predictalytics. Der Abgleich hat es gemeldet, ein
Mensch haette es nie bemerkt.
- Vier Marker waren veraltet (E1, E2, I8, D1).
BEHOBEN: spliceBlock() zerstoerte Dateien, die den Marker erwaehnen
strpos() nahm die erste Fundstelle von "<!-- dc:roadmap". Erwaehnt eine Datei
den Marker im Fliesstext - und die Einleitung einer ordentlichen Roadmap tut
genau das - setzte der Splice dort an und ueberschrieb den halben Dateikopf.
check() klammerte aus demselben Grund Prosa aus statt des Blocks.
Es trifft also ausgerechnet die Dateien, die das Verfahren beschreiben. Der
echte Block wird jetzt am Zeilenanfang samt "stand=" erkannt; das Blockende
wird ab dem Anfang gesucht, nicht ab Dateibeginn.
BEHOBEN: IPv6-Erkennung im Testlauf war launisch
erreichbar() prueft seit jeher IPv6 mit einem blossen connect(). Auf dieser
Strecke gelingt der Verbindungsaufbau zeitweise, waehrend danach nichts mehr
fliesst - der Lauf blieb dann auf IPv6 und jede Anfrage lief 21 Sekunden in
einen Timeout. Weil es launisch war, sah es mal nach einem Haenger und mal
nach einem sauberen Lauf aus. Geprueft wird jetzt bis zur Antwort:
TLS-Handschlag und ein HTTP-HEAD.
WebUI: Verweise im Inhalt hatten keine Farbregel und erschienen im
Browservorgabeblau auf schwarzem Grund. Der Bereichsverweis der Roadmap
springt ausserdem auf die Liste statt auf die Uebersicht - dort wirkt der
Filter, den er setzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
486 lines
18 KiB
Python
486 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Eine handgepflegte ROADMAP.md ins Deploymentcenter uebernehmen.
|
|
|
|
Einmalig je Projekt. Das Verfahren steht in docs/ROADMAP_MIGRATION.md; dieses
|
|
Skript nimmt davon die Schritte 3 und 4 ab - Items anlegen und einplanen.
|
|
|
|
python scripts/roadmap_import.py --lesen
|
|
Nur auswerten und zeigen, was gefunden wurde. Kein Netzverkehr.
|
|
|
|
python scripts/roadmap_import.py --probe
|
|
Gegen die Testinstanz laufen lassen. Braucht ein Werkbank-Token, das
|
|
das Skript sich ueber testctl selbst ausstellt.
|
|
|
|
python scripts/roadmap_import.py --uebernehmen
|
|
Gegen die Produktion. DC_TOKEN muss ein Werkbank-Token des Projekts
|
|
sein (owner_type = dev_agent, Scope bugtracker:manage).
|
|
|
|
Wiederholbar: Jedes Item traegt ein client_ref aus Projekt und alter Kennung
|
|
("roadmap-deploymentcenter-a4"). Ein zweiter Lauf legt deshalb nichts doppelt
|
|
an, sondern findet dieselben Items wieder und schreibt nur die Roadmap-Felder
|
|
neu. Bei sechzig Eintraegen laeuft irgendetwas immer zweimal.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
PROJEKT_DIR = Path(__file__).resolve().parent.parent
|
|
ROADMAP = PROJEKT_DIR / 'docs' / 'ROADMAP.md'
|
|
SLUG = 'deploymentcenter'
|
|
ANKER_DATEI = 'docs/ROADMAP.md'
|
|
|
|
# Zeichen -> (Status, ob als Vorhaben zaehlend)
|
|
MARKER = {
|
|
'✅': 'resolved',
|
|
'🔨': 'in_progress',
|
|
'📋': 'open',
|
|
'🧊': 'deferred',
|
|
'❌': 'rejected',
|
|
'⚙️': 'open', # Betriebsaufgabe: kein Code, aber uebernehmbar
|
|
}
|
|
|
|
# Reihenfolge der Bereiche in roadmap_epics (Migration 016)
|
|
BEREICHE = {
|
|
'A': 'Supervisor und Befunde',
|
|
'B': 'Oberflaeche',
|
|
'C': 'Projektlandschaft',
|
|
'D': 'Werkzeuganbindung (MCP)',
|
|
'E': 'Betrieb',
|
|
'F': 'Quelltext-Zugriff',
|
|
'G': 'Testumgebung',
|
|
'H': 'Verworfen',
|
|
'I': 'Roadmap im Deploymentcenter',
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auswerten
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def entferne_auszeichnung(text):
|
|
"""Markdown-Auszeichnung raus - ein Item-Titel ist kein Fliesstext."""
|
|
text = re.sub(r'~~(.+?)~~', r'\1', text)
|
|
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
|
text = re.sub(r'`([^`]+)`', r'\1', text)
|
|
text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
|
|
return re.sub(r'\s+', ' ', text).strip()
|
|
|
|
|
|
def lies_ausloeser(quelle, kennung):
|
|
"""
|
|
Holt den Ausloeser eines zurueckgestellten Vorhabens aus seinem Absatz.
|
|
|
|
Die Datei schreibt ihn durchgaengig als "**Ausloeser:** ...". Fehlt er,
|
|
ist der Eintrag nach der eigenen Regel gar nicht zurueckgestellt, sondern
|
|
vergessen - dann meldet das Skript das, statt etwas zu erfinden.
|
|
"""
|
|
abschnitt = re.search(
|
|
r'^###\s+%s\s+[—-][^\n]*\n(.*?)(?=^###|\Z)' % re.escape(kennung),
|
|
quelle, re.M | re.S)
|
|
if not abschnitt:
|
|
return None
|
|
m = re.search(r'\*\*Ausl[öo]ser:\*\*\s*(.+?)(?:\n\n|\Z)', abschnitt.group(1), re.S)
|
|
if not m:
|
|
return None
|
|
return entferne_auszeichnung(m.group(1))[:400]
|
|
|
|
|
|
def lies_fassung(stand):
|
|
"""Aus "✅ 2.6.1 — als Arbeitsschlange" wird "2.6.1"."""
|
|
m = re.search(r'(\d+\.\d+\.\d+)', stand)
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def anker(kennung, titel):
|
|
"""
|
|
Verweis auf die Stelle, an der die Begruendung steht.
|
|
|
|
Nach dem Umbau der Datei traegt die Ueberschrift die Item-Nummer; bis
|
|
dahin zeigt der Anker auf die alte Kennung. Das ist Absicht: Der Anker
|
|
soll auch waehrend der Umstellung auf etwas zeigen, das es gibt.
|
|
"""
|
|
stueck = re.sub(r'[^a-z0-9]+', '-', entferne_auszeichnung(titel).lower()).strip('-')
|
|
return '%s#%s-%s' % (ANKER_DATEI, kennung.lower(), stueck[:60])
|
|
|
|
|
|
def lies_reihenfolge(quelle):
|
|
"""
|
|
Die Prioritaet aus dem Abschnitt "Als Naechstes".
|
|
|
|
Sie steht dort als nummerierte Liste mit den Kennungen in eckigen Klammern
|
|
("1. **[B1] Befunde in der Oberflaeche**"). Sie hier auszulesen statt im
|
|
Skript zu hinterlegen ist der Punkt: Solange die Datei die Quelle ist,
|
|
soll auch die Prioritaet aus ihr kommen und nicht aus einer zweiten Liste,
|
|
die daneben veraltet.
|
|
|
|
Danach ergibt sich dieselbe Reihenfolge aus dem Rang im Deploymentcenter -
|
|
und der Abschnitt in der Datei entfaellt.
|
|
|
|
@return Liste der Kennungen in Prioritaetsreihenfolge
|
|
"""
|
|
m = re.search(r'^## Als N[äa]chstes\b(.*?)(?=^---|\Z)', quelle, re.M | re.S)
|
|
if not m:
|
|
return []
|
|
return re.findall(r'^\d+\.\s*\*\*\[([A-Z]\d+)\]', m.group(1), re.M)
|
|
|
|
|
|
def auswerten():
|
|
quelle = ROADMAP.read_text(encoding='utf-8')
|
|
eintraege = []
|
|
gesehen = {}
|
|
|
|
# --- Die nummerierten Vorhaben aus den Bereichstabellen -----------------
|
|
for m in re.finditer(r'^\| *([A-Z])(\d+) *\| *(.+?) *\| *(.+?) *\|\s*$', quelle, re.M):
|
|
bereich, nummer, titel, stand = m.groups()
|
|
kennung = bereich + nummer
|
|
|
|
status = next((v for k, v in MARKER.items() if k in stand), 'open')
|
|
betrieb = '⚙️' in stand
|
|
|
|
# Doppelt vergebene Kennung: Genau der Fehler, den Item-IDs abschaffen.
|
|
# Nicht stillschweigend zusammenfuehren - beide sind eigene Vorhaben.
|
|
if kennung in gesehen:
|
|
gesehen[kennung] += 1
|
|
kennung_eindeutig = '%s-%d' % (kennung, gesehen[kennung])
|
|
else:
|
|
gesehen[kennung] = 1
|
|
kennung_eindeutig = kennung
|
|
|
|
eintraege.append({
|
|
'kennung': kennung_eindeutig,
|
|
'epic': bereich,
|
|
'titel': entferne_auszeichnung(titel),
|
|
'status': status,
|
|
'betrieb': betrieb,
|
|
'fassung': lies_fassung(stand),
|
|
'ausloeser': lies_ausloeser(quelle, kennung) if status == 'deferred' else None,
|
|
'stand_roh': stand,
|
|
})
|
|
|
|
# --- Abschnitt H: Verworfenes ------------------------------------------
|
|
#
|
|
# Ohne Kennung, zwei Spalten (Vorschlag | Warum nicht). Sie kommen mit ins
|
|
# DC, damit dc_roadmap die Frage "wurde das schon abgelehnt?" beantworten
|
|
# kann - sonst muesste ein Agent dafuer die Datei lesen, und genau das
|
|
# tut er nicht zuverlaessig. Die Begruendung bleibt in der Datei.
|
|
h = re.search(r'^## H — Verworfen\b(.*?)(?=^## |\Z)', quelle, re.M | re.S)
|
|
if h:
|
|
nr = 0
|
|
for zeile in re.finditer(r'^\| *(❌ *)?(.+?) *\| *(.+?) *\|\s*$', h.group(1), re.M):
|
|
vorschlag = entferne_auszeichnung(zeile.group(2))
|
|
grund = entferne_auszeichnung(zeile.group(3))
|
|
if not vorschlag or vorschlag.startswith('---') or vorschlag == 'Vorschlag':
|
|
continue
|
|
nr += 1
|
|
eintraege.append({
|
|
'kennung': 'H%d' % nr,
|
|
'epic': 'H',
|
|
'titel': vorschlag,
|
|
'status': 'rejected',
|
|
'betrieb': False,
|
|
'fassung': None,
|
|
'ausloeser': None,
|
|
'begruendung': grund,
|
|
'stand_roh': '❌',
|
|
})
|
|
|
|
return eintraege
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Uebertragen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class Fehler(Exception):
|
|
pass
|
|
|
|
|
|
def ruf(basis, token, methode, pfad, query=None, body=None, versuche=6):
|
|
"""
|
|
Ein Aufruf, mit Geduld bei der Drosselung.
|
|
|
|
/api/bugtracker/v1/report laesst 60 Meldungen je Minute zu. Eine Roadmap
|
|
mit einundsechzig Eintraegen liegt genau darueber - der erste Probelauf
|
|
brach beim letzten Eintrag ab. Andere Projekte werden groessere Dateien
|
|
haben.
|
|
|
|
Behandelt wird das so, wie es die Agentenanleitung von jedem Client
|
|
verlangt: warten und das Intervall verdoppeln, nicht sofort wiederholen.
|
|
Abbrechen und den Menschen bitten, es spaeter noch einmal zu versuchen,
|
|
waere hier besonders unfreundlich - der Lauf ist zwar wiederholbar, aber
|
|
er dauert.
|
|
"""
|
|
url = basis + pfad
|
|
if query:
|
|
url += ('&' if '?' in pfad else '?') + urllib.parse.urlencode(
|
|
{k: v for k, v in query.items() if v is not None})
|
|
|
|
daten = json.dumps(body).encode('utf-8') if body is not None else None
|
|
warte = 5
|
|
|
|
for versuch in range(1, versuche + 1):
|
|
req = urllib.request.Request(url, data=daten, method=methode)
|
|
req.add_header('Authorization', 'Bearer ' + token)
|
|
req.add_header('Content-Type', 'application/json')
|
|
req.add_header('Accept', 'application/json')
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
return json.loads(r.read().decode('utf-8') or '{}')
|
|
except urllib.error.HTTPError as e:
|
|
roh = e.read().decode('utf-8', 'replace')
|
|
try:
|
|
d = json.loads(roh)
|
|
code = d.get('error', {}).get('code', str(e.code))
|
|
text = d.get('error', {}).get('message', roh)[:200]
|
|
except ValueError:
|
|
code, text = 'http_%d' % e.code, roh[:200]
|
|
|
|
if code == 'rate_limited' and versuch < versuche:
|
|
print(' gedrosselt - %d s warten (Versuch %d von %d)'
|
|
% (warte, versuch, versuche))
|
|
time.sleep(warte)
|
|
warte = min(warte * 2, 90)
|
|
continue
|
|
|
|
raise Fehler('%s: %s' % (code, text))
|
|
|
|
raise Fehler('rate_limited: auch nach %d Versuchen gedrosselt.' % versuche)
|
|
|
|
|
|
def bereiche_anlegen(basis, token, eintraege, trocken=False):
|
|
"""
|
|
Sorgt dafuer, dass jeder benutzte Themenbereich auch angelegt ist.
|
|
|
|
Ohne diesen Schritt entstehen Waisen: Items mit einem Bereich, den
|
|
roadmap_epics nicht kennt. Die Uebersicht zeigt sie dann als Warnung an,
|
|
weil ein Tippfehler sonst lautlos einen weiteren Bereich erzeugt - aber
|
|
hier ist es kein Tippfehler, sondern ein fehlender Schritt.
|
|
|
|
Beim Deploymentcenter faellt das nur bei H auf, weil Migration 016 die
|
|
uebrigen Bereiche mitbringt. Fuer jedes andere Projekt fehlen sie alle.
|
|
ON DUPLICATE KEY im Endpunkt macht den Aufruf wiederholbar.
|
|
"""
|
|
benutzt = []
|
|
for e in eintraege:
|
|
if e['epic'] not in benutzt:
|
|
benutzt.append(e['epic'])
|
|
|
|
# Nur anlegen, was fehlt.
|
|
#
|
|
# Der Endpunkt schreibt mit ON DUPLICATE KEY, setzt also auch Titel,
|
|
# Kurzbeschreibung und Sortierung neu. Ein Bereich, den jemand von Hand
|
|
# nachgeschaerft hat - oder den Migration 016 mit einer Beschreibung
|
|
# mitbrachte - verloere sie beim zweiten Lauf wieder. Ein Uebernahmeskript
|
|
# darf anlegen; kuratierte Angaben ueberschreiben darf es nicht.
|
|
vorhanden = set()
|
|
if not trocken:
|
|
antwort = ruf(basis, token, 'GET', '/api/bugtracker/v1/roadmap.php',
|
|
query={'action': 'epics', 'project_slug': SLUG})
|
|
vorhanden = {e['epic'] for e in antwort.get('epics', [])}
|
|
|
|
hoechste = 0
|
|
for i, kuerzel in enumerate(benutzt):
|
|
if kuerzel in vorhanden:
|
|
print(' Bereich [%s] besteht bereits - unveraendert' % kuerzel)
|
|
continue
|
|
|
|
titel = BEREICHE.get(kuerzel, 'Bereich %s' % kuerzel)
|
|
if trocken:
|
|
print(' wuerde Bereich anlegen: [%s] %s' % (kuerzel, titel))
|
|
continue
|
|
|
|
hoechste += 1
|
|
ruf(basis, token, 'POST', '/api/bugtracker/v1/roadmap.php',
|
|
query={'action': 'epic'},
|
|
body={'project_slug': SLUG, 'epic': kuerzel, 'title': titel,
|
|
'sort_order': (len(vorhanden) + hoechste) * 10})
|
|
print(' Bereich [%s] %s - neu angelegt' % (kuerzel, titel))
|
|
|
|
|
|
def uebertragen(basis, token, eintraege, trocken=False):
|
|
angelegt = aktualisiert = 0
|
|
rang = {}
|
|
|
|
bereiche_anlegen(basis, token, eintraege, trocken)
|
|
print()
|
|
|
|
# Prioritaet aus dem Abschnitt "Als Naechstes" der Datei.
|
|
#
|
|
# Der Rang dient zweierlei: innerhalb eines Bereichs ordnet er, ueber alle
|
|
# Bereiche hinweg priorisiert er (nextUp sortiert global danach). Wer
|
|
# ausdruecklich benannt ist, bekommt deshalb einen einstelligen Rang und
|
|
# steht damit vor allem, was nur in Dateireihenfolge eingereiht wurde.
|
|
prioritaet = lies_reihenfolge(ROADMAP.read_text(encoding='utf-8'))
|
|
if prioritaet:
|
|
print('Prioritaet aus "Als Naechstes": %s' % ', '.join(prioritaet))
|
|
print()
|
|
|
|
for e in eintraege:
|
|
rang[e['epic']] = rang.get(e['epic'], 0) + 10
|
|
client_ref = 'roadmap-%s-%s' % (SLUG, e['kennung'].lower())
|
|
|
|
beschreibung = (
|
|
'Uebernommen aus %s, vormals Kennung %s.\n\n'
|
|
'Die Begruendung steht weiterhin in der Datei - hier steht nur der Zustand.'
|
|
% (ANKER_DATEI, e['kennung'])
|
|
)
|
|
if e.get('begruendung'):
|
|
beschreibung = 'Verworfen: %s\n\n%s' % (e['begruendung'], beschreibung)
|
|
if e['betrieb']:
|
|
beschreibung = 'Betriebsaufgabe - kein Code.\n\n' + beschreibung
|
|
|
|
if trocken:
|
|
print(' wuerde anlegen: [%s] %-6s %s' % (e['epic'], e['kennung'], e['titel'][:60]))
|
|
continue
|
|
|
|
# 1. Item anlegen bzw. wiederfinden. client_ref macht das idempotent.
|
|
antwort = ruf(basis, token, 'POST', '/api/bugtracker/v1/report', body={
|
|
'project_slug': SLUG,
|
|
'type': 'feature_request',
|
|
'title': e['titel'][:250],
|
|
'description': beschreibung,
|
|
'severity': 'medium',
|
|
'environment': 'development',
|
|
'client_ref': client_ref,
|
|
})
|
|
item_id = antwort.get('item_id') or antwort.get('id')
|
|
if not item_id:
|
|
raise Fehler('Keine Item-ID in der Antwort: %s' % json.dumps(antwort)[:200])
|
|
|
|
neu = bool(antwort.get('is_new', True))
|
|
angelegt += 1 if neu else 0
|
|
aktualisiert += 0 if neu else 1
|
|
|
|
# 2. Einplanen. Status und Ausloeser muessen zusammen ankommen -
|
|
# getrennt gesetzt lehnt der Server das Zurueckstellen ab.
|
|
# Ausdruecklich priorisierte Eintraege nach vorn - aber nur, solange
|
|
# sie ueberhaupt noch offen sind. Ein erledigtes Vorhaben auf Rang 1
|
|
# stuende dauerhaft an der Spitze einer Liste, die "was ist als
|
|
# Naechstes dran" beantworten soll.
|
|
if e['kennung'] in prioritaet and e['status'] in ('open', 'planned', 'in_progress'):
|
|
eigener_rang = prioritaet.index(e['kennung']) + 1
|
|
else:
|
|
eigener_rang = rang[e['epic']]
|
|
|
|
felder = {
|
|
'project_slug': SLUG,
|
|
'epic': e['epic'],
|
|
'roadmap_rank': eigener_rang,
|
|
'doc_anchor': anker(e['kennung'], e['titel']),
|
|
'status': e['status'],
|
|
}
|
|
if e['status'] == 'deferred':
|
|
if not e['ausloeser']:
|
|
raise Fehler(
|
|
'%s ist zurueckgestellt, aber in der Datei steht kein '
|
|
'"**Ausloeser:**" - nach der eigenen Regel ist das kein '
|
|
'zurueckgestelltes, sondern ein vergessenes Vorhaben.' % e['kennung'])
|
|
felder['defer_trigger'] = e['ausloeser']
|
|
if e['fassung']:
|
|
felder['target_build'] = e['fassung']
|
|
|
|
ruf(basis, token, 'POST', '/api/bugtracker/v1/roadmap.php',
|
|
query={'action': 'set', 'id': item_id}, body=felder)
|
|
|
|
# 3. Erledigte sauber abschliessen, damit resolved_in_build stimmt.
|
|
if e['status'] == 'resolved' and e['fassung']:
|
|
ruf(basis, token, 'POST', '/api/bugtracker/v1/manage',
|
|
query={'action': 'resolve', 'id': item_id},
|
|
body={'resolved_in_build': e['fassung'],
|
|
'resolution_notes': 'Bei der Roadmap-Uebernahme als erledigt uebertragen.'})
|
|
|
|
print(' %s #%-5s [%s] %-10s %s' % (
|
|
'neu ' if neu else 'akt.', item_id, e['epic'], e['status'], e['titel'][:52]))
|
|
|
|
return angelegt, aktualisiert
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description='ROADMAP.md ins Deploymentcenter uebernehmen')
|
|
p.add_argument('--lesen', action='store_true', help='nur auswerten, nichts senden')
|
|
p.add_argument('--probe', action='store_true', help='gegen die Testinstanz')
|
|
p.add_argument('--uebernehmen', action='store_true', help='gegen die Produktion')
|
|
p.add_argument('--trocken', action='store_true', help='zeigen, was gesendet wuerde')
|
|
args = p.parse_args()
|
|
|
|
eintraege = auswerten()
|
|
|
|
zaehler = {}
|
|
for e in eintraege:
|
|
zaehler[e['status']] = zaehler.get(e['status'], 0) + 1
|
|
|
|
print('%d Eintraege aus %s' % (len(eintraege), ANKER_DATEI))
|
|
print(' ' + ', '.join('%s: %d' % kv for kv in sorted(zaehler.items())))
|
|
print()
|
|
|
|
if args.lesen:
|
|
bereich = None
|
|
for e in eintraege:
|
|
if e['epic'] != bereich:
|
|
bereich = e['epic']
|
|
print('[%s] %s' % (bereich, BEREICHE.get(bereich, '?')))
|
|
zusatz = ''
|
|
if e['fassung']:
|
|
zusatz += ' -> %s' % e['fassung']
|
|
if e['ausloeser']:
|
|
zusatz += ' Ausloeser: %s' % e['ausloeser'][:50]
|
|
print(' %-6s %-10s %-52s%s' % (e['kennung'], e['status'], e['titel'][:52], zusatz))
|
|
return 0
|
|
|
|
if args.probe:
|
|
sys.path.insert(0, str(PROJEKT_DIR / 'tests' / 'e2e'))
|
|
import harness
|
|
cfg = harness.lade_config(None)
|
|
client = harness.Client(cfg['base_url'],
|
|
cfg['basic_auth']['user'], cfg['basic_auth']['pass'])
|
|
r = client.post('/testctl.php',
|
|
body={'action': 'workbench_token', 'project_slug': SLUG,
|
|
'name': 'roadmap-import'},
|
|
token=cfg['keys']['shared_key'])
|
|
if not r.ok:
|
|
print('Werkbank-Token nicht erhalten: %s' % r.kurz(300))
|
|
return 2
|
|
basis, token = cfg['base_url'], r.feld('raw_token')
|
|
print('Testinstanz %s, Token frisch ausgestellt.' % basis)
|
|
|
|
elif args.uebernehmen:
|
|
token = os.environ.get('DC_TOKEN', '')
|
|
if not token:
|
|
print('DC_TOKEN fehlt. Erwartet wird ein Werkbank-Token des Projekts')
|
|
print('"%s" (owner_type = dev_agent, Scope bugtracker:manage).' % SLUG)
|
|
return 2
|
|
basis = os.environ.get('DC_BASE_URL', 'https://dc.mhdf.de')
|
|
print('Produktion %s' % basis)
|
|
|
|
else:
|
|
p.print_help()
|
|
return 2
|
|
|
|
try:
|
|
neu, alt = uebertragen(basis, token, eintraege, trocken=args.trocken)
|
|
except Fehler as e:
|
|
print('\nAbbruch: %s' % e)
|
|
return 1
|
|
|
|
if not args.trocken:
|
|
print('\n%d angelegt, %d aktualisiert.' % (neu, alt))
|
|
print('Naechster Schritt: php cli/roadmap.php --write --project=%s' % SLUG)
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|