lease pool: four priority classes (main, 7 Oct 2026 20:37 BST: the class v5 census sat behind eleven adversarial waiters): release > v5 > measure > adv, from --class or the owner and label; a higher class takes the next freed cores before any lower class whatever the arrival order (lower classes yield while a higher one waits); a lower-class holder above 32 threads is pre-empted (TERM to its command, the lane re-queues) when a higher class has waited 120 s; the class in every line; self-test: the known-failed class-order case (a sweep must not take cores a waiting v5 gate asked for) and the pre-emption case. Also: provision.sh ROLE=sweep (a rented cloud sweep worker, minus runner, caddy, cuda, night, chromium, zig) and infra/build-server/cloud-sweep.sh (Hetzner Cloud CCX workers up/list/cost/down with the lease tool)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 19:40:32 +00:00
parent 8e77476a59
commit e48a512c3f
3 changed files with 156 additions and 27 deletions

View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Rented SWEEP WORKERS on Hetzner Cloud (main's order, 7 October 2026): CCX63 (48 dedicated cores) or CCX53 (32) by the hour,
# provisioned like a build box minus the runner (provision.sh ROLE=sweep: build user with the Mac's key, toolchain, sccache,
# the bare mirrors, the lease tool with its pool, ufw), for the adversarial sweeps that queue on build-1 and build-2. Destroyed
# after. Within USD 1,500 a day, a cost line at every USD 100 (cost). Token at ~/.config/igneum/hetzner-token (never printed),
# key igneum-ops (the Mac's ~/.ssh/igneum_ed25519), hcloud CLI.
#
# infra/build-server/cloud-sweep.sh up <n> [--type ccx63|ccx53] [--location fsn1|nbg1|hel1] create igneum-sweep-<n>, provision, host line
# infra/build-server/cloud-sweep.sh list the workers with uptime and cost so far
# infra/build-server/cloud-sweep.sh cost one line: hours, EUR, USD so far (all workers)
# infra/build-server/cloud-sweep.sh down <n> [--force] refuse while a lease is held (lease status), then delete
#
# Host files: ~/.config/igneum/sweep-<n> ("build@<ip>", what a lane's ssh line reads). The pool on a worker: cores 8 to ncpu-1
# (40 on a CCX63, 24 on a CCX53); the lanes use the same `lease pool <threads> -- cmd` line as on the boxes.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export HCLOUD_TOKEN; HCLOUD_TOKEN="$(tr -d '[:space:]' < "$HOME/.config/igneum/hetzner-token")"
HC="${HCLOUD:-/opt/homebrew/bin/hcloud}"; KEY="$HOME/.ssh/igneum_ed25519"
EUR_USD="${EUR_USD:-1.09}"
die() { echo "cloud-sweep: $*" >&2; exit 1; }
hourly() { "$HC" server-type describe "$1" -o json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["prices"][0]["price_hourly"]["gross"])'; }
case "${1:-}" in
up)
n="${2:-}"; [ -n "$n" ] || die "up <n>"; shift 2; TYPE=ccx63; LOC=fsn1
while [ $# -gt 0 ]; do case "$1" in --type) TYPE="$2"; shift 2 ;; --location) LOC="$2"; shift 2 ;; *) die "unknown $1" ;; esac; done
NAME="igneum-sweep-$n"
if ! "$HC" server describe "$NAME" >/dev/null 2>&1; then
"$HC" server create --name "$NAME" --type "$TYPE" --image ubuntu-24.04 --location "$LOC" --ssh-key igneum-ops --label igneum=sweep --label role=sweep >/dev/null
echo "created $NAME ($TYPE, $LOC) at $(TZ=UTC date +%H:%M:%SZ)"
fi
IP=$("$HC" server ip "$NAME")
ssh-keygen -R "$IP" >/dev/null 2>&1 || true
for i in $(seq 1 60); do ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 "root@$IP" true 2>/dev/null && break; sleep 5; done
ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 "root@$IP" true || die "$NAME at $IP: ssh never answered"
# cloud-init may still hold apt for a minute after first boot
ssh -i "$KEY" -o BatchMode=yes "root@$IP" 'cloud-init status --wait >/dev/null 2>&1 || true'
ssh -i "$KEY" -o BatchMode=yes -o ServerAliveInterval=15 "root@$IP" "ROLE=sweep SLOTS=1 WORKTREES='' BOX_HOSTNAME=$NAME P2P_PORTS='' bash -s" < "$HERE/provision.sh" 2>&1 | grep -E "^(changed|ok| |summary|die|error)" | tail -12
# the lease tool straight from this checkout (the worker's mirror has no master until a lane pushes one)
ssh -i "$KEY" -o BatchMode=yes "root@$IP" 'install -d -m 755 -o build -g build /srv/builds/_bin /srv/builds/_locks; cat > /srv/builds/_bin/lease.new && chmod 755 /srv/builds/_bin/lease.new && chown build:build /srv/builds/_bin/lease.new && mv /srv/builds/_bin/lease.new /srv/builds/_bin/lease' < "$HERE/lease.sh"
printf 'build@%s\n' "$IP" > "$HOME/.config/igneum/sweep-$n"
echo "SWEEP WORKER $NAME build@$IP ($TYPE, $LOC, $(ssh -i "$KEY" -o BatchMode=yes "build@$IP" nproc) threads, pool cores 8 to $(( $(ssh -i "$KEY" -o BatchMode=yes "build@$IP" nproc) - 1 ))), lease tool $(ssh -i "$KEY" -o BatchMode=yes "build@$IP" 'sha256sum /srv/builds/_bin/lease | cut -c1-8'), $(hourly "$TYPE") EUR/h; host file ~/.config/igneum/sweep-$n"
;;
list|cost)
"$HC" server list -l igneum=sweep -o json | python3 -c '
import sys,json,datetime,subprocess,os
d=json.load(sys.stdin); now=datetime.datetime.now(datetime.timezone.utc); tot_h=0.0; tot_eur=0.0; rate=float(os.environ.get("EUR_USD","1.09")); mode=sys.argv[1]
for s in d:
t=s["server_type"]["name"]; created=datetime.datetime.fromisoformat(s["created"].replace("Z","+00:00")); h=(now-created).total_seconds()/3600
pr=float([p for p in s["server_type"]["prices"] if p["location"]==s["datacenter"]["location"]["name"]][0]["price_hourly"]["gross"])
eur=h*pr; tot_h+=h; tot_eur+=eur
if mode=="list": print(f"{s[\"name\"]} {s[\"public_net\"][\"ipv4\"][\"ip\"]} {t} {s[\"datacenter\"][\"location\"][\"name\"]} {s[\"status\"]} up {h:.2f} h, EUR {eur:.2f} so far")
print(f"COST sweep workers: {len(d)} running, {tot_h:.2f} worker-hours, EUR {tot_eur:.2f} = USD {tot_eur*rate:.2f} so far (gross, hourly, EUR/USD {rate})")
' "$1"
;;
down)
n="${2:-}"; [ -n "$n" ] || die "down <n>"; NAME="igneum-sweep-$n"; IP=$("$HC" server ip "$NAME" 2>/dev/null || true)
if [ -n "$IP" ] && [ "${3:-}" != --force ]; then
held=$(ssh -i "$KEY" -o BatchMode=yes -o ConnectTimeout=8 "build@$IP" '/srv/builds/_bin/lease status 2>/dev/null | sed -n "/^leases:/,/^quiet:/p" | grep -c "^ pid"' 2>/dev/null || echo 0)
[ "$held" = 0 ] || die "$NAME holds $held lease(s) (lease status on build@$IP); --force to delete anyway"
fi
"$HC" server delete "$NAME" >/dev/null && echo "deleted $NAME at $(TZ=UTC date +%H:%M:%SZ)"; rm -f "$HOME/.config/igneum/sweep-$n"
;;
*) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
esac

