Second independent agent, given only "produce defensible benchmark evidence", found the skill from CLAUDE.md and ran the full plan first try: 129.37s clean, 0.17s no-op, 9.69s changed-file, box and runner both confirmed gone. The receipt-bound cleanup fixed in the previous commit worked for it. Five things still failed it. Two were code. The post-stop poll wrote to status-before-stop.log, so the evidence pack's own pre-stop record was overwritten with post-stop state and an auditor would misread when the box existed; it writes status-after-stop.log now. And cleanup-preview.json paired warmup_ref with the benchmarked source_sha, so the destruction record named a SHA that is not on the ref beside it; it records both refs now, schema 2, and the regression test asserts it. Three were documentation. benchmark.md bills itself as the full plan but never mentioned the deployment gate, so its warmup block deadlocks for 15 minutes on `status --wait`; it now points at the guarded approval and warns against approving workflow_runs[0]. The PREVIEW cleanup exits 75 on success, which the same plan's mandated `set -e` turns into an abort right before cleanup; both call sites say so and show the `set +e` wrapper. And SKILL.md claimed stopping the box ends the warmup run, which is false: the measured run sat in_progress for four minutes afterwards holding a 32 vCPU runner, so cancelling is now a required step rather than a hedge.
386 lines
14 KiB
Bash
Executable File
386 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
if [[ $# -ne 4 ]]; then
|
|
echo "usage: $0 <testbox-id> <evidence-directory> <ownership-token> <PREVIEW|STOP:preview-sha>" >&2
|
|
exit 64
|
|
fi
|
|
|
|
testbox_id="$1"
|
|
evidence_dir="$2"
|
|
ownership_token="$3"
|
|
operator_confirmation="$4"
|
|
if [[ ! "$testbox_id" =~ ^tbx_[A-Za-z0-9_-]+$ ]]; then
|
|
echo "invalid Testbox ID: $testbox_id" >&2
|
|
exit 64
|
|
fi
|
|
if [[ ! "$ownership_token" =~ ^[0-9a-f]{32}$ ]]; then
|
|
echo "ownership token must be a 32-character lowercase hex value" >&2
|
|
exit 64
|
|
fi
|
|
if [[ "$operator_confirmation" != "PREVIEW" && ! "$operator_confirmation" =~ ^STOP:[0-9a-f]{64}$ ]]; then
|
|
echo "confirmation must be PREVIEW or STOP:<64-character preview SHA>" >&2
|
|
exit 64
|
|
fi
|
|
|
|
mkdir -p "$evidence_dir"
|
|
sha256_file() {
|
|
if command -v sha256sum >/dev/null; then
|
|
sha256sum "$1" | awk '{print $1}'
|
|
elif command -v shasum >/dev/null; then
|
|
shasum -a 256 "$1" | awk '{print $1}'
|
|
else
|
|
echo "sha256sum or shasum is required for cleanup preview hashing" >&2
|
|
return 65
|
|
fi
|
|
}
|
|
bounded_command() {
|
|
scripts_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
"$scripts_dir/blacksmith-bounded-command.sh" "$@"
|
|
}
|
|
receipt_path="$evidence_dir/testbox-receipt.json"
|
|
if [[ ! -s "$receipt_path" ]]; then
|
|
echo "refusing cleanup without the warmup ownership receipt: $receipt_path" >&2
|
|
exit 65
|
|
fi
|
|
python3 - "$receipt_path" "$testbox_id" "$ownership_token" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
receipt_path, expected_id, expected_token = sys.argv[1:]
|
|
try:
|
|
receipt = json.loads(pathlib.Path(receipt_path).read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
raise SystemExit(f"invalid warmup ownership receipt: {error}")
|
|
if receipt.get("testbox_id") != expected_id:
|
|
raise SystemExit("cleanup ID does not match the warmup ownership receipt")
|
|
if receipt.get("confirmation_token") != expected_token:
|
|
raise SystemExit("ownership token does not match the warmup ownership receipt")
|
|
# warmup_ref is what `blacksmith testbox list` shows and is always main in the
|
|
# broker lane. source_ref is the branch being benchmarked and never appears in
|
|
# the inventory. Conflating them made every receipt-bound cleanup exit 66.
|
|
for field in ("workflow", "job", "warmup_ref", "source_ref", "source_sha", "source_tree_sha", "ghostty_gitlink_sha"):
|
|
if not receipt.get(field):
|
|
raise SystemExit(f"warmup ownership receipt is missing {field}")
|
|
PY
|
|
receipt_workflow="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["workflow"])
|
|
PY
|
|
)"
|
|
receipt_job="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["job"])
|
|
PY
|
|
)"
|
|
receipt_ref="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["warmup_ref"])
|
|
PY
|
|
)"
|
|
receipt_source_ref="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["source_ref"])
|
|
PY
|
|
)"
|
|
receipt_source_sha="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["source_sha"])
|
|
PY
|
|
)"
|
|
receipt_source_tree_sha="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["source_tree_sha"])
|
|
PY
|
|
)"
|
|
receipt_ghostty_sha="$(python3 - "$receipt_path" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
print(json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))["ghostty_gitlink_sha"])
|
|
PY
|
|
)"
|
|
|
|
# Parse either the table emitted by `list/status --id` or a summary response.
|
|
# The parser validates context only for a row containing this exact ID. It does
|
|
# not rely on fixed whitespace columns, because queued rows may have an empty IP.
|
|
parse_cli_output() {
|
|
local log_path="$1"
|
|
python3 - "$log_path" "$testbox_id" "$receipt_workflow" "$receipt_job" "$receipt_ref" <<'PY'
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
path, expected_id, expected_workflow, expected_job, expected_ref = sys.argv[1:]
|
|
text = pathlib.Path(path).read_text(encoding="utf-8", errors="replace")
|
|
for line in text.splitlines():
|
|
fields = line.split()
|
|
if not fields or fields[0] != expected_id:
|
|
continue
|
|
if len(fields) < 2:
|
|
raise SystemExit(66)
|
|
status = fields[1].lower()
|
|
# Blacksmith CLI releases have emitted both ID/STATUS/REPO/WORKFLOW/CREATED
|
|
# and ID/STATUS/IP/WORKFLOW/JOB/REF/... schemas. Require the exact workflow
|
|
# in either schema; when job/ref columns exist, require those too.
|
|
try:
|
|
workflow_index = fields.index(expected_workflow, 2)
|
|
except ValueError:
|
|
raise SystemExit(66)
|
|
trailing = fields[workflow_index + 1:]
|
|
# Known schemas either end after CREATED (no job/ref columns) or expose
|
|
# JOB and REF immediately after WORKFLOW. If either expected field appears,
|
|
# require the exact pair in the exact order; reject all ambiguous contexts.
|
|
if len(trailing) >= 2 and trailing[:2] == [expected_job, expected_ref]:
|
|
pass
|
|
elif expected_job in trailing or expected_ref in trailing:
|
|
raise SystemExit(66)
|
|
elif len(trailing) not in (1, 2):
|
|
raise SystemExit(66)
|
|
print(status)
|
|
raise SystemExit(0)
|
|
|
|
# Some CLI versions use a summary such as `[tbx_...] Status: ready`.
|
|
if re.search(rf"\b{re.escape(expected_id)}\b", text):
|
|
match = re.search(r"\bstatus\s*:?\s*([A-Za-z_]+)", text, re.IGNORECASE)
|
|
if match:
|
|
print(match.group(1).lower())
|
|
raise SystemExit(0)
|
|
raise SystemExit(3)
|
|
PY
|
|
}
|
|
|
|
is_terminal() {
|
|
case "$1" in
|
|
completed|stopped|cancelled|failed|terminated|hydration_failed) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
is_active() {
|
|
case "$1" in
|
|
ready|running|hydrating|in_progress|queued) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
is_known_absence() {
|
|
grep -Eiq '(not found|already[[:space:]]+(stopped|completed)|hydration_failed|HTTP[[:space:]]+404|status[[:space:]]+code[[:space:]]+404|HTTP[[:space:]]+409|status[[:space:]]+code[[:space:]]+409)' "$1"
|
|
}
|
|
|
|
inventory_log="$evidence_dir/list-before-stop.log"
|
|
set +e
|
|
bounded_command 20 blacksmith testbox list --all >"$inventory_log" 2>&1
|
|
inventory_status=$?
|
|
set -e
|
|
if (( inventory_status != 0 )); then
|
|
echo "failed to capture the Testbox inventory before cleanup; refusing stop" >&2
|
|
exit "$inventory_status"
|
|
fi
|
|
|
|
inventory_row_present=0
|
|
set +e
|
|
parse_cli_output "$inventory_log" >/dev/null
|
|
inventory_parse_status=$?
|
|
set -e
|
|
case "$inventory_parse_status" in
|
|
0) inventory_row_present=1 ;;
|
|
3) : ;;
|
|
66) echo "inventory ownership context differs from the warmup receipt; refusing cleanup" >&2; exit 66 ;;
|
|
*) echo "could not parse the Testbox inventory; refusing cleanup" >&2; exit "$inventory_parse_status" ;;
|
|
esac
|
|
|
|
status_log="$evidence_dir/status-before-stop.log"
|
|
set +e
|
|
bounded_command 20 blacksmith testbox status --id "$testbox_id" >"$status_log" 2>&1
|
|
status_command_status=$?
|
|
set -e
|
|
status_value=""
|
|
status_absent=0
|
|
if (( status_command_status == 0 )); then
|
|
set +e
|
|
status_value="$(parse_cli_output "$status_log")"
|
|
status_parse_status=$?
|
|
set -e
|
|
case "$status_parse_status" in
|
|
0) ;;
|
|
3) echo "status omitted the owned Testbox $testbox_id; refusing cleanup" >&2; exit 66 ;;
|
|
66) echo "status ownership context differs from the warmup receipt; refusing cleanup" >&2; exit 66 ;;
|
|
*) echo "could not parse status for owned Testbox $testbox_id; refusing cleanup" >&2; exit "$status_parse_status" ;;
|
|
esac
|
|
elif is_known_absence "$status_log"; then
|
|
status_absent=1
|
|
else
|
|
echo "failed to inspect owned Testbox $testbox_id before cleanup; refusing stop" >&2
|
|
exit "$status_command_status"
|
|
fi
|
|
|
|
if (( status_absent == 0 )) && is_active "$status_value" && (( inventory_row_present == 0 )); then
|
|
echo "owned Testbox is active but absent from the inventory; refusing cleanup" >&2
|
|
exit 66
|
|
fi
|
|
if (( status_absent == 0 )) && ! is_active "$status_value" && ! is_terminal "$status_value"; then
|
|
echo "unknown status for owned Testbox $testbox_id; refusing cleanup" >&2
|
|
exit 66
|
|
fi
|
|
|
|
preview_path="$evidence_dir/cleanup-preview.json"
|
|
python3 - "$preview_path" "$testbox_id" "${status_value:-absent}" "$inventory_row_present" "$receipt_workflow" "$receipt_job" "$receipt_ref" "$receipt_source_ref" "$receipt_source_sha" "$receipt_source_tree_sha" "$receipt_ghostty_sha" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
(path, testbox_id, status, inventory_present, workflow, job, warmup_ref,
|
|
source_ref, source_sha, source_tree_sha, ghostty_sha) = sys.argv[1:]
|
|
payload = {
|
|
"schema": 2,
|
|
"testbox_id": testbox_id,
|
|
"status": status,
|
|
"inventory_row_present": bool(int(inventory_present)),
|
|
"workflow": workflow,
|
|
"job": job,
|
|
# The ref the box was warmed from, which is what the inventory row shows.
|
|
"warmup_ref": warmup_ref,
|
|
# The branch actually benchmarked. Pairing warmup_ref with source_sha made
|
|
# the destruction record claim a SHA that is not on the ref beside it.
|
|
"source_ref": source_ref,
|
|
"source_sha": source_sha,
|
|
"source_tree_sha": source_tree_sha,
|
|
"ghostty_gitlink_sha": ghostty_sha,
|
|
}
|
|
out = pathlib.Path(path)
|
|
out.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
out.chmod(0o600)
|
|
PY
|
|
preview_sha="$(sha256_file "$preview_path" | awk '{print $1}')"
|
|
printf 'Testbox cleanup preview: id=%s status=%s inventory_row=%s workflow=%s job=%s ref=%s\n' \
|
|
"$testbox_id" "${status_value:-absent}" "$inventory_row_present" "$receipt_workflow" "$receipt_job" "$receipt_ref"
|
|
printf 'Preview SHA: %s\n' "$preview_sha"
|
|
if [[ "$operator_confirmation" == "PREVIEW" ]]; then
|
|
echo "Review the preview, then rerun with the same token and STOP:$preview_sha to authorize stop." >&2
|
|
exit 75
|
|
fi
|
|
expected_preview_sha="${operator_confirmation#STOP:}"
|
|
if [[ "$operator_confirmation" != "PREVIEW" && "$expected_preview_sha" != "$preview_sha" ]]; then
|
|
echo "current cleanup preview differs from the supplied confirmation; refusing stop" >&2
|
|
exit 67
|
|
fi
|
|
|
|
# The pre-stop status is evidence of when the box was alive. The poll below
|
|
# writes to its own file so it cannot overwrite that record.
|
|
post_status_log="$evidence_dir/status-after-stop.log"
|
|
stop_log="$evidence_dir/stop.log"
|
|
list_log="$evidence_dir/list-after-stop.log"
|
|
cleanup_status=0
|
|
poll_deadline=$((SECONDS + 120))
|
|
poll_attempt=0
|
|
if (( status_absent == 1 )) || is_terminal "$status_value"; then
|
|
printf 'Testbox %s is already terminal or absent; no stop request needed\n' "$testbox_id" >"$stop_log"
|
|
else
|
|
set +e
|
|
bounded_command 20 blacksmith testbox stop --id "$testbox_id" >"$stop_log" 2>&1
|
|
stop_status=$?
|
|
set -e
|
|
if (( stop_status != 0 )); then
|
|
if is_known_absence "$stop_log"; then
|
|
printf 'stop reached a known terminal or absent state for %s; continuing\n' "$testbox_id" >&2
|
|
else
|
|
echo "failed to stop Testbox $testbox_id; see $stop_log" >&2
|
|
cleanup_status=$stop_status
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Poll the ID-specific endpoint until cancellation propagates. Never treat a
|
|
# different row in the global inventory as proof that this ID is terminal.
|
|
while :; do
|
|
poll_attempt=$((poll_attempt + 1))
|
|
: >"$post_status_log"
|
|
set +e
|
|
bounded_command 20 blacksmith testbox status --id "$testbox_id" >"$post_status_log" 2>&1
|
|
status_command_status=$?
|
|
set -e
|
|
if (( status_command_status == 0 )); then
|
|
set +e
|
|
status_value="$(parse_cli_output "$post_status_log")"
|
|
status_parse_status=$?
|
|
set -e
|
|
if (( status_parse_status != 0 )); then
|
|
echo "could not parse post-stop status for $testbox_id; see $post_status_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=66
|
|
break
|
|
fi
|
|
if is_terminal "$status_value"; then
|
|
break
|
|
fi
|
|
if ! is_active "$status_value"; then
|
|
echo "unknown post-stop status for $testbox_id; see $post_status_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=66
|
|
break
|
|
fi
|
|
elif is_known_absence "$post_status_log"; then
|
|
break
|
|
else
|
|
echo "failed to inspect Testbox $testbox_id after cleanup; see $post_status_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=$status_command_status
|
|
break
|
|
fi
|
|
if (( SECONDS >= poll_deadline )); then
|
|
echo "Testbox $testbox_id is still active after bounded cleanup polling" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=1
|
|
break
|
|
fi
|
|
sleep_seconds=$((poll_attempt < 6 ? poll_attempt * 2 : 10))
|
|
sleep "$sleep_seconds"
|
|
done
|
|
|
|
set +e
|
|
bounded_command 20 blacksmith testbox list --all >"$list_log" 2>&1
|
|
list_status=$?
|
|
set -e
|
|
if (( list_status != 0 )); then
|
|
echo "failed to list Testboxes after stopping $testbox_id; see $list_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=$list_status
|
|
else
|
|
set +e
|
|
listed_status="$(parse_cli_output "$list_log")"
|
|
listed_parse_status=$?
|
|
set -e
|
|
case "$listed_parse_status" in
|
|
0)
|
|
if is_active "$listed_status"; then
|
|
echo "Testbox $testbox_id is still active after cleanup; see $list_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=1
|
|
elif ! is_terminal "$listed_status"; then
|
|
echo "unknown status for Testbox $testbox_id in inventory: $listed_status" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=66
|
|
fi
|
|
;;
|
|
3) ;;
|
|
66)
|
|
echo "Testbox $testbox_id ownership changed in final inventory; see $list_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=66
|
|
;;
|
|
*)
|
|
echo "could not parse final Testbox inventory; see $list_log" >&2
|
|
(( cleanup_status == 0 )) && cleanup_status=$listed_parse_status
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if (( cleanup_status != 0 )); then
|
|
exit "$cleanup_status"
|
|
fi
|
|
printf 'verified Testbox %s is no longer active\n' "$testbox_id"
|