Files
Deploymentcenter/public/testkit/dc-testkit.sh
T
Deploymentcenter BotandClaude Opus 5 3b56839a1e fix(2.7.1): Auskuenfte, die stimmen - Fassung, Health, doctor, Ansicht
Der erste Durchlauf der Testinstanz hat neun Bugs zutage gefoerdert, und fast
alle waren dieselbe Sorte Fehler: eine Auskunft, die nicht stimmte.

Am folgenreichsten die Fassung. config/config.php fuehrte app.version als feste
Zeichenkette und stand seit 2.0.0 darauf, waehrend 2.7.0 lief - unbemerkt, weil
die Datei gitignored ist und in keinem Diff auftaucht. Zwei gemeldete Bugs sind
daran fehldiagnostiziert worden ("der Server ist zu alt"). src/bootstrap.php
leitet die Fassung jetzt aus "current" in public/docs/changelog.json ab.

Im Einzelnen:

- #346 Fassung aus dem Changelog statt aus der Konfiguration
- #60  /api/health ueberlebt eine leere Datenbank: eigener Check "schema",
       healthy=false mit Begruendung statt HTTP 500
- #54  dc-testkit doctor prueft Mindestfassung (semantisch) und Rechte, statt
       sie nur anzuzeigen - dafuer neu: GET /api/tokens/v1/whoami
- #53  dc-testkit.sh unter /testkit/ erreichbar (Route fehlte, der
       dokumentierte Bezugsweg hat nie funktioniert) und mit LF ausgeliefert
       (deploy.py/deploy_dev.py entfernen CR bei .sh/.bash beim Upload)
- #288 Filter und Formulare werfen nicht mehr aufs Dashboard zurueck: die
       offene Ansicht wandert bei GET als "view" in den Query-String, und die
       Filter loesen ueber requestSubmit() aus - form.submit() erzeugt kein
       submit-Ereignis, weshalb der zentrale Zuhoerer dort nie lief
- #347 /api/bugtracker/v1/roadmap auch ohne .php erreichbar
- #55  Fassungshinweis in TESTUMGEBUNG.md 4.6 (serverseitig war das mit 2.6.0
       bereits behoben)

Nicht geprueft: php -l. Auf dem Arbeitsplatz ist kein PHP installiert; die
Abnahme laeuft ueber die Testinstanz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:06:33 +02:00