View file

@ -20,6 +20,15 @@
# fewer are free; then runs the command under nice N (default 10) pinned to the cores taken, with "{cores}" in any
# argument replaced by the COUNT taken and "{cpuset}" by the set, and LEASE_CORES / LEASE_CPUSET exported. The sum of
# pooled threads on a box therefore never passes 88. Rule: no sweep starts except through `lease pool`.
# PRIORITY CLASSES (main, 7 Oct 2026 20:37 UK: the class v5 census sat behind eleven adversarial waiters): release (release
# builds and canaries) > v5 ("v5 gate", "v5 kit") > measure (measurement rows) > adv (adversarial sweeps). The class comes
# from --class <release|v5|measure|adv>, else from the owner (release, shipper, build-server: release; class-v5: v5;
# measure*: measure) or the label ("release", "canary", "pair": release; "v5 gate", "v5 kit": v5; "measure": measure), else
# adv. A higher-class waiter takes the next freed cores before any lower-class waiter regardless of arrival order (a lower
# class yields while a higher class waits). A lower-class holder above LEASE_PREEMPT_MIN_CORES (32) threads is pre-empted
# (SIGTERM to its command, which every sweep honours at its shard boundary; the lane re-queues it) when a higher class has
# waited LEASE_PREEMPT_S (120 s), newest holder first, one per period, logged in _log/preempt.log. The class stands in
# every wait and lease line (lease status).
# lease status every lease, the quiet holder and every waiter with its label
# lease reap a holder (lease, quiet or build slot) whose process has been STOPPED (state T) for 5 minutes or more is
# killed and its file cleared, one line each in _log/reaped.log; remote-run.sh's keeper calls this every
@ -96,8 +105,13 @@ run_cores() {
exit $rc
}
run_pool() {
local want="$1"; shift; local label="" owner="${IGNEUM_AGENT:-unknown}" nice=10 min="" reserve="${LEASE_POOL_RESERVE:-8}"
while [ $# -gt 0 ]; do case "$1" in --label) label="$2"; shift 2 ;; --owner) owner="$2"; shift 2 ;; --nice) nice="$2"; shift 2 ;; --min) min="$2"; shift 2 ;; --) shift; break ;; *) say "unknown option $1"; exit 2 ;; esac; done
local want="$1"; shift; local label="" owner="${IGNEUM_AGENT:-unknown}" nice=10 min="" reserve="${LEASE_POOL_RESERVE:-8}" class="" rank
while [ $# -gt 0 ]; do case "$1" in --label) label="$2"; shift 2 ;; --owner) owner="$2"; shift 2 ;; --nice) nice="$2"; shift 2 ;; --min) min="$2"; shift 2 ;; --class) class="$2"; shift 2 ;; --priority) class=v5; shift ;; --) shift; break ;; *) say "unknown option $1"; exit 2 ;; esac; done
if [ -z "$class" ]; then
case "$owner" in release|shipper|build-server) class=release ;; class-v5) class=v5 ;; measure*) class=measure ;; esac
[ -n "$class" ] || case "$label" in *release*|*canary*|*pair*) class=release ;; *"v5 gate"*|*"v5 kit"*) class=v5 ;; *measure*) class=measure ;; *) class=adv ;; esac
fi
case "$class" in release) rank=0 ;; v5) rank=1 ;; measure) rank=2 ;; adv) rank=3 ;; *) say "unknown class $class (release, v5, measure, adv)"; exit 2 ;; esac
[ "$want" -ge 1 ] 2>/dev/null || { say "pool needs a thread count"; exit 2; }
[ -n "$label" ] || { say "--label is required (the dashboard and the next lane read it)"; exit 2; }
[ $# -gt 0 ] || { say "no command"; exit 2; }
@ -105,29 +119,54 @@ run_pool() {
[ -n "$min" ] || min=$(( want < 16 ? want : 16 )); [ "$min" -gt "$want" ] && min=$want
local t0 waited=0 line waitfile="$LOCKS/wait-$$" fds=() cores=() c fd taken said=0
mkdir -p "$LOCKS"; t0=$(date +%s)
line="pid $$ since $(now) waited 0 s: lease pool $want (min $min): $label; owner=$owner"; printf '%s\n' "$line" > "$waitfile"
line="pid $$ since $(now) waited 0 s: lease pool $want (min $min) class $class/$rank: $label; owner=$owner"; printf '%s\n' "$line" > "$waitfile"
trap 'rm -f "$waitfile" "$LOCKS/lease-$$"' EXIT
local last_preempt=0 preempt_s="${LEASE_PREEMPT_S:-120}" preempt_min="${LEASE_PREEMPT_MIN_CORES:-32}" step="${LEASE_POOL_STEP_S:-10}" higher
while :; do
fds=(); cores=(); taken=0
for ((c = reserve; c < ncpu && taken < want; c++)); do
exec {fd}>>"$LOCKS/core-$c"
if flock -n "$fd"; then fds+=("$fd"); cores+=("$c"); taken=$((taken + 1)); else exec {fd}>&-; fi
done
[ "$taken" -ge "$min" ] && break
for fd in "${fds[@]}"; do exec {fd}>&-; done
# a waiter yields while a HIGHER class waits (its wait line carries "class <name>/<rank>")
higher=$(cat "$LOCKS"/wait-* 2>/dev/null | grep -v "^pid $$ " | sed -n 's/.* class [a-z0-9]*\/\([0-9]\):.*/\1/p' | awk -v r="$rank" '$1 < r' | head -1)
if [ -z "$higher" ]; then
for ((c = reserve; c < ncpu && taken < want; c++)); do
exec {fd}>>"$LOCKS/core-$c"
if flock -n "$fd"; then fds+=("$fd"); cores+=("$c"); taken=$((taken + 1)); else exec {fd}>&-; fi
done
[ "$taken" -ge "$min" ] && break
for fd in "${fds[@]}"; do exec {fd}>&-; done
fi
waited=$(( $(date +%s) - t0 )); [ "$waited" -ge 7200 ] && { say "gave up waiting for $min free pool cores after 2 h"; exit 75; }
[ "$said" = 0 ] && { say "$taken of $min pool cores free ($(held_cores | wc -w) leased); waiting"; said=1; }
printf '%s\n' "pid $$ since $(now) waited $waited s: lease pool $want (min $min, $taken free): $label; owner=$owner" > "$waitfile"
sleep 10
[ "$said" = 0 ] && { say "$taken of $min pool cores free ($(held_cores | wc -w) leased)$([ -n "$higher" ] && echo ', a higher class waits ahead'); waiting"; said=1; }
printf '%s\n' "pid $$ since $(now) waited $waited s: lease pool $want (min $min, $taken free) class $class/$rank: $label; owner=$owner" > "$waitfile"
# pre-emption: this class has waited preempt_s with the pool short: TERM to the NEWEST lower-class holder above preempt_min cores
if [ -z "$higher" ] && [ "$waited" -ge "$preempt_s" ] && [ $(( $(date +%s) - last_preempt )) -ge "$preempt_s" ]; then
local vf vpid vrank vcores
for vf in $(ls -t "$LOCKS"/lease-* 2>/dev/null); do
vrank=$(sed -n 's/.* class [a-z0-9]*\/\([0-9]\) cmd .*/\1/p' "$vf" | head -1); [ -n "$vrank" ] || continue
vcores=$(sed -n 's/.* lease pool \([0-9]*\) of .*/\1/p' "$vf" | head -1)
[ "$vrank" -gt "$rank" ] && [ "${vcores:-0}" -gt "$preempt_min" ] || continue
vpid=$(sed -n 's/.* cmd \([0-9]*\);.*/\1/p' "$vf" | head -1)
[ -n "$vpid" ] && kill -0 "$vpid" 2>/dev/null || continue
kill -TERM "$vpid" 2>/dev/null; last_preempt=$(date +%s); mkdir -p "$LOGS"
echo "$(now) class $class waiter pid $$ ($label; owner=$owner) waited $waited s: TERM to cmd $vpid of $(head -c 200 "$vf" | tr '\n' ' ')" >> "$LOGS/preempt.log"
say "pool short for $waited s: sent TERM to the newest lower-class holder's command (pid $vpid, $vcores cores)"
break
done
fi
sleep "$step"
done
waited=$(( $(date +%s) - t0 ))
local set; set=$(printf '%s,' "${cores[@]}"); set=${set%,}
line="pid $$ since $(now) waited $waited s: lease pool $taken of $want cores $set: $label; owner=$owner"; printf '%s\n' "$line" > "$LOCKS/lease-$$"; rm -f "$waitfile"
say "holding $taken pool cores ($set, waited $waited s): $label"
( for fd in "${fds[@]}"; do exec {fd}>&-; done; while kill -0 $$ 2>/dev/null; do touch "$LOCKS/lease-$$" 2>/dev/null; [ -s "$LOCKS/lease-$$" ] || printf '%s\n' "$line" > "$LOCKS/lease-$$"; sleep 20; done ) & local keeper=$!
line="pid $$ since $(now) waited $waited s: lease pool $taken of $want cores $set class $class/$rank: $label; owner=$owner"; printf '%s\n' "$line" > "$LOCKS/lease-$$"; rm -f "$waitfile"
say "holding $taken pool cores ($set, waited $waited s, class $class): $label"
local args=() a; for a in "$@"; do a=${a//\{cores\}/$taken}; a=${a//\{cpuset\}/$set}; args+=("$a"); done
export LEASE_CORES="$taken" LEASE_CPUSET="$set"
if [ "${LEASE_NO_PIN:-0}" = 1 ]; then "${args[@]}"; else nice -n "$nice" taskset -c "$set" "${args[@]}"; fi; local rc=$?
local cmdpid
if [ "${LEASE_NO_PIN:-0}" = 1 ]; then "${args[@]}" & else nice -n "$nice" taskset -c "$set" "${args[@]}" & fi; cmdpid=$!
# the command's pid in the holder line (a rank P waiter's TERM goes to it, never to the lease or its keeper)
line="pid $$ since $(now) waited $waited s: lease pool $taken of $want cores $set class $class/$rank cmd $cmdpid; $label; owner=$owner"; printf '%s\n' "$line" > "$LOCKS/lease-$$"
( for fd in "${fds[@]}"; do exec {fd}>&-; done; while kill -0 $$ 2>/dev/null; do touch "$LOCKS/lease-$$" 2>/dev/null; [ -s "$LOCKS/lease-$$" ] || printf '%s\n' "$line" > "$LOCKS/lease-$$"; sleep 20; done ) & local keeper=$!
trap 'kill -TERM "$cmdpid" 2>/dev/null' TERM INT
wait "$cmdpid"; local rc=$?; trap - TERM INT
pkill -P "$keeper" 2>/dev/null; kill "$keeper" 2>/dev/null; wait "$keeper" 2>/dev/null
rm -f "$LOCKS/lease-$$"; say "released $taken pool cores after $(( $(date +%s) - t0 )) s, exit $rc"
exit $rc
@ -199,9 +238,27 @@ self_test() {
bash "$me" pool 3 --min 1 --label P2 -- bash -c "date +%s.%N > '$t/p2.start'; echo {cores} > '$t/p2.n'" 2>/dev/null
python3 -c "import sys; sys.exit(0 if float(open('$t/p2.start').read()) >= float(open('$t/p1.end').read()) else 1)" || f "a pool run started while the pool was full instead of waiting"
[ "$(cat "$t/p2.n" 2>/dev/null)" = 3 ] || f "{cores} was not replaced by the count taken (got '$(cat "$t/p2.n" 2>/dev/null)')"
wait; unset LEASE_POOL_NCPU
wait
# 7. CLASS ORDER (known-failed first): an adv holder fills the pool; a v5 waiter and then an adv waiter queue; when the holder
# ends, the v5 waiter runs first and the adv waiter only after it, whatever the arrival order
export LEASE_PREEMPT_S=100 LEASE_POOL_STEP_S=1
bash "$me" pool 4 --min 4 --label HOLD --owner adv-test -- bash -c "date +%s > '$t/h.start'; sleep 4" 2>/dev/null & for i in $(seq 1 60); do [ -s "$t/h.start" ] && break; sleep 0.1; done; sleep 0.3
bash "$me" pool 4 --min 4 --label "v5 gate test" --owner class-v5 -- bash -c "date +%s.%N > '$t/v.start'; sleep 2; date +%s.%N > '$t/v.end'" 2>/dev/null & sleep 1.2
bash "$me" pool 4 --min 2 --label SWEEP --owner adv-test -- bash -c "date +%s.%N > '$t/s.start'" 2>/dev/null &
wait
[ -s "$t/v.start" ] && [ -s "$t/s.start" ] || f "class-order fixture did not run both waiters"
python3 -c "import sys; sys.exit(0 if float(open('$t/s.start').read()) >= float(open('$t/v.end').read()) else 1)" || f "a sweep took cores a waiting v5 gate asked for"
# 8. PRE-EMPTION: an adv holder above the core floor fills the pool; a release waiter pre-empts it (TERM to its command) after
# LEASE_PREEMPT_S and runs
export LEASE_PREEMPT_S=1 LEASE_PREEMPT_MIN_CORES=1
bash "$me" pool 4 --min 4 --label ADV --owner adv-test -- bash -c "trap 'echo termed > \"$t/adv.termed\"; exit 0' TERM; date +%s > '$t/adv.start'; sleep 30 & wait" 2>/dev/null & for i in $(seq 1 60); do [ -s "$t/adv.start" ] && break; sleep 0.1; done; sleep 0.3
bash "$me" pool 4 --min 2 --label "release canary test" --owner release -- bash -c "date +%s > '$t/p.start'" 2>/dev/null
[ -s "$t/adv.termed" ] || f "the release waiter did not pre-empt the adv holder"
[ -s "$t/p.start" ] || f "the release waiter did not run after the pre-emption"
wait; unset LEASE_PREEMPT_S LEASE_POOL_STEP_S LEASE_POOL_NCPU LEASE_PREEMPT_MIN_CORES
grep -q "class release waiter" "$t/log/preempt.log" 2>/dev/null || f "no preempt.log line"
[ -z "$(ls "$t/locks" | grep -E '^(lease|wait)-')" ] || f "pool lease or wait files left behind: $(ls "$t/locks")"
[ "$fail" = 0 ] && echo "lease self-test: disjoint leases run together, a shared core waits, quiet is refused beside a slot or a lease and capped, a stopped holder is reaped after the window, a running one is kept, a pool run takes the free pool cores and the next waits for them"
[ "$fail" = 0 ] && echo "lease self-test: disjoint leases run together, a shared core waits, quiet is refused beside a slot or a lease and capped, a stopped holder is reaped after the window, a running one is kept, a pool run takes the free pool cores and the next waits for them, a higher class is served before a lower one whatever the arrival order, a release waiter pre-empts an adv holder above the floor"
return $fail
}
case "${1:-}" in
@ -211,5 +268,5 @@ case "${1:-}" in
status) status ;;
reap) reap ;;
--self-test) self_test ;;
*) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
*) sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
esac

