Files
Deploymentcenter/public/testkit/dc-testkit.sh
T
Deploymentcenter BotandClaude Opus 5 1d5ac92051 feat(2.7.0): Roadmap im DC, Testinstanz mit Testtor, Supervisor und MCP
Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.

ROADMAP IM DEPLOYMENTCENTER (2.7.0)

Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.

Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.

  - Migration 016: epic, roadmap_rank, target_build, defer_trigger,
    doc_anchor, Status "deferred", Tabelle roadmap_epics.
    KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
  - Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
    epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
  - Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
    schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
    die Kennung nach einer Loeschung neu vergeben wurde.
  - Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
    Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
    Repository, damit sie auf allen drei Schreibwegen gleich greift.
  - Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
    WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
    dc_roadmap und dc_roadmap_set im MCP-Adapter.
  - cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
    --check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
  - docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.

TESTINSTANZ UND TESTTOR

  - dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
    Basic-Auth davor, Rocket.Chat aus.
  - 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
  - scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
    die Testinstanz und laesst dann den Testlauf laufen. Erst bei
    Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
    Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
  - tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
    nicht versehentlich auf dc.mhdf.de landen.
  - Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
    Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
    nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.

SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)

  - Migration 013: projektuebergreifend melden (dev_agent, evidence,
    triage_external, affects).
  - Migration 014: supervisor_findings, Katalog, fuenf serverseitige
    Detektoren im Tick.
  - Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
  - DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
  - MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
    von Fremddaten gegen Prompt-Injektion.

BEHOBEN

  - observe und status im Supervisor legten den Zustand des Befundes als
    "status" in die Antwort. Http::ok() merged ['status' => 'success']
    darunter, also gewann der Befundzustand - eine angenommene Meldung kam
    als status="open" an. Heisst jetzt finding_status. War Item #59.

Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 21:59:20 +02:00

1216 lines
42 KiB
Bash

