@@ -510,6 +510,60 @@ get_compose_flags() {
510510 fi
511511}
512512
513+ # Run `docker compose <args...>` with a compact summary on success and a
514+ # surfaced error banner on failure.
515+ #
516+ # Usage: _compose_run_with_summary <verb> <compose_args...>
517+ # <verb> Human-readable gerund phrase, e.g. "Restarting all services"
518+ # <compose_args> Everything that goes after `docker compose`, including
519+ # any `-f` flags resolved by get_compose_flags.
520+ #
521+ # Behavior:
522+ # - Runs `docker compose --progress quiet <compose_args>`, capturing
523+ # stdout+stderr to a mktemp log file.
524+ # - On success: prints "<verb> — done" and removes the log file.
525+ # - On failure: prints an error banner, surfaces up to 20 lines matching
526+ # /error|unhealthy|failed|dependency/, preserves the full
527+ # log file for inspection, and returns the compose exit
528+ # code so the caller (under `set -e`) aborts with it.
529+ #
530+ # The summary grep pipeline can legitimately produce zero matches if the
531+ # failing compose output has no error-keyword hits. `upstream/main` today
532+ # runs under `set -e` only (no `pipefail`), so grep's exit 1 is absorbed
533+ # by the pipeline's final-stage exit and the function continues to the
534+ # log-path surface below. When pipefail is eventually adopted (sibling
535+ # nounset/exit-code audit change), grep's no-match or a SIGPIPE from
536+ # `head -20` on >20 matches would abort this function before the caller
537+ # sees the compose log path or the compose exit code. `|| warn "..."`
538+ # is the project-blessed form (per CLAUDE.md) for "tolerate this specific
539+ # non-match and log why the summary is empty" — it costs nothing today
540+ # and keeps the function correct under future pipefail.
541+ _compose_run_with_summary () {
542+ local _verb=" $1 " ; shift
543+ log " ${_verb} ..."
544+
545+ local _compose_log
546+ _compose_log=$( mktemp)
547+
548+ local _rc=0
549+ docker compose --progress quiet " $@ " > " $_compose_log " 2>&1 || _rc=$?
550+
551+ if (( _rc == 0 )) ; then
552+ success " ${_verb} — done"
553+ rm -f " $_compose_log "
554+ return 0
555+ fi
556+
557+ log_error " ${_verb} failed:"
558+ grep -iE ' error|unhealthy|failed|dependency' " $_compose_log " \
559+ | sed ' s/^/ /' \
560+ | head -20 \
561+ || warn " (no error keywords matched in compose log)"
562+ echo " "
563+ log " Full compose output: $_compose_log "
564+ return " $_rc "
565+ }
566+
513567# =============================================================================
514568# Commands
515569# =============================================================================
@@ -778,14 +832,10 @@ cmd_restart() {
778832 read -ra flags <<< " $flags_str"
779833
780834 if [[ -z " $service " ]]; then
781- log " Restarting all services..."
782- docker compose " ${flags[@]} " up -d
783- success " All services restarted"
835+ _compose_run_with_summary " Restarting all services" " ${flags[@]} " up -d
784836 else
785837 service=$( resolve_service " $service " )
786- log " Restarting $service ..."
787- docker compose " ${flags[@]} " up -d " $service "
788- success " $service restarted"
838+ _compose_run_with_summary " Restarting $service " " ${flags[@]} " up -d " $service "
789839 fi
790840}
791841
@@ -801,14 +851,10 @@ cmd_stop() {
801851 read -ra flags <<< " $flags_str"
802852
803853 if [[ -z " $service " ]]; then
804- log " Stopping all services..."
805- docker compose " ${flags[@]} " down
806- success " All services stopped"
854+ _compose_run_with_summary " Stopping all services" " ${flags[@]} " down
807855 else
808856 service=$( resolve_service " $service " )
809- log " Stopping $service ..."
810- docker compose " ${flags[@]} " stop " $service "
811- success " $service stopped"
857+ _compose_run_with_summary " Stopping $service " " ${flags[@]} " stop " $service "
812858 fi
813859}
814860
@@ -826,15 +872,11 @@ cmd_start() {
826872 read -ra flags <<< " $flags_str"
827873
828874 if [[ -z " $service " ]]; then
829- log " Starting all services..."
830- docker compose " ${flags[@]} " up -d
831- success " All services started"
875+ _compose_run_with_summary " Starting all services" " ${flags[@]} " up -d
832876 else
833877 service=$( resolve_service " $service " )
834878 _run_hook " $service " " pre_start"
835- log " Starting $service ..."
836- docker compose " ${flags[@]} " up -d " $service "
837- success " $service started"
879+ _compose_run_with_summary " Starting $service " " ${flags[@]} " up -d " $service "
838880 _run_hook " $service " " post_start"
839881 fi
840882}
@@ -1040,13 +1082,11 @@ cmd_update() {
10401082 warn " dream-update.sh and dream-backup.sh not found; skipping pre-update snapshot."
10411083 fi
10421084
1043- log " Pulling latest images..."
1044- if ! docker compose " ${flags[@]} " pull; then
1085+ if ! _compose_run_with_summary " Pulling latest images" " ${flags[@]} " pull; then
10451086 error " Failed to pull latest images"
10461087 fi
10471088
1048- log " Recreating containers with new images..."
1049- if ! docker compose " ${flags[@]} " up -d --force-recreate; then
1089+ if ! _compose_run_with_summary " Recreating containers with new images" " ${flags[@]} " up -d --force-recreate; then
10501090 error " Failed to recreate containers. Run 'dream rollback' to restore previous state."
10511091 fi
10521092
0 commit comments