Skip to content

Commit a91fb17

Browse files
committed
Fix background failure reporting and D2 formatting
1 parent 25a8a68 commit a91fb17

14 files changed

Lines changed: 393 additions & 71 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,12 @@ Currently used by:
2121
And in our internal monorepo.
2222

2323
For robust example usage of the flag parser see [./examples/date.sh](./examples/date.sh).
24+
25+
Use `runjob name command` for foreground work. For parallel work, replace
26+
`runjob name command &` with `runjob_bg name command`, then call `waitjobs`.
27+
The launcher records each process ID immediately so completed failures cannot be
28+
lost from the shell's job list. `waitjobs` rejects unregistered background jobs
29+
that it can still detect; migrate callers before updating this shared library.
30+
31+
`ci_waitjobs` waits for jobs and checks generated files. Commit-message policy is
32+
opt-in through `bin/nofixups.sh`; cleanup and notifications do not enforce it.

‎bin/fmt.sh‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ d2fmt() {
5555
curl -fsSL https://d2lang.com/install.sh | sh -s --
5656
)
5757
fi
58-
sh_c XARGS_N=1 hide xargsd --null "'\.\(d2\)$'" d2 fmt
58+
sh_c XARGS_N=1 hide xargsd "'\.\(d2\)$'" d2 fmt
5959
}
6060