#!/usr/bin/env bash
#
# dc-testkit - Werkzeugkasten fuer Agenten auf der Linux-Testumgebung
#
# Zweck: Ein Agent, der auf der geteilten Test-VM arbeitet, soll ohne Rueckfrage
# an einen Menschen einen Testlauf durchfuehren koennen - Lizenz besorgen, Paket
# installieren, Update einspielen, Ergebnis melden. Bis hierher brauchte er fuer
# jeden dieser Schritte jemanden, der im WebUI etwas anklickt.
#
# Warum ein Skript und nicht "der Agent ruft die API direkt auf": Weil sonst
# jeder Agent seine eigene Fassung der immer gleichen fuenf curl-Aufrufe baut,
# jede mit anderen Vorgaben, anderer Fehlerbehandlung und anderem Umgang mit
# dem Token. Die Unterschiede faellt niemandem auf, solange nichts schiefgeht -
# und wenn etwas schiefgeht, ist unklar, ob die Anwendung oder der Aufruf schuld
# war. Ein Weg, den alle nehmen, ist der einzige, dessen Fehler man findet.
#
# Abhaengigkeiten: curl und python3. Beides ist auf der VM vorhanden; jq wird
# bewusst NICHT vorausgesetzt, weil es dort nicht installiert ist und eine
# Installation nur fuer das Lesen von JSON die Maschine fuer alle veraendert.
#
# Vollstaendige Anleitung: docs/TESTUMGEBUNG.md im Deploymentcenter-Repo.
set -euo pipefail
DC_TESTKIT_VERSION="1.0.0"
# ---------------------------------------------------------------------------
# Konfiguration
#
# Reihenfolge: Schalter auf der Kommandozeile schlaegt Umgebungsvariable
# schlaegt Konfigurationsdatei schlaegt Vorgabe. Die Datei ist der Ort fuer
# alles, was auf dieser Maschine dauerhaft gilt - insbesondere das Token, das
# damit nicht in der Shell-History landet.
# ---------------------------------------------------------------------------
CONFIG_FILE="${DC_TESTKIT_CONFIG:-$HOME/.config/dc-testkit/config}"
if [ -f "$CONFIG_FILE" ]; then
# shellcheck disable=SC1090
. "$CONFIG_FILE"
fi
DC_URL="${DC_URL:-https://dc.mhdf.de}"
DC_URL="${DC_URL%/}"
DC_PROJECT="${DC_PROJECT:-}"
DC_TOKEN="${DC_TOKEN:-}"
DC_CHANNEL="${DC_CHANNEL:-prod}"
# Die Maschinenkennung entscheidet, welchen DEV-Schluessel der Server
# heraussucht. Sie muss ueber Laeufe hinweg gleich bleiben - sonst sammelt sich
# je Lauf ein weiterer Schluessel an, bis die Grenze je Projekt greift.
DC_INSTANCE="${DC_INSTANCE:-$(hostname -s 2>/dev/null || hostname)}"
STATE_ROOT="${DC_TESTKIT_STATE:-$HOME/.local/state/dc-testkit}"
# ---------------------------------------------------------------------------
# Ausgabe
# ---------------------------------------------------------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_RESET=$'\033[0m'; C_DIM=$'\033[2m'; C_RED=$'\033[31m'
C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_BLUE=$'\033[34m'
else
C_RESET=''; C_DIM=''; C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''
fi
note() { printf '%s\n' "$*"; }
step() { printf '%s==>%s %s\n' "$C_BLUE" "$C_RESET" "$*"; }
ok() { printf '%s ok%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf '%swarn%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
dim() { printf '%s%s%s\n' "$C_DIM" "$*" "$C_RESET"; }
die() {
printf '%sFehler%s %s\n' "$C_RED" "$C_RESET" "$*" >&2
exit 1
}
# ---------------------------------------------------------------------------
# JSON
#
# python3 statt jq: siehe Kopf. Der Pfad wird mit Punkten angegeben,
# Listenindizes in eckigen Klammern - json_get '.latest_release.version'.
# Fehlt der Wert, kommt eine leere Zeile und Rueckgabewert 1; das laesst sich
# mit || unterscheiden von "Wert ist leer".
# ---------------------------------------------------------------------------
json_get() {
local path="$1"
python3 -c '
import json, re, sys
path = sys.argv[1].lstrip(".")
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(2)
cur = data
if path:
for part in re.findall(r"[^.\[\]]+", path):
try:
cur = cur[int(part)] if part.isdigit() and isinstance(cur, list) else cur[part]
except (KeyError, IndexError, TypeError):
sys.exit(1)
if cur is None:
sys.exit(1)
if isinstance(cur, bool):
print("true" if cur else "false")
elif isinstance(cur, (dict, list)):
print(json.dumps(cur, ensure_ascii=False))
else:
print(cur)
' "$path"
}
# Baut ein JSON-Objekt aus Schluessel-Wert-Paaren. Ohne das muesste jeder
# Aufruf sein JSON von Hand zusammensetzen - und spaetestens bei einem
# Stacktrace mit Anfuehrungszeichen und Zeilenumbruechen geht das schief.
json_object() {
python3 -c '
import json, sys
args = sys.argv[1:]
out = {}
for i in range(0, len(args) - 1, 2):
key, value = args[i], args[i + 1]
if value == "":
continue
if value in ("true", "false"):
out[key] = value == "true"
elif key in ("line_no", "limit", "ttl_days") and value.lstrip("-").isdigit():
out[key] = int(value)
else:
out[key] = value
print(json.dumps(out, ensure_ascii=False))
' "$@"
}
# ---------------------------------------------------------------------------
# HTTP
#
# Setzt API_STATUS und API_BODY. Bricht NICHT von selbst ab - der Aufrufer
# entscheidet, ob ein 404 ein Fehler ist. Bei 429 wird gewartet und wiederholt,
# mit verdoppeltem Abstand; das ist die im Handbuch verlangte Reaktion und
# gehoert an genau eine Stelle statt in jeden Aufruf.
# ---------------------------------------------------------------------------
API_STATUS=0
API_BODY=''
api() {
local method="$1" path="$2" payload="${3:-}"
local url="$DC_URL$path"
local attempt=0 delay=2 raw
while :; do
local -a args=(--silent --show-error --location --max-time 60
-w '\n%{http_code}' -X "$method"
-H 'Accept: application/json'
-H "User-Agent: dc-testkit/$DC_TESTKIT_VERSION")
[ -n "$DC_TOKEN" ] && args+=(-H "Authorization: Bearer $DC_TOKEN")
if [ -n "$payload" ]; then
args+=(-H 'Content-Type: application/json' --data-binary "$payload")
fi
if ! raw=$(curl "${args[@]}" "$url" 2>&1); then
API_STATUS=0
API_BODY="$raw"
return 1
fi
API_STATUS="${raw##*$'\n'}"
API_BODY="${raw%$'\n'*}"
# 429 und 5xx sind keine Aussage ueber die Sache, sondern ueber den
# Zeitpunkt. Dreimal wiederholen, dann aufgeben.
if { [ "$API_STATUS" = "429" ] || [ "$API_STATUS" -ge 500 ] 2>/dev/null; } && [ "$attempt" -lt 3 ]; then
attempt=$((attempt + 1))
warn "HTTP $API_STATUS von $path - erneut in ${delay}s (Versuch $attempt/3)"
sleep "$delay"
delay=$((delay * 2))
continue
fi
break
done
[ "$API_STATUS" -ge 200 ] 2>/dev/null && [ "$API_STATUS" -lt 300 ]
}
# Fehlermeldung aus der Antwort ziehen. Der code ist stabil und fuer Programme
# gedacht, die message fuer Menschen - beide ausgeben, damit ein Agent das eine
# auswerten und das andere zitieren kann.
api_error() {
local code msg
code=$(printf '%s' "$API_BODY" | json_get '.error.code' 2>/dev/null || true)
msg=$(printf '%s' "$API_BODY" | json_get '.error.message' 2>/dev/null || true)
if [ -n "$code" ]; then
printf 'HTTP %s [%s] %s' "$API_STATUS" "$code" "$msg"
else
printf 'HTTP %s %s' "$API_STATUS" "$(printf '%s' "$API_BODY" | head -c 400)"
fi
}
# ---------------------------------------------------------------------------
# Zustand je Projekt
# ---------------------------------------------------------------------------
require_project() {
[ -n "$DC_PROJECT" ] || die "Kein Projekt gesetzt. DC_PROJECT=<slug> oder --project <slug>."
}
require_token() {
[ -n "$DC_TOKEN" ] || die "Kein Token gesetzt. DC_TOKEN in $CONFIG_FILE hinterlegen."
}
state_dir() {
require_project
printf '%s/%s' "$STATE_ROOT" "$DC_PROJECT"
}
sandbox_dir() {
printf '%s/sandbox' "$(state_dir)"
}
agent_bin() {
printf '%s/update-agent' "$(state_dir)"
}
key_file() {
printf '%s/license.key' "$(state_dir)"
}
ensure_state() {
local dir
dir="$(state_dir)"
mkdir -p "$dir"
chmod 700 "$dir" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# doctor - laeuft hier ueberhaupt etwas?
#
# Vor dem ersten Testlauf, und als Erstes, wenn etwas unerklaerlich scheitert.
# Prueft von unten nach oben: Werkzeuge, Netz, Token, Projekt.
# ---------------------------------------------------------------------------
cmd_doctor() {
local failed=0
step "Werkzeuge"
for tool in curl python3; do
if command -v "$tool" >/dev/null 2>&1; then
ok "$tool $(command -v "$tool")"
else
warn "$tool fehlt - ohne das geht nichts"
failed=1
fi
done
step "Deploymentcenter unter $DC_URL"
if api GET /api/health; then
local healthy
healthy=$(printf '%s' "$API_BODY" | json_get '.healthy' || echo '?')
ok "erreichbar, healthy=$healthy, Fassung $(printf '%s' "$API_BODY" | json_get '.version' || echo '?')"
else
warn "nicht erreichbar: $(api_error)"
failed=1
fi
step "Token"
if [ -z "$DC_TOKEN" ]; then
warn "kein Token gesetzt (DC_TOKEN)"
failed=1
elif api GET /api/bugtracker/v1/projects; then
ok "gueltig - der Bugtracker antwortet"
else
warn "Token wird nicht angenommen: $(api_error)"
failed=1
fi
step "Projekt"
if [ -z "$DC_PROJECT" ]; then
warn "kein Projekt gesetzt (DC_PROJECT)"
failed=1
elif [ -z "$DC_TOKEN" ]; then
# Ohne Token ist die Projektliste nicht abrufbar. Das stillschweigend
# zu ueberspringen liesse den Eindruck entstehen, das Projekt sei
# geprueft worden.
warn "$DC_PROJECT nicht pruefbar - dafuer wird ein Token gebraucht"
elif api GET /api/bugtracker/v1/projects; then
if printf '%s' "$API_BODY" | grep -q "\"$DC_PROJECT\""; then
ok "$DC_PROJECT ist im Deploymentcenter bekannt"
else
warn "$DC_PROJECT kommt in der Projektliste nicht vor"
failed=1
fi
else
warn "Projektliste nicht abrufbar: $(api_error)"
failed=1
fi
step "Arbeitsplatz"
if [ -n "$DC_PROJECT" ]; then
dim " Zustand: $(state_dir)"
dim " Sandkasten: $(sandbox_dir)"
dim " Kennung: $DC_INSTANCE"
fi
dim " Platz: $(df -Ph "$HOME" | awk 'NR==2 {print $4 " frei"}')"
if [ "$failed" -eq 0 ]; then
printf '\n%sBereit.%s\n' "$C_GREEN" "$C_RESET"
else
printf '\n%sNicht bereit - siehe oben.%s\n' "$C_YELLOW" "$C_RESET"
return 1
fi
}
# ---------------------------------------------------------------------------
# license - DEV-Schluessel holen, verlaengern, zurueckgeben
#
# Der Aufruf ist absichtlich wiederholbar. Er darf am Anfang jedes Laufs
# stehen: Gibt es fuer diese Maschine schon einen Schluessel, kommt derselbe
# zurueck und seine Frist wird verlaengert.
#
# Der Schluessel wird in eine Datei mit Rechten 600 geschrieben und NICHT
# ausgegeben. Ausgabe nur auf ausdrueckliches --print - sonst steht er im
# Terminalprotokoll, im Sitzungsverlauf des Agenten und am Ende in einem
# Bugreport.
# ---------------------------------------------------------------------------
cmd_license() {
require_project; require_token; ensure_state
local ttl='' mode='issue' print=0
while [ $# -gt 0 ]; do
case "$1" in
--ttl) ttl="$2"; shift 2 ;;
--revoke) mode='revoke'; shift ;;
--print) print=1; shift ;;
*) die "Unbekannter Schalter fuer license: $1" ;;
esac
done
local payload
payload=$(json_object \
project_slug "$DC_PROJECT" \
instance_id "$DC_INSTANCE" \
ttl_days "$ttl" \
action_mode "$mode")
if ! api POST /api/license/v1/dev-key "$payload"; then
die "DEV-Schluessel nicht erhalten: $(api_error)"
fi
# Ein Deploymentcenter vor 2.6.1 kennt die Aktion nicht. Es antwortet dann
# aber NICHT mit 404: Die Lizenz-API faellt bei unbekannten Pfadsegmenten
# auf 'status' zurueck und liefert HTTP 200 mit {"status":"ok","module":
# "license"}. Ohne diese Pruefung sucht der erste Agent den Fehler bei sich.
if [ "$(printf '%s' "$API_BODY" | json_get '.module' 2>/dev/null || true)" = 'license' ]; then
die "Dieses Deploymentcenter kennt /api/license/v1/dev-key noch nicht.
Der Server antwortet mit der Verfuegbarkeitspruefung des Lizenzmoduls statt mit
einem Schluessel - die Fassung ist aelter als 2.6.1. Es muss deployt werden."
fi
if [ "$mode" = 'revoke' ]; then
rm -f "$(key_file)"
ok "$(printf '%s' "$API_BODY" | json_get '.message')"
return 0
fi
local key expires
key=$(printf '%s' "$API_BODY" | json_get '.license_key') \
|| die "Antwort enthaelt keinen Schluessel: $API_BODY"
expires=$(printf '%s' "$API_BODY" | json_get '.expires_at' || echo '?')
local file
file="$(key_file)"
( umask 077; printf '%s' "$key" > "$file" )
ok "$(printf '%s' "$API_BODY" | json_get '.message')"
dim " gilt bis: $expires UTC"
dim " Aktivierungen: $(printf '%s' "$API_BODY" | json_get '.activations_used')/$(printf '%s' "$API_BODY" | json_get '.max_activations')"
dim " abgelegt in: $file"
[ "$print" -eq 1 ] && printf '%s\n' "$key"
return 0
}
load_key() {
local file
file="$(key_file)"
[ -f "$file" ] || die "Kein DEV-Schluessel vorhanden. Zuerst: dc-testkit license"
cat "$file"
}
# ---------------------------------------------------------------------------
# agent - den update-agent besorgen
#
# Mit Pruefsummenabgleich gegen installer.json. Das ist nicht Zierde: Ein
# Testlauf, der ein halb heruntergeladenes Binary startet, meldet Fehler, die
# es in der Anwendung nicht gibt.
# ---------------------------------------------------------------------------
cmd_agent() {
require_project; ensure_state
local force=0
[ "${1:-}" = '--force' ] && force=1
local bin
bin="$(agent_bin)"
if [ -x "$bin" ] && [ "$force" -eq 0 ]; then
ok "update-agent liegt bereits vor ($bin)"
return 0
fi
local arch platform
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) platform='linux-x64' ;;
aarch64|arm64) platform='linux-arm64' ;;
*) die "Fuer die Architektur $arch gibt es kein update-agent-Binary." ;;
esac
step "update-agent fuer $platform holen"
local manifest
manifest=$(curl -fsSL --max-time 60 "$DC_URL/installer/installer.json") \
|| die "installer.json nicht abrufbar."
local expected file idx=0 found=''
while :; do
local p
p=$(printf '%s' "$manifest" | json_get ".binaries[$idx].platform") || break
if [ "$p" = "$platform" ]; then
found="$idx"
break
fi
idx=$((idx + 1))
done
[ -n "$found" ] || die "installer.json kennt kein Binary fuer $platform."
file=$(printf '%s' "$manifest" | json_get ".binaries[$found].file")
expected=$(printf '%s' "$manifest" | json_get ".binaries[$found].sha256")
curl -fSL --max-time 600 -o "$bin.part" "$DC_URL/installer/$file" \
|| die "Download von $file fehlgeschlagen."
local actual
actual=$(sha256sum "$bin.part" | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
rm -f "$bin.part"
die "Pruefsumme weicht ab. Erwartet $expected, bekommen $actual."
fi
mv "$bin.part" "$bin"
chmod +x "$bin"
ok "update-agent $(printf '%s' "$manifest" | json_get '.version') abgelegt, Pruefsumme stimmt"
}
# Der Schluessel geht ueber die Umgebung, nicht ueber --license-key.
#
# Die Maschine ist geteilt. Was in der Befehlszeile steht, sieht jeder andere
# Benutzer in "ps aux" - ein Lizenzschluessel gehoert da nicht hin, auch kein
# befristeter. Die Umgebung eines Prozesses liest dagegen nur, wer ihm gehoert
# (oder root). Der update-agent nimmt DC_LICENSE_KEY seit jeher entgegen und
# bevorzugt es sogar gegenueber dem Schalter.
run_agent() {
local bin
bin="$(agent_bin)"
[ -x "$bin" ] || die "update-agent fehlt. Zuerst: dc-testkit agent"
DC_LICENSE_KEY="$(load_key)" "$bin" \
--base-url "$DC_URL" \
--project "$DC_PROJECT" \
--channel "$DC_CHANNEL" \
"$@"
}
# ---------------------------------------------------------------------------
# check / install / update / repair - die Auslieferungswege
#
# ZUM VERSTAENDNIS DES INSTALL-BEFEHLS: "--action install" des update-agent ist
# ein Dialog mit Auswahllisten und Passwortabfrage. Er laesst sich nicht
# kopflos treiben, und ein Agent darf ohnehin kein Passwort eingeben. Was hier
# als "install" laeuft, ist deshalb die Erstauslieferung in ein LEERES
# Verzeichnis ueber den Update-Weg.
#
# Das ist keine Notloesung: Der Installer holt die Dateien laut
# SETUP_INTEGRATION_GUIDE ueber genau denselben Pfad - Download, SHA256,
# Signaturpruefung, Staging, Rollback. Was der Dialog zusaetzlich tut, ist
# fragen und eine Konfiguration schreiben. Geprueft wird hier also die
# Auslieferung, nicht die Gespraechsfuehrung.
# ---------------------------------------------------------------------------
cmd_check() {
require_project
step "Auf Updates pruefen ($DC_PROJECT, Kanal $DC_CHANNEL)"
run_agent --action check --target-dir "$(sandbox_dir)" "$@"
}
cmd_install() {
require_project; ensure_state
local dir
dir="$(sandbox_dir)"
if [ -d "$dir" ] && [ -n "$(ls -A "$dir" 2>/dev/null)" ]; then
die "Der Sandkasten $dir ist nicht leer. Erst 'dc-testkit cleanup --sandbox', sonst pruefst du ein Update statt einer Erstinstallation."
fi
mkdir -p "$dir"
step "Erstauslieferung nach $dir"
run_agent --action update --version latest --target-dir "$dir" "$@"
}
cmd_update() {
require_project
local version='latest'
if [ "${1:-}" = '--version' ]; then
version="$2"; shift 2
fi
step "Update auf $version"
run_agent --action update --version "$version" --target-dir "$(sandbox_dir)" "$@"
}
cmd_repair() {
require_project
step "Integritaetsreparatur"
run_agent --action repair --target-dir "$(sandbox_dir)" "$@"
}
cmd_list() {
require_project
run_agent --action list "$@"
}
# ---------------------------------------------------------------------------
# verify - was liegt jetzt eigentlich da?
#
# Ein "Update erfolgreich" auf der Konsole ist keine Aussage darueber, was im
# Zielverzeichnis steht. Diese Pruefung liest das Manifest und vergleicht es
# mit dem, was der Server als neueste Fassung fuehrt.
# ---------------------------------------------------------------------------
cmd_verify() {
require_project
local dir manifest
dir="$(sandbox_dir)"
manifest="$dir/manifest.json"
step "Installation in $dir pruefen"
[ -d "$dir" ] || die "$dir gibt es nicht - es wurde nichts installiert."
local files
files=$(find "$dir" -type f | wc -l)
dim " Dateien: $files"
[ "$files" -gt 0 ] || die "Das Zielverzeichnis ist leer."
if [ ! -f "$manifest" ]; then
warn "Keine manifest.json im Zielverzeichnis."
warn "Ohne sie meldet der Agent beim naechsten Lauf 'Unbekannt' als installierte Fassung."
return 1
fi
local installed
installed=$(json_get '.version' < "$manifest" || echo '')
[ -n "$installed" ] || die "manifest.json enthaelt keine Version."
ok "installiert: $installed"
local platform
case "$(uname -m)" in
x86_64|amd64) platform='linux-x64' ;;
aarch64|arm64) platform='linux-arm64' ;;
*) platform='' ;;
esac
if api GET "/api/updateservice/v1/latest?product=$DC_PROJECT&channel=$DC_CHANNEL&platform=$platform"; then
local latest
latest=$(printf '%s' "$API_BODY" | json_get '.latest_release.version' || echo '')
if [ -z "$latest" ]; then
warn "Der Server nennt keine neueste Fassung fuer Kanal $DC_CHANNEL/$platform."
elif [ "$latest" = "$installed" ]; then
ok "entspricht der neuesten Fassung auf dem Server"
else
warn "Server fuehrt $latest, installiert ist $installed"
return 1
fi
else
warn "Serverabgleich nicht moeglich: $(api_error)"
fi
return 0
}
# ---------------------------------------------------------------------------
# report - Fehler und Wuensche in den Bugtracker
#
# client_ref wird aus Projekt, Titel und Fundstelle abgeleitet, nicht
# gewuerfelt. Damit legt derselbe Befund aus zehn Testlaeufen ein Item an und
# nicht zehn - genau dafuer ist das Feld da. Wer bewusst ein zweites Item will,
# gibt --ref selbst an.
# ---------------------------------------------------------------------------
# Welche Fassung wird hier gerade geprueft?
#
# Das ist die wichtigste Einzelangabe einer Meldung von der Testumgebung, und
# sie von Hand mitzugeben vergisst jeder. Ohne sie kann der Server nicht
# entscheiden, ob ein bereits behobener Fehler eine Regression ist oder bloss
# ein veralteter Pruefstand - und legt im Zweifel ein Regressionsitem an.
#
# Quelle ist die manifest.json im Sandkasten, also genau die Fassung, gegen die
# der Lauf tatsaechlich lief - nicht die, die jemand fuer aktuell haelt.
detect_build() {
local manifest
manifest="$(sandbox_dir)/manifest.json"
[ -f "$manifest" ] || return 1
json_get '.version' < "$manifest" 2>/dev/null || return 1
}
cmd_report() {
require_project; require_token
local type='bug' title='' description='' severity='medium' file='' line=''
local ref='' error_message='' stack='' build='' target_project='' evidence=''
while [ $# -gt 0 ]; do
case "$1" in
--type) type="$2"; shift 2 ;;
--title) title="$2"; shift 2 ;;
--desc) description="$2"; shift 2 ;;
--desc-file) description="$(cat "$2")"; shift 2 ;;
--severity) severity="$2"; shift 2 ;;
--file) file="$2"; shift 2 ;;
--line) line="$2"; shift 2 ;;
--error) error_message="$2"; shift 2 ;;
--stack-file) stack="$(cat "$2")"; shift 2 ;;
--build) build="$2"; shift 2 ;;
--project) target_project="$2"; shift 2 ;;
--evidence) evidence="$2"; shift 2 ;;
--ref) ref="$2"; shift 2 ;;
*) die "Unbekannter Schalter fuer report: $1" ;;
esac
done
[ -n "$title" ] || die "--title fehlt."
# Der Server kennt genau zwei Typen, und er ist dabei nicht waehlerisch,
# sondern still: alles, was nicht woertlich "feature_request" heisst, wird
# zu "bug". Ein "--type feature" landete damit als Fehler im Tracker, und
# niemand haette es gemerkt. Deshalb hier uebersetzen statt durchreichen -
# und alles Unbekannte ablehnen, statt es stillschweigend umzudeuten.
case "$type" in
bug)
;;
feature|feature_request|fr|wunsch|verbesserung)
type='feature_request' ;;
idea|idee)
die "\"idea\" ist kein Typ, sondern ein Schweregrad.
Gemeint ist vermutlich: --type feature --severity idea" ;;
*)
die "Unbekannter Typ \"$type\". Erlaubt sind: bug, feature" ;;
esac
case "$severity" in
idea|wishlist|low|medium|high|critical) ;;
*) die "Unbekannter Schweregrad \"$severity\".
Erlaubt sind: idea, wishlist, low, medium, high, critical" ;;
esac
# Beschreibung darf auch ueber die Standardeingabe kommen - Fehlertexte
# entstehen meist als Ausgabe eines anderen Befehls.
if [ -z "$description" ] && [ ! -t 0 ]; then
description="$(cat)"
fi
[ -n "$description" ] || die "Keine Beschreibung. --desc, --desc-file oder ueber die Standardeingabe."
target_project="${target_project:-$DC_PROJECT}"
# Fassung nachtragen, wenn sie nicht ausdruecklich angegeben wurde. Nur fuer
# das eigene Projekt: Bei einer Fremdmeldung ist die eigene Fassung nicht
# die des gemeldeten Projekts, und eine falsche Angabe waere schlimmer als
# keine - der Server verglicher sie sonst gegen dessen resolved_in_build.
if [ -z "$build" ] && [ "$target_project" = "$DC_PROJECT" ]; then
build="$(detect_build || true)"
[ -n "$build" ] && dim " geprüfte Fassung: $build (aus manifest.json)"
fi
# Meldung gegen ein FREMDES Projekt verlangt eine Begruendung von
# mindestens 40 Zeichen - der Server weist sie sonst mit 400
# missing_evidence ab. Der Grund steht in report.php: Ein Agent in Projekt X
# kann kaum unterscheiden, ob das fremde Projekt kaputt ist oder ob X es
# falsch benutzt, und die Voreinstellung eines hilfsbereiten Modells ist im
# Zweifel "melde es". Hier abzufangen statt den Server ablehnen zu lassen
# spart einen Fehlversuch und macht die Anforderung sichtbar.
if [ "$target_project" != "$DC_PROJECT" ]; then
if [ "${#evidence}" -lt 40 ]; then
die "Meldung gegen das fremde Projekt \"$target_project\" verlangt --evidence
mit mindestens 40 Zeichen: Was wurde beobachtet, und warum liegt die Ursache
dort und nicht in \"$DC_PROJECT\"?"
fi
# Der Server deckelt den Schweregrad einer Fremdmeldung auf medium und
# sagt es nicht. Wer "critical" schreibt und spaeter "medium" vorfindet,
# haelt das fuer einen Fehler. Also vorher sagen, dass es so gemeint ist:
# Die Dringlichkeit in einem fremden Projekt kann der Melder nicht
# beurteilen - dazu muesste er wissen, wer sonst noch betroffen ist.
case "$severity" in
high|critical)
warn "Schweregrad \"$severity\" wird bei einer Fremdmeldung auf \"medium\" gedeckelt."
warn "Die Einstufung nimmt \"$target_project\" selbst vor - schreib das Dringliche in die Begruendung."
;;
esac
elif [ -n "$evidence" ]; then
# Im eigenen Projekt verwirft der Server das Feld ohnehin.
evidence=''
fi
if [ -z "$ref" ]; then
ref="testkit-$(printf '%s|%s|%s|%s' "$target_project" "$title" "$file" "$line" \
| sha256sum | cut -c1-24)"
fi
# Der Testlauf gehoert in die Beschreibung: Ohne die Angabe, auf welcher
# Maschine und gegen welche Fassung geprueft wurde, ist ein Befund von der
# Testumgebung nicht von einem aus dem Feld zu unterscheiden.
description="$description
--
Gemeldet von dc-testkit $DC_TESTKIT_VERSION auf $DC_INSTANCE ($(uname -sr)), Kanal $DC_CHANNEL."
local payload
payload=$(json_object \
project_slug "$target_project" \
type "$type" \
title "$title" \
description "$description" \
error_message "$error_message" \
stack_trace "$stack" \
severity "$severity" \
environment 'development' \
build_version "$build" \
file_path "$file" \
line_no "$line" \
evidence "$evidence" \
client_ref "$ref")
if ! api POST /api/bugtracker/v1/report "$payload"; then
die "Meldung nicht angenommen: $(api_error)"
fi
# Feldnamen stammen aus public/api/bugtracker/v1/report.php - nicht raten,
# der Endpunkt heisst sie item_id/is_new und nicht id/duplicate.
local id is_new hits occurrences
id=$(printf '%s' "$API_BODY" | json_get '.item_id' || echo '?')
is_new=$(printf '%s' "$API_BODY" | json_get '.is_new' || echo 'false')
hits=$(printf '%s' "$API_BODY" | json_get '.idempotent_hit' || echo 'false')
occurrences=$(printf '%s' "$API_BODY" | json_get '.occurrence_count' || echo '')
# Bekannt und in einer spaeteren Fassung behoben. Das ist der haeufigste
# Fall auf einer Testumgebung, die nicht immer die neueste Fassung faehrt -
# und die einzige Antwort, bei der NICHTS angelegt wurde.
local fixed_in reported_build regression
fixed_in=$(printf '%s' "$API_BODY" | json_get '.known_fixed_in' || echo '')
reported_build=$(printf '%s' "$API_BODY" | json_get '.reported_build' || echo '')
regression=$(printf '%s' "$API_BODY" | json_get '.regression_of' || echo '')
if [ -n "$fixed_in" ]; then
ok "Nicht gemeldet - schon behoben."
dim " Item #$id, behoben in $fixed_in; geprüft wurde $reported_build."
dim " Es wurde nichts angelegt und nichts hochgezählt."
dim " Aktualisiere und prüfe erneut. Tritt es dann immer noch auf,"
dim " ist es eine Regression und gehört gemeldet."
return 0
fi
# Der Fix, der geprueft werden sollte, traegt nicht. Kein neues Item -
# Versuch und Fehlschlag bleiben auf einem Strang.
if [ "$(printf '%s' "$API_BODY" | json_get '.fix_insufficient' || echo false)" = 'true' ]; then
warn "FIX TRAEGT NICHT: Item #$id steht wieder in der Arbeitsschlange."
dim " Geprüft wurde $reported_build. Es wurde kein neues Item angelegt;"
dim " der Beleg hängt am bestehenden."
return 0
fi
if [ -n "$regression" ]; then
warn "REGRESSION: Item #$id ist ein Wiederauftreten von #$regression."
warn "Der Fehler galt als behoben und ist in $reported_build wieder da."
fi
if [ "$is_new" = 'true' ]; then
ok "Gemeldet als Item #$id in $target_project"
elif [ "$hits" = 'true' ]; then
ok "Schon gemeldet - Item #$id, unveraendert (client_ref $ref)"
else
ok "Wiederkehrendes Vorkommnis - Item #$id, nun ${occurrences:-?}x gezaehlt"
fi
# Eine Fremdmeldung landet NICHT in "open", sondern im Eingang
# triage_external. Ohne diesen Hinweis sucht der Meldende sein Item in der
# offenen Liste des fremden Projekts und findet es nicht - und meldet in
# drei Wochen dieselbe Sache noch einmal.
local status affected
status=$(printf '%s' "$API_BODY" | json_get '.item_status' || echo '')
affected=$(printf '%s' "$API_BODY" | json_get '.affects' || echo '')
if [ "$status" = 'triage_external' ]; then
dim " Status: triage_external - Eingang fuer Fremdmeldungen."
dim " \"$target_project\" bestaetigt und stuft ein; vorher zaehlt es"
dim " nicht in dessen Statistik. Herkunft ist als \"$DC_PROJECT\" vermerkt."
[ -n "$affected" ] && [ "$affected" != '[]' ] && dim " Bisher betroffen gemeldet: $affected"
elif [ -n "$status" ]; then
dim " Status: $status"
fi
dim " $DC_URL/index.php#tab-bugtracker"
}
# ---------------------------------------------------------------------------
# Arbeit uebernehmen und abschliessen
#
# Duenne Huellen um /manage. Sie sind hier, damit ein Agent den ganzen Kreis
# ueber ein Werkzeug schliessen kann - uebernehmen, kommentieren, abschliessen -
# statt fuer die Haelfte auf curl auszuweichen und dabei das Uebernehmen zu
# vergessen. Wer nicht uebernimmt, arbeitet irgendwann parallel zu jemandem.
# ---------------------------------------------------------------------------
# Wohin darf ich ueberhaupt melden?
#
# Ein Agent, der in seinem Projekt einen Fehler des Deploymentcenters findet,
# muss dessen Slug kennen. Ihn zu raten geht schief - eine Meldung an ein
# unbekanntes Projekt wird abgewiesen, und der Agent legt sie dann ersatzweise
# im eigenen ab, wo sie niemand findet, der sie beheben koennte.
cmd_projects() {
require_token
if ! api GET /api/bugtracker/v1/projects; then
die "Projektliste nicht abrufbar: $(api_error)"
fi
printf '%s' "$API_BODY" | python3 -c '
import json, sys
data = json.load(sys.stdin)
# Der Endpunkt hat die Liste ueber die Zeit unter verschiedenen Schluesseln
# gefuehrt; hier wird genommen, was da ist, statt einen Namen zu erzwingen.
items = None
for key in ("projects", "items", "data"):
if isinstance(data.get(key), list):
items = data[key]
break
if items is None:
items = [v for v in data.values() if isinstance(v, list)][:1]
items = items[0] if items else []
if not items:
print(" (keine Projekte gemeldet)")
raise SystemExit(0)
for entry in items:
if isinstance(entry, str):
print(" %s" % entry)
continue
slug = entry.get("slug") or entry.get("project_slug") or "?"
name = entry.get("name") or ""
print(" %-24s %s" % (slug, name))
'
dim ""
dim "Eigenes Projekt: $DC_PROJECT"
dim "In ein fremdes melden: --project <slug> --evidence \"...\" (mind. 40 Zeichen)"
}
cmd_next() {
require_project; require_token
local payload
payload=$(json_object project_slug "$DC_PROJECT" limit "${1:-1}")
if ! api POST '/api/bugtracker/v1/manage?action=next' "$payload"; then
if printf '%s' "$API_BODY" | grep -q 'already_claimed'; then
warn "Schon von jemand anderem uebernommen - naechstes Item nehmen."
return 2
fi
die "Kein Item erhalten: $(api_error)"
fi
# Eine Pruefaufgabe sieht im JSON aus wie jede andere Arbeit, ist aber eine
# andere Frage: nicht "behebe das", sondern "traegt der Fix?". Ohne diesen
# Hinweis faengt ein Agent an, einen bereits behobenen Fehler noch einmal zu
# beheben.
printf '%s' "$API_BODY" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)
for item in data.get("items") or []:
if item.get("status") != "in_verification":
continue
print("")
print(" NACHPRUEFUNG, keine neue Fehlersuche:")
print(" Item #%s %s" % (item.get("id"), item.get("title")))
print(" Behoben laut Tracker in: %s" % (item.get("resolved_in_build") or "unbekannt"))
if item.get("project_slug"):
print(" Projekt: %s" % item["project_slug"])
print("")
print(" Gegen diesen Build testen. Dann eines von beidem:")
print(" traegt -> dc-testkit confirm %s \"kurz was geprueft wurde\"" % item.get("id"))
print(" traegt nicht-> den Fehler erneut melden (dc-testkit report ...);")
print(" das Item wechselt dann selbst auf fix_insufficient.")
' || true
printf '%s\n' "$API_BODY"
}
# Nachpruefung bestaetigen.
#
# Bewusst getrennt von "resolve": resolve behauptet "ich habe es behoben, in
# Build X" und setzt resolved_in_build. Hier wird nichts behauptet, sondern
# etwas bestaetigt - der Fix eines anderen. Beides in einen Befehl zu legen
# hiesse, den Unterschied zwischen Fixen und Pruefen einzuebnen.
cmd_confirm() {
require_token
[ $# -ge 1 ] || die "Aufruf: dc-testkit confirm <id> [was geprueft wurde]"
local id="$1"; shift
local note="${1:-}"
local build
build="$(detect_build || true)"
local text="Nachpruefung bestanden${build:+ (geprueft gegen $build)}."
[ -n "$note" ] && text="$text $note"
local payload
payload=$(json_object status closed notes "$text")
api POST "/api/bugtracker/v1/manage?action=status&id=$id" "$payload" \
|| die "Bestaetigung nicht angenommen: $(api_error)"
ok "Item #$id bestaetigt und geschlossen${build:+ - geprüft gegen $build}"
}
cmd_comment() {
require_token
[ $# -ge 2 ] || die "Aufruf: dc-testkit comment <id> <text> [aktion]"
local id="$1"; shift
local comment="$1"; shift
local action="${1:-investigated}"
local payload
payload=$(json_object comment "$comment" action_taken "$action")
api POST "/api/bugtracker/v1/manage?action=comment&id=$id" "$payload" \
|| die "Kommentar nicht angenommen: $(api_error)"
ok "Kommentar an Item #$id"
}
cmd_resolve() {
require_token
[ $# -ge 2 ] || die "Aufruf: dc-testkit resolve <id> <build> [notizen]"
local id="$1"; shift
local build="$1"; shift
local notes="${1:-}"
local payload
payload=$(json_object resolved_in_build "$build" resolution_notes "$notes")
api POST "/api/bugtracker/v1/manage?action=resolve&id=$id" "$payload" \
|| die "Abschluss nicht angenommen: $(api_error)"
ok "Item #$id abgeschlossen (behoben in $build)"
}
cmd_release_claim() {
require_token
[ $# -ge 1 ] || die "Aufruf: dc-testkit unclaim <id> [grund]"
local id="$1"; shift
local payload
payload=$(json_object note "${1:-Zurueckgegeben durch dc-testkit}")
api POST "/api/bugtracker/v1/manage?action=release&id=$id" "$payload" \
|| die "Rueckgabe nicht angenommen: $(api_error)"
ok "Item #$id zurueckgegeben"
}
# ---------------------------------------------------------------------------
# run - der ganze Durchlauf
#
# Lizenz holen, Agent besorgen, in einen leeren Sandkasten ausliefern, Ergebnis
# pruefen. Scheitert ein Schritt, wird der Befund gemeldet statt nur gedruckt -
# denn ein fehlgeschlagener Testlauf, von dem nur das Terminal weiss, ist kein
# Testlauf gewesen.
#
# Die Meldung ist ueber client_ref entstoert: Derselbe Fehlschlag in fuenf
# Naechten erzeugt ein Item, nicht fuenf.
# ---------------------------------------------------------------------------
cmd_run() {
require_project; require_token
local report=1
[ "${1:-}" = '--no-report' ] && report=0
local log
log="$(state_dir)/last-run.log"
ensure_state
local failed_step=''
# Der Schritt laeuft in einer SUBSHELL, und das ist der Punkt: die() beendet
# mit exit. Ohne Subshell risse der erste Fehlschlag das ganze Skript mit -
# und ausgerechnet der Durchlauf, der Fehlschlaege melden soll, kaeme nie
# bis zur Meldung. Dateien, die ein Schritt anlegt (Schluessel, Binary,
# Sandkasten), ueberleben die Subshell ohnehin; nur Variablen nicht, und die
# gibt niemand weiter.
_stage() {
local name="$1"; shift
step "$name"
if ( "$@" ) >>"$log" 2>&1; then
ok "$name"
return 0
fi
failed_step="$name"
warn "$name fehlgeschlagen"
return 1
}
: > "$log"
{
_stage "DEV-Lizenz holen" cmd_license \
&& _stage "update-agent besorgen" cmd_agent \
&& _stage "Erstauslieferung" cmd_install \
&& _stage "Ergebnis pruefen" cmd_verify \
&& _stage "Update auf latest" cmd_update \
&& _stage "Ergebnis erneut pruefen" cmd_verify
} || true
if [ -z "$failed_step" ]; then
printf '\n%sDurchlauf vollstaendig.%s\n' "$C_GREEN" "$C_RESET"
dim "Protokoll: $log"
return 0
fi
printf '\n%sDurchlauf abgebrochen bei: %s%s\n' "$C_RED" "$failed_step" "$C_RESET"
dim "Protokoll: $log"
if [ "$report" -eq 1 ]; then
step "Befund melden"
tail -c 4000 "$log" | cmd_report \
--type bug \
--title "Testlauf scheitert: $failed_step ($DC_PROJECT/$DC_CHANNEL)" \
--severity high \
--error "$(tail -n 1 "$log")" \
--ref "testkit-run-$(printf '%s|%s|%s' "$DC_PROJECT" "$DC_CHANNEL" "$failed_step" | sha256sum | cut -c1-24)"
fi
return 1
}
# ---------------------------------------------------------------------------
# cleanup - nichts zuruecklassen
#
# Die Regel der Testumgebung lautet: Nichts Dauerhaftes hinterlassen, was nicht
# in der Belegungsliste steht. Der DEV-Schluessel gehoert dazu - er laeuft zwar
# von selbst ab, aber ein zurueckgegebener Schluessel macht sofort Platz fuer
# die naechste Maschine.
# ---------------------------------------------------------------------------
cmd_cleanup() {
require_project
local only_sandbox=0
[ "${1:-}" = '--sandbox' ] && only_sandbox=1
local dir
dir="$(sandbox_dir)"
if [ -d "$dir" ]; then
rm -rf "$dir"
ok "Sandkasten entfernt: $dir"
fi
[ "$only_sandbox" -eq 1 ] && return 0
if [ -f "$(key_file)" ] && [ -n "$DC_TOKEN" ]; then
cmd_license --revoke || warn "DEV-Schluessel liess sich nicht zurueckgeben - er laeuft von selbst ab."
fi
rm -f "$(agent_bin)" "$(state_dir)/last-run.log"
ok "Aufgeraeumt."
}
# ---------------------------------------------------------------------------
# Einstieg
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
dc-testkit $DC_TESTKIT_VERSION - Testlaeufe gegen das Deploymentcenter
dc-testkit [--project <slug>] [--channel <kanal>] [--url <adresse>] <befehl>
Einrichten und pruefen
doctor Werkzeuge, Netz, Token und Projekt pruefen
license [--ttl N] [--print] [--revoke]
DEV-Lizenzschluessel holen, verlaengern, zurueckgeben
agent [--force] update-agent herunterladen, Pruefsumme abgleichen
Auslieferung pruefen
check Gibt es ein Update?
install Erstauslieferung in einen leeren Sandkasten
update [--version <ver>] Update oder Rueckfall auf eine aeltere Fassung
repair Integritaetsreparatur
list Verfuegbare Fassungen
verify Was liegt im Sandkasten, und passt es zum Server?
run [--no-report] Alles nacheinander; meldet, was scheitert
Bugtracker
projects Wohin darf ich melden? Alle Projekt-Slugs
report --title T --desc D [--type bug|feature] [--severity S]
[--file F --line N] [--error M] [--stack-file P]
report --project FREMDER-SLUG --evidence "warum dort und nicht hier"
Befund im FREMDEN Projekt melden. Begruendung mind. 40 Zeichen,
Schweregrad wird auf medium gedeckelt, landet in triage_external.
next [limit] Naechstes Item uebernehmen - auch Nachpruefungen
comment <id> <text> [aktion]
resolve <id> <build> [notizen] "ich habe es behoben"
confirm <id> [notiz] "der Fix eines anderen traegt" -> schliesst
unclaim <id> [grund] Uebernahme zurueckgeben
Aufraeumen
cleanup [--sandbox] Sandkasten weg, Schluessel zurueckgeben
Schweregrade: idea, wishlist, low, medium, high, critical
Typen: bug, feature ("idea" ist ein Schweregrad, kein Typ)
Konfiguration: $CONFIG_FILE
DC_URL, DC_TOKEN, DC_PROJECT, DC_CHANNEL, DC_INSTANCE
EOF
}
main() {
while [ $# -gt 0 ]; do
case "$1" in
--project) DC_PROJECT="$2"; shift 2 ;;
--channel) DC_CHANNEL="$2"; shift 2 ;;
--url) DC_URL="${2%/}"; shift 2 ;;
--instance) DC_INSTANCE="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--version) printf 'dc-testkit %s\n' "$DC_TESTKIT_VERSION"; exit 0 ;;
*) break ;;
esac
done
local command="${1:-}"
[ $# -gt 0 ] && shift
case "$command" in
doctor) cmd_doctor "$@" ;;
license) cmd_license "$@" ;;
agent) cmd_agent "$@" ;;
check) cmd_check "$@" ;;
install) cmd_install "$@" ;;
update) cmd_update "$@" ;;
repair) cmd_repair "$@" ;;
list) cmd_list "$@" ;;
verify) cmd_verify "$@" ;;
run) cmd_run "$@" ;;
projects) cmd_projects "$@" ;;
report) cmd_report "$@" ;;
next) cmd_next "$@" ;;
comment) cmd_comment "$@" ;;
resolve) cmd_resolve "$@" ;;
confirm) cmd_confirm "$@" ;;
unclaim) cmd_release_claim "$@" ;;
cleanup) cmd_cleanup "$@" ;;
''|help) usage ;;
*) die "Unbekannter Befehl: $command (dc-testkit --help)" ;;
esac
}
main "$@"