Skip to content

Commit e179120

Browse files
authored
Merge pull request #7 from KingPin/feat/summary-list-names
Name updated/failed containers in the run summary
2 parents d996f7b + 37d4861 commit e179120

3 files changed

Lines changed: 51 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Notification state is persisted in `${CACHE_LOCATION}/hoist-<safe-name>.notified
7777

7878
Each `process_container` invocation appends outcome tokens (`updated`, `update_failed`, `notified`, `no_change`, `skipped`, `not_compose_managed`, `would_update`, `would_notify`, `paused`, `constraint_blocked`, `group_aborted`, `unhealthy`, `rolled_back`, `rollback_failed`) to `${CACHE_LOCATION}/hoist-<safe-name>.run-result`. After all containers finish, `print_summary` aggregates them into a single one-line summary. Result files (and `hoist-group-*.failed` flags) are wiped at the start of each run. The end-of-run Healthchecks.io ping is `/fail` if any container produced `update_failed`, `unhealthy`, or `rollback_failed`; otherwise success. **`not_compose_managed`** flags a container with hoist `update`/`notify` labels but no `com.docker.compose.*` metadata — it's logged unconditionally and can't be acted on.
7979

80+
When `SUMMARY_LIST_NAMES` is true (default), the summary also names which containers landed in the `updated`/`update_failed` buckets, e.g. `4 updated [web, api, db, cache] (1 failed: worker)`, instead of just counts — the other buckets stay counts-only to avoid bloating the line on routine runs. `process_container` writes each container's raw name once, up front, to a sibling `${CACHE_LOCATION}/hoist-<safe-name>.name` file (wiped alongside `.run-result`/`.rollup` at run start); `print_summary` joins it back in only for containers whose `.run-result` contained `updated` or `update_failed`. Set `SUMMARY_LIST_NAMES=false` to keep the old count-only text (e.g. for scripts that parse the summary line verbatim).
81+
8082
### Run lock and exit code
8183

8284
`_acquire_run_lock` (`hoist.sh:417`) takes an exclusive `flock` on `${CACHE_LOCATION}/hoist.lock` at startup (with an `mkdir`-plus-stale-PID fallback when `flock` is absent). A second run that finds the lock held logs and exits 0 rather than racing over shared state; `--list`/`--status` skips the lock. **flock fd handoff:** bash sets no close-on-exec on the `{_LOCK_FD}` redirection fd, so docker/compose children and `&`-spawned workers would otherwise inherit it and keep the lock held — a Ctrl+C that orphans a `docker compose pull` subprocess would wedge every later run with "Another hoist run holds the lock". So after acquiring, hoist hands the held fd to a single childless holder process (`( exec sleep … ) &`, `disown`ed so the parallel pool's `wait`/`wait -n` ignore it), closes its own copy of the fd, and arms an EXIT trap (also reached via `_on_signal`'s `exit`) that kills the holder — releasing the lock the instant hoist truly exits, regardless of orphaned docker children. After `print_summary`, hoist **exits non-zero** if any container produced `update_failed`, `unhealthy`, or `rollback_failed` (drives `Type=oneshot` systemd, cron `MAILTO`, CI). Boolean gates (`update`/`notify`/`rollback`/`healthcheck.wait`, `ROLLBACK_DEFAULT`) are routed through `_is_true`, which accepts `true|1|yes|on` case-insensitively.

hoist.conf.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
# Log containers skipped due to missing hoist labels (default: false). Automatically enabled by --dry-run.
2525
# VERBOSE=true
2626

27+
# Name updated/failed containers in the end-of-run summary line, e.g.
28+
# "4 updated [web, api, db, cache] (1 failed: worker)" instead of just counts (default: true).
29+
# SUMMARY_LIST_NAMES=false
30+
2731
# Maximum time in seconds for webhook HTTP requests (default: 30).
2832
# CURL_TIMEOUT=30
2933

hoist.sh

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
HOIST_VERSION="1.8.3"
2+
HOIST_VERSION="1.9.0"
33
HOIST_REPO="KingPin/hoist"
44

55
DOCKER_BINARY="${DOCKER_BINARY:-$(which docker)}"
@@ -30,6 +30,7 @@ ROLLBACK_DEFAULT="false"
3030
MAINTENANCE_WINDOW=""
3131
HOIST_HOSTNAME=""
3232
VERBOSE=false
33+
SUMMARY_LIST_NAMES=true
3334
CURL_TIMEOUT="${CURL_TIMEOUT:-30}"
3435
UPDATE_CHECK="${UPDATE_CHECK:-notify}"
3536
# Self-update API check is cached this many seconds so cron-fired runs don't hit
@@ -2541,6 +2542,10 @@ process_container() {
25412542
local _result_file="${CACHE_LOCATION}/hoist-${safe_name}.run-result"
25422543
# Refuse to record outcomes through a planted symlink (shared-/tmp hardening).
25432544
_state_path_safe "$_result_file" || return 1
2545+
if [[ $SUMMARY_LIST_NAMES == true ]]; then
2546+
local _name_file="${CACHE_LOCATION}/hoist-${safe_name}.name"
2547+
_state_path_safe "$_name_file" && printf '%s' "$container_name" > "$_name_file"
2548+
fi
25442549
local -a _tokens=()
25452550
log "$container_name: Checking..."
25462551

@@ -2580,6 +2585,9 @@ process_container() {
25802585
_dedup_key=${_dedup_key// /_}
25812586
if ! mkdir "${CACHE_LOCATION}/hoist-compose-${_dedup_key}.deduped" 2>/dev/null; then
25822587
[[ $VERBOSE == true ]] && log "$container_name: replica of '$docker_compose_service' already processed this run — skipping"
2588+
# No .run-result entry is written on this path — drop the .name
2589+
# file written earlier so it doesn't orphan until the next run's wipe.
2590+
[[ $SUMMARY_LIST_NAMES == true ]] && rm -f "${CACHE_LOCATION}/hoist-${safe_name}.name"
25832591
return 0
25842592
fi
25852593
fi
@@ -2626,12 +2634,14 @@ print_summary() {
26262634
local paused=0 constraint_blocked=0 group_aborted=0
26272635
local unhealthy=0 rolled_back=0 rollback_failed=0 not_compose_managed=0
26282636
local f token
2637+
local -a updated_names=() failed_names=()
26292638
for f in "${CACHE_LOCATION}"/hoist-*.run-result; do
26302639
[[ -f $f ]] || continue
2640+
local _saw_updated=false _saw_update_failed=false
26312641
while IFS= read -r token; do
26322642
case "$token" in
2633-
updated) (( updated++ )) ;;
2634-
update_failed) (( update_failed++ )) ;;
2643+
updated) (( updated++ )); _saw_updated=true ;;
2644+
update_failed) (( update_failed++ )); _saw_update_failed=true ;;
26352645
notified) (( notified++ )) ;;
26362646
no_change) (( no_change++ )) ;;
26372647
skipped) (( skipped++ )) ;;
@@ -2646,8 +2656,30 @@ print_summary() {
26462656
not_compose_managed) (( not_compose_managed++ )) ;;
26472657
esac
26482658
done < "$f"
2659+
if [[ $SUMMARY_LIST_NAMES == true && ( $_saw_updated == true || $_saw_update_failed == true ) ]]; then
2660+
local _cname _name_file="${f%.run-result}.name"
2661+
_cname="${f##*/hoist-}"
2662+
_cname="${_cname%.run-result}"
2663+
if [[ -L $_name_file ]]; then
2664+
log "Refusing to read state file through a symlink: $_name_file"
2665+
elif [[ -r $_name_file ]]; then
2666+
_cname=$(cat "$_name_file")
2667+
fi
2668+
[[ $_saw_updated == true ]] && updated_names+=("$_cname")
2669+
[[ $_saw_update_failed == true ]] && failed_names+=("$_cname")
2670+
fi
26492671
done
26502672

2673+
local updated_joined="" failed_joined=""
2674+
if (( ${#updated_names[@]} )); then
2675+
updated_joined=$(printf '%s, ' "${updated_names[@]}")
2676+
updated_joined=${updated_joined%, }
2677+
fi
2678+
if (( ${#failed_names[@]} )); then
2679+
failed_joined=$(printf '%s, ' "${failed_names[@]}")
2680+
failed_joined=${failed_joined%, }
2681+
fi
2682+
26512683
local msg
26522684
if [[ $DRY_RUN == true ]]; then
26532685
# Dry-run does not pull, so it cannot know which containers actually have
@@ -2656,7 +2688,14 @@ print_summary() {
26562688
msg="Run complete (dry-run): ${would_update} eligible to update, ${would_notify} eligible to notify (not pulled — run live to detect available updates), ${skipped} skipped"
26572689
else
26582690
local updated_part="${updated} updated"
2659-
[[ $update_failed -gt 0 ]] && updated_part+=" (${update_failed} failed)"
2691+
if [[ $SUMMARY_LIST_NAMES == true && $updated -gt 0 ]]; then
2692+
updated_part+=" [${updated_joined}]"
2693+
fi
2694+
if [[ $update_failed -gt 0 ]]; then
2695+
updated_part+=" (${update_failed} failed"
2696+
[[ $SUMMARY_LIST_NAMES == true ]] && updated_part+=": ${failed_joined}"
2697+
updated_part+=")"
2698+
fi
26602699
msg="Run complete: ${updated_part}, ${notified} notified, ${no_change} no-change, ${skipped} skipped"
26612700
fi
26622701
[[ $unhealthy -gt 0 ]] && msg+=", ${unhealthy} unhealthy"
@@ -2801,10 +2840,10 @@ if [[ $DO_LIST == true ]]; then
28012840
fi
28022841

28032842
# Serialize runs (after --list, which is read-only and needs no lock) so the
2804-
# shared run-result/rollup state below isn't clobbered by an overlapping run.
2843+
# shared run-result/rollup/name state below isn't clobbered by an overlapping run.
28052844
_acquire_run_lock
28062845

2807-
rm -f "${CACHE_LOCATION}"/hoist-*.run-result "${CACHE_LOCATION}"/hoist-*.rollup "${CACHE_LOCATION}"/hoist-group-*.failed 2>/dev/null || true
2846+
rm -f "${CACHE_LOCATION}"/hoist-*.run-result "${CACHE_LOCATION}"/hoist-*.rollup "${CACHE_LOCATION}"/hoist-*.name "${CACHE_LOCATION}"/hoist-group-*.failed 2>/dev/null || true
28082847
rmdir "${CACHE_LOCATION}"/hoist-compose-*.deduped 2>/dev/null || true
28092848
setup_environment
28102849
check_maintenance_window

0 commit comments

Comments
 (0)