6161
main() {
@@ -66,22 +66,22 @@ main() {
6666
# runjob trailing-whitespace trailing_whitespace
6767
# fi
6868
if <"$CHANGED_FILES" grep -q '\.\(md\)$'; then
69-
runjob mdtocsubst mdtocsubst_xargsd &
69+
runjob_bg mdtocsubst mdtocsubst_xargsd
7070
fi
7171
if search_up go.mod >/dev/null; then
72-
runjob go.mod gomodtidy &
72+
runjob_bg go.mod gomodtidy
7373
fi
7474
if <"$CHANGED_FILES" grep -q '\.\(go\)$'; then
75-
runjob gofmt &
75+
runjob_bg gofmt
7676
fi
7777
if search_up package.json >/dev/null; then
78-
runjob package.json pkgjson &
78+
runjob_bg package.json pkgjson
7979
fi
8080
if <"$CHANGED_FILES" grep -q '\.\(js\|jsx\|ts\|tsx\|scss\|css\|html\)$'; then
81-
runjob prettier &
81+
runjob_bg prettier
8282
fi
8383
if <"$CHANGED_FILES" grep -qm1 '\.\(d2\)$'; then
84-
runjob d2fmt &
84+
runjob_bg d2fmt
8585
fi
8686
waitjobs
8787
}

‎ci/ci.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ fmtgen() {
1313

1414
job_parseflags "$@"
1515
ensure_git_base
16-
fmtgen &
16+
_job_bg fmtgen
1717
if is_changed lib; then
18-
runjob test ./ci/test.sh &
18+
runjob_bg test ./ci/test.sh
1919
fi
2020
ci_waitjobs

‎ci/test.sh‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,25 @@ cd - >/dev/null
99
job_parseflags "$@"
1010
if is_changed ./lib/test.sh ./lib/rand.sh ./lib/log.sh \
1111
./lib/temp.sh; then
12-
runjob log ./lib/log_test.sh &
12+
runjob_bg log ./lib/log_test.sh
1313
fi
1414
if is_changed ./lib/test.sh ./lib/rand.sh ./lib/log.sh \
1515
./lib/flag.sh ./lib/temp.sh; then
16-
runjob flag ./lib/flag_test.sh &
16+
runjob_bg flag ./lib/flag_test.sh
1717
fi
1818
if is_changed \
1919
./lib/test.sh ./lib/rand.sh ./lib/log.sh ./lib/git.sh \
2020
./lib/flag.sh ./lib/ci.sh ./lib/job.sh ./lib/notify.sh \
2121
./lib/temp.sh ./lib/release.sh; then
22-
runjob make ./lib/make_test.sh &
22+
runjob_bg make ./lib/make_test.sh
23+
fi
24+
if is_changed ./lib/job.sh ./lib/job_test.sh ./lib/ci.sh ./lib/ci_test.sh \
25+
./lib/notify.sh ./lib/log.sh ./lib/test.sh ./lib/temp.sh ./ci/test.sh; then
26+
runjob_bg job ./lib/job_test.sh
27+
runjob_bg ci ./lib/ci_test.sh
28+
fi
29+
if is_changed ./bin/fmt.sh ./lib/fmt_test.sh ./lib/git.sh ./lib/job.sh \
30+
./lib/log.sh ./lib/test.sh ./lib/temp.sh ./ci/test.sh; then
31+
runjob_bg formatter ./lib/fmt_test.sh
2332
fi
2433
waitjobs

‎lib.sh‎

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ci_go_lint() {
1111
ci_waitjobs() {
1212
if [ -z "${CI-}" ]; then
1313
waitjobs
14-
return 0
14+
return "$?"
1515
fi
1616

1717
capcode waitjobs
@@ -24,7 +24,6 @@ ci_waitjobs() {
2424
notify
2525
return "$code"
2626
fi
27-
capcode nofixups
2827
notify
2928
return "$code"
3029
}
@@ -435,6 +434,8 @@ LIB_JOB=1
435434
# and propogating of signals. Not sure how to debug even without something like gdb and
436435
# going through the source code of the shell too.
437436
runjob() {(
437+
# A child must never wait on its parent's other jobs.
438+
JOB_PIDS=
438439
jobname=$1
439440
export JOBNAME=${JOBNAME+$JOBNAME/}$jobname
440441
shift
@@ -462,16 +463,29 @@ runjob() {(
462463
# We add the prefix to all lines and remove any warning lines about recursive make.
463464
# We cannot silence these with -s which is unfortunate.
464465
(sed -e "s#^#$(echop "$jobname"): #" -e "/make\[.\]: warning: -j/d" "$stdout" || true) &
466+
RUNJOB_READERS="$!"
465467
# This intentionally does not output to our stderr, it becomes our stdout.
466468
(sed -e "s#^#$(echop "$jobname"): #" -e "/make\[.\]: warning: -j/d" "$stderr" || true) &
469+
RUNJOB_READERS="$RUNJOB_READERS $!"
467470

468471
start="$(awk 'BEGIN{srand(); print srand()}')"
469472
trap runjob_exittrap EXIT
470473
# For some reason without wrapping this in a subshell, the waitjobs in subjob
471474
# case_notequal_sign of ./lib/flags_test.sh freezes.
472-
( eval "$*" >"$stdout" 2>"$stderr" )
475+
( JOB_PIDS=; eval "$*" >"$stdout" 2>"$stderr" )
473476
)}
474477

478+
# Use this instead of `runjob ... &` so the shell retains the child's exit status.
479+
runjob_bg() {
480+
_job_bg runjob "$@"
481+
}
482+
483+
# Register an unprefixed helper without adding a job-filter level.
484+
_job_bg() {
485+
JOB_PIDS= "$@" &
486+
JOB_PIDS="${JOB_PIDS-} $!"
487+
}
488+
475489
_runjob_filter() {
476490
if [ -z "${JOBFILTER-}" ]; then
477491
return 0
@@ -511,42 +525,62 @@ runjob_filter() {
511525

512526
runjob_exittrap() {
513527
code="$?"
528+
trap - EXIT
514529
end="$(awk 'BEGIN{srand(); print srand()}')"
515530
dur="$((end - start))"
516531

517-
waitjobs_sigtrap
532+
# Preserve the existing output-reader shutdown behavior. Their status must
533+
# never replace the command's exit status.
534+
for reader_pid in $RUNJOB_READERS; do
535+
kill "$reader_pid" 2>/dev/null || true
536+
done
537+
for reader_pid in $RUNJOB_READERS; do
538+
wait "$reader_pid" 2>/dev/null || true
539+
done
518540
if [ "$code" -eq 0 ]; then
519541
echop "$jobname\$" "$(setaf 2 success)" "($(echo_dur "$dur"))"
520542
else
521543
echop "$jobname\$" "$(setaf 1 failure)" "($(echo_dur "$dur"))"
522544
fi
545+
exit "$code"
523546
}
524547

525548
waitjobs() {
549+
waitjobs_status=0
550+
# Fail clearly for legacy callers rather than silently dropping their failures.
551+
# `jobs` must run in this shell: dash has an empty job table in substitutions.
526552
wait_tmpdir="$(mktempd)"
527-
jobs -l > "$wait_tmpdir/jobsl"
528-
trap waitjobs_sigtrap INT TERM
529-
530-
jobs -p > "$wait_tmpdir/jobsp"
553+
jobs -p >"$wait_tmpdir/jobsp"
531554
for pid in $(cat "$wait_tmpdir/jobsp"); do
555+
case " ${JOB_PIDS-} " in
556+
*" $pid "*) ;;
557+
*)
558+
echoerr "unregistered background job $pid: use runjob_bg instead of runjob ... &"
559+
JOB_PIDS="${JOB_PIDS-} $pid"
560+
waitjobs_status=1
561+
;;
562+
esac
563+
done
564+
trap waitjobs_sigtrap INT TERM
565+
set -- ${JOB_PIDS-}
566+
for pid do
532567
if ! wait "$pid"; then
533-
caterr <<EOF
534-
failed to wait on $pid:
535-
$(<"$wait_tmpdir/jobsl" grep "$pid")
536-
EOF
537-
FAILURE=1
568+
echoerr "background job $pid failed"
569+
waitjobs_status=1
538570
fi
571+
shift
572+
JOB_PIDS="$*"
539573
done
540-
if [ -n "${FAILURE-}" ]; then
541-
return 1
542-
fi
574+
JOB_PIDS=
575+
trap - INT TERM
576+
return "$waitjobs_status"
543577
}
544578

545579
waitjobs_sigtrap() {
546-
for pid in $(jobs -p); do
580+
waitjobs_status=1
581+
for pid in ${JOB_PIDS-}; do
547582
kill "$pid" 2> /dev/null || true
548583
done
549-
waitjobs
550584
}
551585

552586
job_parseflags() {
@@ -1060,9 +1094,6 @@ EOF
10601094
return 1
10611095
fi
10621096

1063-
if [ "$code" -eq 0 ]; then
1064-
capcode nofixups
1065-
fi
10661097
if [ "$code" -eq 0 ]; then
10671098
status=success
10681099
emoji=🟢

‎lib/ci.sh‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ ci_go_lint() {
1515
ci_waitjobs() {
1616
if [ -z "${CI-}" ]; then
1717
waitjobs
18-
return 0
18+
return "$?"
1919
fi
2020

2121
capcode waitjobs
@@ -28,7 +28,6 @@ ci_waitjobs() {
2828
notify
2929
return "$code"
3030
fi
31-
capcode nofixups
3231
notify
3332
return "$code"
3433
}

‎lib/ci_test.sh‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/sh
2+
set -eu
3+
cd -- "$(dirname "$0")"
4+
. ./test.sh
5+
. ./ci.sh
6+
cd - >/dev/null
7+
8+
case_commit_wording() {
9+
CI=1
10+
CI_MAKE_ROOT=0
11+
GIT_BASE=
12+
cd "$(mktempd)"
13+
git_pure init -q
14+
printf 'tracked\n' >file
15+
git_pure add file
16+
git_pure -c commit.gpgsign=false commit -qm 'fixup! permitted by caller'
17+
ci_waitjobs
18+
# The opt-in standalone check still works.
19+
if nofixups; then
20+
echoerr "standalone nofixups check should reject the fixture"
21+
return 1
22+
fi
23+
printf 'dirty\n' >>file
24+
if ci_waitjobs; then
25+
echoerr "CI cleanup accepted a dirty tracked file"
26+
return 1
27+
fi
28+
}
29+
30+
case_local_failure() {
31+
CI=
32+
runjob_bg failing false
33+
if ci_waitjobs; then
34+
echoerr "local cleanup accepted a failed background job"
35+
return 1
36+
fi
37+
}
38+
39+
case_notification_status() {
40+
CI=1
41+
CI_MAKE_ROOT=1
42+
GITHUB_REF_PROTECTED=true
43+
GITHUB_RUN_ID=1
44+
GITHUB_JOB=test
45+
GITHUB_REPOSITORY=test/repo
46+
GITHUB_WORKFLOW=test
47+
GITHUB_TOKEN=test
48+
DISCORD_WEBHOOK_URL=https://example.invalid/webhook
49+
nofixups() { echoerr "notification invoked nofixups"; return 1; }
50+
curl() {
51+
case "$*" in
52+
*' -X POST '*) return 0 ;;
53+
*) printf '%s\n' '{"jobs":[{"name":"test","html_url":"https://example.invalid/job"}]}' ;;
54+
esac
55+
}
56+
code=0
57+
notify
58+
assert code 0
59+
}
60+
61+
job_parseflags "$@"
62+
runjob case_commit_wording
63+
runjob case_local_failure
64+
runjob case_notification_status

‎lib/flag_test.sh‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ case_notequal_sign() {
153153
assert_term '' "$@"
154154
}
155155

156-
runjob case_with_args &
157-
runjob case_without_args &
158-
runjob case_term &
156+
runjob_bg case_with_args
157+
runjob_bg case_without_args
158+
runjob_bg case_term
159159
waitjobs
160160
}
161161

@@ -253,11 +253,11 @@ case_flag_fmt() {
253253
}
254254

255255
job_parseflags "$@"
256-
runjob case_term &
257-
runjob case_equal_sign &
258-
runjob case_notequal_sign &
259-
runjob case_reqarg &
260-
runjob case_nonemptyarg &
261-
runjob case_noarg &
262-
runjob case_flag_fmt &
256+
runjob_bg case_term
257+
runjob_bg case_equal_sign
258+
runjob_bg case_notequal_sign
259+
runjob_bg case_reqarg
260+
runjob_bg case_nonemptyarg
261+
runjob_bg case_noarg
262+
runjob_bg case_flag_fmt
263263
waitjobs

‎lib/fmt_test.sh‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/sh
2+
set -eu
3+
cd -- "$(dirname "$0")"
4+
. ./test.sh
5+
formatter=$(cd ../bin && pwd)/fmt.sh
6+
cd - >/dev/null
7+
8+
case_d2_formatter() {
9+
directory=$(mktempd)
10+
mkdir "$directory/bin" "$directory/repo"
11+
cat >"$directory/bin/d2" <<'EOF'
12+
#!/bin/sh
13+
printf '%s\n' "$*" >>"$FORMATTER_CALLS"
14+
exit "${FORMATTER_STATUS:-0}"
15+
EOF
16+
chmod +x "$directory/bin/d2"
17+
PATH="$directory/bin:$PATH"
18+
export PATH FORMATTER_CALLS="$directory/calls"
19+
# Match git's physical root path (macOS temporary directories use a symlink).
20+
cd -P "$directory/repo"
21+
git_pure init -q
22+
printf 'a -> b\n' >fixture.d2
23+
printf 'unchanged\n' >unchanged.d2
24+
git_pure add .
25+
git_pure -c commit.gpgsign=false commit -qm fixture
26+
export CI=1 GIT_BASE=HEAD OS=linux
27+
printf 'a->b\n' >fixture.d2
28+
29+
"$formatter"
30+
assert calls "$(cat "$FORMATTER_CALLS")" 'fmt fixture.d2'
31+
export FORMATTER_STATUS=23
32+
if "$formatter"; then
33+
echoerr "formatter failure was ignored"
34+
return 1
35+
fi
36+
}
37+
38+
job_parseflags "$@"
39+
runjob case_d2_formatter

0 commit comments

Comments
 (0)