1336 lines
48 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}"
# Aelteste Fassung des Deploymentcenters, mit der dieses Testkit vollstaendig
# arbeiten kann. Darunter fehlen /api/license/v1/dev-key (dc-testkit license),
# der Bezug ueber /testkit/ und /api/tokens/v1/whoami (Rechtepruefung in
# doctor). Die Zahl stand vorher nur in einer Fehlermeldung mitten im Ablauf -
# doctor kannte sie nicht und meldete "Bereit" auf einem zu alten Server.
DC_MIN_SERVER="${DC_MIN_SERVER:-2.6.1}"
# 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
}
# ---------------------------------------------------------------------------
# Fassungen und Rechte
# ---------------------------------------------------------------------------
# Ist $1 aelter als $2? 0 = ja, 1 = nein, 2 = nicht vergleichbar.
#
# Nicht "sort -V": das fehlt auf BusyBox-Systemen, und die Antwort waere dort
# still falsch statt laut fehlend. Ein Vorabkennzeichen (1.2.3-rc1) wird
# abgeschnitten - fuer die Frage "reicht der Server aus" genuegt das.
version_lt() {
local a="${1%%-*}" b="${2%%-*}" i av bv
for i in 1 2 3; do
av=$(printf '%s' "$a" | cut -d. -f$i)
bv=$(printf '%s' "$b" | cut -d. -f$i)
av="${av:-0}"; bv="${bv:-0}"
case "$av$bv" in
''|*[!0-9]*) return 2 ;;
esac
[ "$av" -lt "$bv" ] && return 0
[ "$av" -gt "$bv" ] && return 1
done
return 1
}
# Deckt die Rechteliste $1 (JSON-Array aus whoami) das Recht $2 ab?
#
# Die Wildcards muessen mitgedacht werden: "*" deckt alles ab, "bugtracker:*"
# alles darunter. Sonst meldete das Werkzeug ein Recht als fehlend, das der
# Server anerkennt - und schickte jemanden ein Token neu bauen, das passt.
has_scope() {
local list="$1" want="$2" prefix
prefix="${2%%:*}"
case "$list" in
*'"*"'*) return 0 ;;
*"\"$want\""*) return 0 ;;
*"\"$prefix:*\""*) return 0 ;;
esac
return 1
}
# ---------------------------------------------------------------------------
# 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 server_version
healthy=$(printf '%s' "$API_BODY" | json_get '.healthy' || echo '?')
server_version=$(printf '%s' "$API_BODY" | json_get '.version' || echo '')
ok "erreichbar, healthy=$healthy, Fassung ${server_version:-?}"
# Die Fassung stand doctor immer schon vor Augen - verglichen wurde sie
# nur nie. Deshalb meldete er "Bereit" und der erste Schritt, der den
# Server wirklich braucht, scheiterte danach mit "kennt
# /api/license/v1/dev-key noch nicht".
if [ -z "$server_version" ]; then
warn "der Server nennt keine Fassung - $DC_MIN_SERVER nicht pruefbar"
else
version_lt "$server_version" "$DC_MIN_SERVER"
case $? in
0)
warn "zu alt: $server_version - gebraucht wird mindestens $DC_MIN_SERVER"
warn " Es fehlen: dc-testkit license (dev-key), der Bezug ueber /testkit/"
warn " und die Rechtepruefung. Der Server muss deployt werden."
failed=1
;;
2)
warn "Fassung \"$server_version\" ist nicht auswertbar - $DC_MIN_SERVER ungeprueft"
;;
*)
ok "erfuellt die Mindestfassung $DC_MIN_SERVER"
;;
esac
fi
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/tokens/v1/whoami; then
# Fruehere Fassungen meldeten hier nur "gueltig - der Bugtracker
# antwortet". Das lief auch mit einem Laufzeit-Token durch, dem
# bugtracker:manage und license:dev fehlten - der Lauf scheiterte dann
# erst dort, wo die Rechte wirklich gebraucht wurden.
local tok_name tok_owner tok_project tok_scopes tok_workbench want
tok_name=$(printf '%s' "$API_BODY" | json_get '.name' || echo '?')
tok_owner=$(printf '%s' "$API_BODY" | json_get '.owner_type' || echo '?')
tok_project=$(printf '%s' "$API_BODY" | json_get '.project_slug' || echo '')
tok_scopes=$(printf '%s' "$API_BODY" | json_get '.scopes' || echo '[]')
tok_workbench=$(printf '%s' "$API_BODY" | json_get '.is_workbench' || echo 'false')
ok "gueltig - \"$tok_name\", Besitzerart $tok_owner, Projekt ${tok_project:-keins}"
dim " Rechte: $tok_scopes"
if [ "$tok_workbench" != 'true' ]; then
warn "kein Werkbank-Token: Besitzerart ist $tok_owner, gebraucht wird dev_agent"
warn " Ein Laufzeit-Token darf nicht in fremde Projekte melden (TESTUMGEBUNG.md 4.2)."
failed=1
fi
for want in bugtracker:manage license:dev updateservice:read; do
if ! has_scope "$tok_scopes" "$want"; then
warn "Recht fehlt: $want"
failed=1
fi
done
if [ -n "$tok_project" ] && [ -n "$DC_PROJECT" ] && [ "$tok_project" != "$DC_PROJECT" ]; then
warn "Token gehoert zu $tok_project, gearbeitet wird an $DC_PROJECT"
warn " Je Projekt ein eigenes Token - sonst ist die Herkunft einer Meldung nicht mehr ablesbar."
failed=1
fi
elif [ "$API_STATUS" = '404' ]; then
# Server ohne whoami. Dann bleibt nur die alte, grobe Pruefung - und
# der ausdrueckliche Hinweis, dass die Rechte ungeprueft sind. Still
# "gueltig" zu melden waere genau der Fehler, der hier behoben wird.
if api GET /api/bugtracker/v1/projects; then
warn "gueltig, aber die Rechte sind ungeprueft"
warn " /api/tokens/v1/whoami gibt es erst ab $DC_MIN_SERVER."
else
warn "Token wird nicht angenommen: $(api_error)"
failed=1
fi
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 "$@"