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>
1336 lines
48 KiB
Bash
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 "$@"
|