View file

@ -61,6 +61,7 @@ case "$BOX_HOSTNAME" in *-2|*-4) [ -n "$SLOTS_GIVEN" ] || SLOTS=3 ;; esac # bu
WORKERS_HOST="${WORKERS_HOST:-build.igneum.network}" # the dashboard feed's HTTPS name (A record in deSEC, 6 Oct 2026)
SSH_PUBKEY="${SSH_PUBKEY:-}"
BUILD_USER=build
ROLE="${ROLE:-box}" # box (the default) or sweep: a rented cloud sweep worker, provisioned minus the runner (7 Oct 2026)
BUILD_HOME=/home/$BUILD_USER
# the GitHub Actions runner (step_runner, 6 October 2026 evening): a dedicated user, never build and never root
RUNNER_USER=runner
@ -673,14 +674,21 @@ do_provision() {
step_profile
step_node
step_sshd
step_caddy
step_cuda
step_ufw
step_runner
step_cargo_tools
step_zig
step_night
step_headless_check
if [ "$ROLE" = sweep ]; then
# a rented SWEEP WORKER (Hetzner Cloud CCX, by the hour, main's order 7 Oct 2026): the build user, toolchain, sccache, mirrors,
# lease tool and firewall only; no dashboard feed, no CUDA, no CI runner, no night battery, no Chromium, no zig
step_ufw
step_cargo_tools
else
step_caddy
step_cuda
step_ufw
step_runner
step_cargo_tools
step_zig
step_night
step_headless_check
fi
step_summary
log "done"
}